From 83bc3810a930c60973e60a7a02b677c2fb1f444c Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 5 May 2023 23:45:49 -0700 Subject: [PATCH 01/10] [Tests] simplify tests --- test/no_only.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/no_only.js b/test/no_only.js index 4e755c98..95ad07eb 100644 --- a/test/no_only.js +++ b/test/no_only.js @@ -8,12 +8,12 @@ var stripFullStack = require('./common').stripFullStack; var tapeBin = 'node ' + path.join(__dirname, '../bin/tape'); -var expectedExitCodeFailure = (/^0\.10\.\d+$/).test(process.versions.node); var expectedStackTraceBug = (/^3\.[012]\.\d+$/).test(process.versions.node); // https://github.com/nodejs/node/issues/2581 +var expectedExitCodeOnError = (/^0\.(?:9|10)/).test(process.versions.node) ? 8 : 1; // node v0.9 sets this exit code to 8, for some reason tap.test( 'Should throw error when --no-only is passed via cli and there is a .only test', - { todo: expectedExitCodeFailure || expectedStackTraceBug ? 'Fails on these node versions' : false }, + { todo: expectedStackTraceBug ? 'Fails on these node versions' : false }, function (tt) { tt.plan(3); @@ -22,14 +22,14 @@ tap.test( }, function (err, stdout, stderr) { tt.same(stdout.toString('utf8'), ''); tt.match(stripFullStack(stderr.toString('utf8')).join('\n'), /Error: `only` tests are prohibited\n/); - tt.equal(err.code, 1); + tt.equal(err.code, expectedExitCodeOnError); }); } ); tap.test( 'Should throw error when NODE_TAPE_NO_ONLY_TEST is passed via envs and there is an .only test', - { todo: expectedExitCodeFailure || expectedStackTraceBug ? 'Fails on these node versions' : false }, + { todo: expectedStackTraceBug ? 'Fails on these node versions' : false }, function (tt) { tt.plan(3); @@ -39,14 +39,14 @@ tap.test( }, function (err, stdout, stderr) { tt.same(stdout.toString('utf8'), ''); tt.match(stripFullStack(stderr.toString('utf8')).join('\n'), /Error: `only` tests are prohibited\n/); - tt.equal(err.code, 1); + tt.equal(err.code, expectedExitCodeOnError); }); } ); tap.test( 'Should override NODE_TAPE_NO_ONLY_TEST env if --no-only is passed from cli', - { todo: expectedExitCodeFailure || expectedStackTraceBug ? 'Fails on these node versions' : false }, + { todo: expectedStackTraceBug ? 'Fails on these node versions' : false }, function (tt) { tt.plan(3); @@ -56,7 +56,7 @@ tap.test( }, function (err, stdout, stderr) { tt.same(stdout.toString('utf8'), ''); tt.match(stripFullStack(stderr.toString('utf8')).join('\n'), /Error: `only` tests are prohibited\n/); - tt.equal(err.code, 1); + tt.equal(err.code, expectedExitCodeOnError); }); } ); From a576f8d8b24e84324b7ee0498c37759423c7ca87 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 14 Jun 2023 23:21:50 -0700 Subject: [PATCH 02/10] [Dev Deps] pin `jackspeak` since 2.1.2+ depends on npm aliases, which kill the install process in npm < 6 See https://github.com/isaacs/jackspeak/issues/4 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a83d8340..29d4bf10 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "es-value-fixtures": "^1.4.2", "eslint": "=8.8.0", "falafel": "^2.2.5", + "jackspeak": "=2.1.1", "js-yaml": "^3.14.0", "npm-run-posix-or-windows": "^2.0.2", "npmignore": "^0.3.0", From 71231114c92b4a093b468875c4d8f741ab1a49d4 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 21 Sep 2023 14:28:04 -0700 Subject: [PATCH 03/10] [Dev Deps] update `@ljharb/eslint-config`, `array.prototype.flatmap`, `aud` --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 29d4bf10..912a342a 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "through": "~2.3.8" }, "devDependencies": { - "@ljharb/eslint-config": "^21.0.1", - "array.prototype.flatmap": "^1.3.1", - "aud": "^2.0.2", + "@ljharb/eslint-config": "^21.1.0", + "array.prototype.flatmap": "^1.3.2", + "aud": "^2.0.3", "auto-changelog": "^2.4.0", "concat-stream": "^1.6.2", "eclint": "^2.8.1", From feee0949f1f23ef4f13c9847c20284f7864cd67e Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 21 Sep 2023 14:28:53 -0700 Subject: [PATCH 04/10] [Deps] update `minimist`, `resolve`, `string.prototype.trim` --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 912a342a..09822fe9 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "has": "~1.0.3", "inherits": "~2.0.4", "is-regex": "~1.1.4", - "minimist": "~1.2.7", + "minimist": "~1.2.8", "object-inspect": "~1.12.3", - "resolve": "~1.22.1", + "resolve": "~1.22.6", "resumer": "~0.0.0", - "string.prototype.trim": "~1.2.7", + "string.prototype.trim": "~1.2.8", "through": "~2.3.8" }, "devDependencies": { From a8a7d67c9ffe1daa46407f35685a1ef8d53df66a Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 17 Jul 2023 23:59:15 -0700 Subject: [PATCH 05/10] [Deps] switch from `through` and `resumer` to `@ljharb/through` and `@ljharb/resumer` --- index.js | 2 +- lib/default_stream.js | 2 +- lib/results.js | 4 ++-- package.json | 6 +++--- test/objectMode.js | 2 +- test/objectModeWithComment.js | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 1a11e3ca..8ec5be34 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ var defined = require('defined'); var createDefaultStream = require('./lib/default_stream'); var Test = require('./lib/test'); var createResult = require('./lib/results'); -var through = require('through'); +var through = require('@ljharb/through'); var canEmitExit = typeof process !== 'undefined' && process && typeof process.on === 'function' && process.browser !== true; diff --git a/lib/default_stream.js b/lib/default_stream.js index 2b355548..a0d1fa18 100644 --- a/lib/default_stream.js +++ b/lib/default_stream.js @@ -1,6 +1,6 @@ 'use strict'; -var through = require('through'); +var through = require('@ljharb/through'); var fs = require('fs'); module.exports = function () { diff --git a/lib/results.js b/lib/results.js index 23fc0734..a03298bd 100644 --- a/lib/results.js +++ b/lib/results.js @@ -3,8 +3,8 @@ var defined = require('defined'); var EventEmitter = require('events').EventEmitter; var inherits = require('inherits'); -var through = require('through'); -var resumer = require('resumer'); +var through = require('@ljharb/through'); +var resumer = require('@ljharb/resumer'); var inspect = require('object-inspect'); var callBound = require('call-bind/callBound'); var has = require('has'); diff --git a/package.json b/package.json index 09822fe9..2c15b217 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "test": "test" }, "dependencies": { + "@ljharb/resumer": "~0.0.1", + "@ljharb/through": "~2.3.9", "call-bind": "~1.0.2", "deep-equal": "~1.1.1", "defined": "~1.0.1", @@ -24,9 +26,7 @@ "minimist": "~1.2.8", "object-inspect": "~1.12.3", "resolve": "~1.22.6", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.2.8", - "through": "~2.3.8" + "string.prototype.trim": "~1.2.8" }, "devDependencies": { "@ljharb/eslint-config": "^21.1.0", diff --git a/test/objectMode.js b/test/objectMode.js index eb7eddf1..c7ad8221 100644 --- a/test/objectMode.js +++ b/test/objectMode.js @@ -3,7 +3,7 @@ var tap = require('tap'); var tape = require('../'); var forEach = require('for-each'); -var through = require('through'); +var through = require('@ljharb/through'); tap.test('object results', function (assert) { var printer = through({ objectMode: true }); diff --git a/test/objectModeWithComment.js b/test/objectModeWithComment.js index b00f0779..5608f059 100644 --- a/test/objectModeWithComment.js +++ b/test/objectModeWithComment.js @@ -2,7 +2,7 @@ var tap = require('tap'); var tape = require('../'); -var through = require('through'); +var through = require('@ljharb/through'); tap.test('test.comment() in objectMode', function (assert) { var printer = through({ objectMode: true }); From 92aaa5106b65824d82ce72c0d1dfcc6df6cff753 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 7 Sep 2023 22:56:03 -0700 Subject: [PATCH 06/10] =?UTF-8?q?Revert=20"[meta]=20ensure=20`not-in-publi?= =?UTF-8?q?sh`=E2=80=98s=20absence=20does=20not=20fail=20anything"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit fb3a243bfe1baa4c8afb44b8c654cd98500c2e9f. Hopefully this will fix node's CITGM runs on windows. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c15b217..e8d7504e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "version": "auto-changelog && git add CHANGELOG.md", "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"", "prepublishOnly": "safe-publish-latest", - "prepublish": "!(type not-in-publish) || not-in-publish || npm run prepublishOnly", + "prepublish": "not-in-publish || npm run prepublishOnly", "prelint:files": "git ls-files 2>/dev/null | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' || echo '*.md *.js test/*.js'", "eclint": "FILES=\"$(npm run --silent prelint:files)\" eclint check \"${FILES:=package.json}\"", "eclint:windows": "eclint check *.js", From c45db4e0978999cece915e7f1a223aa9eb445ae0 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 21 Sep 2023 13:55:17 -0700 Subject: [PATCH 07/10] [Performance] use inline `typeof` --- lib/test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/test.js b/lib/test.js index 926da137..90bbedf6 100644 --- a/lib/test.js +++ b/lib/test.js @@ -30,12 +30,11 @@ function getTestArgs(name_, opts_, cb_) { for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; - var t = typeof arg; - if (t === 'string') { + if (typeof arg === 'string') { name = arg; - } else if (t === 'object') { + } else if (typeof arg === 'object') { opts = arg || opts; - } else if (t === 'function') { + } else if (typeof arg === 'function') { cb = arg; } } From 3d96d6945ea1cda7780fb3fc6bc04c275ace594a Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 20 Sep 2023 13:47:51 -0700 Subject: [PATCH 08/10] [New] add `t.capture` and `t.captureFn`, modeled after tap --- lib/test.js | 74 ++++++++++++++++++++++++++ package.json | 1 + readme.markdown | 26 ++++++++- test/capture.js | 132 ++++++++++++++++++++++++++++++++++++++++++++++ test/captureFn.js | 75 ++++++++++++++++++++++++++ 5 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 test/capture.js create mode 100644 test/captureFn.js diff --git a/lib/test.js b/lib/test.js index 90bbedf6..6e5f2672 100644 --- a/lib/test.js +++ b/lib/test.js @@ -8,13 +8,18 @@ var EventEmitter = require('events').EventEmitter; var has = require('has'); var isRegExp = require('is-regex'); var trim = require('string.prototype.trim'); +var callBind = require('call-bind'); var callBound = require('call-bind/callBound'); var forEach = require('for-each'); var inspect = require('object-inspect'); +var mockProperty = require('mock-property'); + var isEnumerable = callBound('Object.prototype.propertyIsEnumerable'); var toLowerCase = callBound('String.prototype.toLowerCase'); var $exec = callBound('RegExp.prototype.exec'); var objectToString = callBound('Object.prototype.toString'); +var $push = callBound('Array.prototype.push'); +var $slice = callBound('Array.prototype.slice'); var nextTick = typeof setImmediate !== 'undefined' ? setImmediate @@ -171,6 +176,75 @@ Test.prototype.teardown = function (fn) { } }; +function wrapFunction(original) { + if (typeof original !== 'undefined' && typeof original !== 'function') { + throw new TypeError('`original` must be a function or `undefined`'); + } + + var bound = original && callBind.apply(original); + + var calls = []; + + var wrapObject = { + __proto__: null, + wrapped: function wrapped() { + var args = $slice(arguments); + var completed = false; + try { + var returned = original ? bound(this, arguments) : void undefined; + $push(calls, { args: args, receiver: this, returned: returned }); + completed = true; + return returned; + } finally { + if (!completed) { + $push(calls, { args: args, receiver: this, threw: true }); + } + } + }, + calls: calls, + results: function results() { + try { + return calls; + } finally { + calls = []; + wrapObject.calls = calls; + } + } + }; + return wrapObject; +} + +Test.prototype.capture = function capture(obj, method) { + if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { + throw new TypeError('`obj` must be an object'); + } + if (typeof method !== 'string' && typeof method !== 'symbol') { + throw new TypeError('`method` must be a string or a symbol'); + } + var implementation = arguments.length > 2 ? arguments[2] : void undefined; + if (typeof implementation !== 'undefined' && typeof implementation !== 'function') { + throw new TypeError('`implementation`, if provided, must be a function'); + } + + var wrapper = wrapFunction(implementation); + var restore = mockProperty(obj, method, { value: wrapper.wrapped }); + this.teardown(restore); + + wrapper.results.restore = restore; + + return wrapper.results; +}; + +Test.prototype.captureFn = function captureFn(original) { + if (typeof original !== 'function') { + throw new TypeError('`original` must be a function'); + } + + var wrapObject = wrapFunction(original); + wrapObject.wrapped.calls = wrapObject.calls; + return wrapObject.wrapped; +}; + Test.prototype._end = function (err) { var self = this; if (this._progeny.length) { diff --git a/package.json b/package.json index e8d7504e..777ed6b3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "inherits": "~2.0.4", "is-regex": "~1.1.4", "minimist": "~1.2.8", + "mock-property": "~1.0.0", "object-inspect": "~1.12.3", "resolve": "~1.22.6", "string.prototype.trim": "~1.2.8" diff --git a/readme.markdown b/readme.markdown index 22ad29e8..8f773989 100644 --- a/readme.markdown +++ b/readme.markdown @@ -360,7 +360,8 @@ You may pass the same options that [`test()`](#testname-opts-cb) accepts. ## t.comment(message) -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.) +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.) Multiline output will be split by `\n` characters, and each one printed as a comment. @@ -372,6 +373,29 @@ Assert that `string` matches the RegExp `regexp`. Will fail when the first two a Assert that `string` does not match the RegExp `regexp`. Will fail when the first two arguments are the wrong type. +## t.capture(obj, method, implementation = () => {}) + +Replaces `obj[method]` with the supplied implementation. +`obj` must be a non-primitive, `method` must be a valid property key (string or symbol), and `implementation`, if provided, must be a function. + +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: + - `{ args: [x, y, z], receiver: o, returned: a }` + - `{ args: [x, y, z], receiver: o, threw: true, thrown: e }` + +The replacement will automatically be restored on test teardown. +You can restore it manually, if desired, by calling `.restore()` on the returned results function. + +Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html). + +## t.captureFn(original) + +Wraps the supplied function. +The returned wrapper has a `.calls` property, which is an array that will be populated with call result objects, described under `t.capture()`. + +Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html). + ## 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. diff --git a/test/capture.js b/test/capture.js new file mode 100644 index 00000000..0fee3782 --- /dev/null +++ b/test/capture.js @@ -0,0 +1,132 @@ +'use strict'; + +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var inspect = require('object-inspect'); +var forEach = require('for-each'); +var v = require('es-value-fixtures'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('capture: output', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + var count = 0; + test.createStream().pipe(concat(function (body) { + tt.same(stripFullStack(body.toString('utf8')), [].concat( + 'TAP version 13', + '# argument validation', + v.primitives.map(function (x) { + return 'ok ' + ++count + ' ' + inspect(x) + ' is not an Object'; + }), + v.nonPropertyKeys.map(function (x) { + return 'ok ' + ++count + ' ' + inspect(x) + ' is not a valid property key'; + }), + v.nonFunctions.filter(function (x) { return typeof x !== 'undefined'; }).map(function (x) { + return 'ok ' + ++count + ' ' + inspect(x) + ' is not a function'; + }), + '# captures calls', + 'ok ' + ++count + ' property has expected initial value', + '# capturing', + 'ok ' + ++count + ' throwing implementation throws', + 'ok ' + ++count + ' should be equivalent', + 'ok ' + ++count + ' should be equivalent', + 'ok ' + ++count + ' should be equivalent', + 'ok ' + ++count + ' should be equivalent', + 'ok ' + ++count + ' should be equivalent', + '# post-capturing', + 'ok ' + ++count + ' property is restored', + 'ok ' + ++count + ' added property is removed', + '', + '1..' + count, + '# tests ' + count, + '# pass ' + count, + '', + '# ok', + '' + )); + })); + + test('argument validation', function (t) { + forEach(v.primitives, function (primitive) { + t.throws( + function () { t.capture(primitive, ''); }, + TypeError, + inspect(primitive) + ' is not an Object' + ); + }); + + forEach(v.nonPropertyKeys, function (nonPropertyKey) { + t.throws( + function () { t.capture({}, nonPropertyKey); }, + TypeError, + inspect(nonPropertyKey) + ' is not a valid property key' + ); + }); + + forEach(v.nonFunctions, function (nonFunction) { + if (typeof nonFunction !== 'undefined') { + t.throws( + function () { t.capture({}, '', nonFunction); }, + TypeError, + inspect(nonFunction) + ' is not a function' + ); + } + }); + + t.end(); + }); + + test('captures calls', function (t) { + var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } }; + var o = { foo: sentinel, inspect: function () { return '{ o OBJECT }'; } }; + t.equal(o.foo, sentinel, 'property has expected initial value'); + + t.test('capturing', function (st) { + var results = st.capture(o, 'foo', function () { return sentinel; }); + var results2 = st.capture(o, 'foo2'); + var up = new SyntaxError('foo'); + var resultsThrow = st.capture(o, 'fooThrow', function () { throw up; }); + + o.foo(1, 2, 3); + o.foo(3, 4, 5); + o.foo2.call(sentinel, 1); + st.throws( + function () { o.fooThrow(1, 2, 3); }, + SyntaxError, + 'throwing implementation throws' + ); + + st.deepEqual(results(), [ + { args: [1, 2, 3], receiver: o, returned: sentinel }, + { args: [3, 4, 5], receiver: o, returned: sentinel } + ]); + st.deepEqual(results(), []); + + o.foo(6, 7, 8); + st.deepEqual(results(), [ + { args: [6, 7, 8], receiver: o, returned: sentinel } + ]); + + st.deepEqual(results2(), [ + { args: [1], receiver: sentinel, returned: undefined } + ]); + st.deepEqual(resultsThrow(), [ + { args: [1, 2, 3], receiver: o, threw: true } + ]); + + st.end(); + }); + + t.test('post-capturing', function (st) { + st.equal(o.foo, sentinel, 'property is restored'); + st.notOk('foo2' in o, 'added property is removed'); + + st.end(); + }); + + t.end(); + }); +}); diff --git a/test/captureFn.js b/test/captureFn.js new file mode 100644 index 00000000..45b58a5b --- /dev/null +++ b/test/captureFn.js @@ -0,0 +1,75 @@ +'use strict'; + +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var inspect = require('object-inspect'); +var forEach = require('for-each'); +var v = require('es-value-fixtures'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('captureFn: output', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + var count = 0; + test.createStream().pipe(concat(function (body) { + tt.same(stripFullStack(body.toString('utf8')), [].concat( + 'TAP version 13', + '# argument validation', + v.nonFunctions.map(function (x) { + return 'ok ' + ++count + ' ' + inspect(x) + ' is not a function'; + }), + '# captured fn calls', + 'ok ' + ++count + ' return value is passed through', + 'ok ' + ++count + ' throwing implementation throws', + 'ok ' + ++count + ' should be equivalent', + 'ok ' + ++count + ' should be equivalent', + '', + '1..' + count, + '# tests ' + count, + '# pass ' + count, + '', + '# ok', + '' + )); + })); + + test('argument validation', function (t) { + forEach(v.nonFunctions, function (nonFunction) { + t.throws( + function () { t.captureFn(nonFunction); }, + TypeError, + inspect(nonFunction) + ' is not a function' + ); + }); + + t.end(); + }); + + test('captured fn calls', function (t) { + var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } }; + + var wrappedSentinelThunk = t.captureFn(function () { return sentinel; }); + var up = new SyntaxError('foo'); + var wrappedThrower = t.captureFn(function () { throw up; }); + + t.equal(wrappedSentinelThunk(1, 2), sentinel, 'return value is passed through'); + t.throws( + function () { wrappedThrower.call(sentinel, 1, 2, 3); }, + SyntaxError, + 'throwing implementation throws' + ); + + t.deepEqual(wrappedSentinelThunk.calls, [ + { args: [1, 2], receiver: undefined, returned: sentinel } + ]); + + t.deepEqual(wrappedThrower.calls, [ + { args: [1, 2, 3], receiver: sentinel, threw: true } + ]); + + t.end(); + }); +}); From e60aeca688fe1d3a363f74f31c83d816035aca4c Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 20 Sep 2023 15:40:04 -0700 Subject: [PATCH 09/10] [New] add `t.intercept()` --- lib/test.js | 97 ++++++++++++++ readme.markdown | 17 +++ test/intercept.js | 316 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 430 insertions(+) create mode 100644 test/intercept.js diff --git a/lib/test.js b/lib/test.js index 6e5f2672..00bc8fd4 100644 --- a/lib/test.js +++ b/lib/test.js @@ -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) { diff --git a/readme.markdown b/readme.markdown index 8f773989..e2bf69b9 100644 --- a/readme.markdown +++ b/readme.markdown @@ -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. diff --git a/test/intercept.js b/test/intercept.js new file mode 100644 index 00000000..1d640a9c --- /dev/null +++ b/test/intercept.js @@ -0,0 +1,316 @@ +'use strict'; + +var tape = require('../'); +var tap = require('tap'); +var concat = require('concat-stream'); +var inspect = require('object-inspect'); +var forEach = require('for-each'); +var v = require('es-value-fixtures'); + +var stripFullStack = require('./common').stripFullStack; + +tap.test('intercept: output', function (tt) { + tt.plan(1); + + var test = tape.createHarness(); + var count = 0; + test.createStream().pipe(concat(function (body) { + tt.same(stripFullStack(body.toString('utf8')), [].concat( + 'TAP version 13', + '# argument validation', + v.primitives.map(function (x) { + return 'ok ' + ++count + ' obj: ' + inspect(x) + ' is not an Object'; + }), + v.nonPropertyKeys.map(function (x) { + return 'ok ' + ++count + ' ' + inspect(x) + ' is not a valid property key'; + }), + v.primitives.map(function (x) { + return 'ok ' + ++count + ' desc: ' + inspect(x) + ' is not an Object'; + }), + 'ok ' + ++count + ' configurable false is not allowed', + 'ok ' + ++count + ' mixed data (value) and accessor (get) is not allowed', + 'ok ' + ++count + ' mixed data (writable) and accessor (set) is not allowed', + v.nonBooleans.map(function (x) { + return 'ok ' + ++count + ' ' + inspect(x) + ' is not a Boolean'; + }), + '# intercepts gets/sets', + 'ok ' + ++count + ' property has expected initial value', + '# intercepting', + 'ok ' + ++count + ' sentinel is returned from Get', + 'ok ' + ++count + ' sentinel is returned from Get again', + 'ok ' + ++count + ' sentinel is returned from Get with .call', + 'ok ' + ++count + ' undefined is returned from Get', + 'ok ' + ++count + ' undefined is returned from Get with .call', + 'ok ' + ++count + ' foo2: nonwritable property throws on Set', + 'ok ' + ++count + ' undefined is still returned from Get', + 'ok ' + ++count + ' throwing get implementation throws', + 'ok ' + ++count + ' throwing get implementation throws with .call', + 'ok ' + ++count + ' throwing set implementation throws', + 'ok ' + ++count + ' throwing set implementation throws with .call', + 'ok ' + ++count + ' fooThrowSet: get is undefined', + 'ok ' + ++count + ' getter: sentinel is returned from Get', + 'ok ' + ++count + ' getter: sentinel is returned from Get with .call', + 'ok ' + ++count + ' setter: setted value is returned from Get', + 'ok ' + ++count + ' setter: setted value is returned from Get with .call', + 'ok ' + ++count + ' sloppy: undefined is returned from Get', + 'ok ' + ++count + ' nonwritable data property in sloppy mode does not throw on Set', + 'ok ' + ++count + ' sloppy: undefined is still returned from Get', + 'ok ' + ++count + ' resultsThrowGet: results are correct', + 'ok ' + ++count + ' resultsThrowSet: results are correct', + 'ok ' + ++count + ' foo: results are correct', + 'ok ' + ++count + ' foo2: results are correct', + 'ok ' + ++count + ' sloppy: results are correct', + 'ok ' + ++count + ' getter: results are correct', + 'ok ' + ++count + ' setter: results are correct', + + '# post-intercepting', + 'ok ' + ++count + ' property is restored', + 'ok ' + ++count + ' added foo2 property is removed', + 'ok ' + ++count + ' added fooThrowGet property is removed', + 'ok ' + ++count + ' added fooThrowSet property is removed', + 'ok ' + ++count + ' added slops property is removed', + 'ok ' + ++count + ' added getter property is removed', + 'ok ' + ++count + ' added setter property is removed', + '', + '1..' + count, + '# tests ' + count, + '# pass ' + count, + '', + '# ok', + '' + )); + })); + + test('argument validation', function (t) { + forEach(v.primitives, function (primitive) { + t.throws( + function () { t.intercept(primitive, ''); }, + TypeError, + 'obj: ' + inspect(primitive) + ' is not an Object' + ); + }); + + forEach(v.nonPropertyKeys, function (nonPropertyKey) { + t.throws( + function () { t.intercept({}, nonPropertyKey); }, + TypeError, + inspect(nonPropertyKey) + ' is not a valid property key' + ); + }); + + forEach(v.primitives, function (primitive) { + t.throws( + function () { t.intercept({}, '', primitive); }, + TypeError, + 'desc: ' + inspect(primitive) + ' is not an Object' + ); + }); + t.throws( + function () { t.intercept({}, '', { configurable: false }); }, + TypeError, + 'configurable false is not allowed' + ); + t.throws( + function () { t.intercept({}, '', { value: 1, get: function () {} }); }, + TypeError, + 'mixed data (value) and accessor (get) is not allowed' + ); + t.throws( + function () { t.intercept({}, '', { writable: true, set: function () {} }); }, + TypeError, + 'mixed data (writable) and accessor (set) is not allowed' + ); + + forEach(v.nonBooleans, function (nonBoolean) { + t.throws( + function () { t.intercept({}, '', {}, nonBoolean); }, + TypeError, + inspect(nonBoolean) + ' is not a Boolean' + ); + }); + + t.end(); + }); + + test('intercepts gets/sets', function (t) { + var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } }; + var o = { foo: sentinel, inspect: function () { return '{ o OBJECT }'; } }; + t.equal(o.foo, sentinel, 'property has expected initial value'); + + t.test('intercepting', function (st) { + var up = new SyntaxError('gross'); + + var results = st.intercept(o, 'foo', { value: sentinel, writable: true }); + st.equal(o.foo, sentinel, 'sentinel is returned from Get'); + st.equal(o.foo, sentinel, 'sentinel is returned from Get again'); + st.equal( + Object.getOwnPropertyDescriptor(o, 'foo').get.call(o, 1, 2, 3), + sentinel, + 'sentinel is returned from Get with .call' + ); + o.foo = 42; + results.restore(); + + var results2 = st.intercept(o, 'foo2'); + st.equal(o.foo2, undefined, 'undefined is returned from Get'); + st.equal( + Object.getOwnPropertyDescriptor(o, 'foo2').get.call(o, 4, 5), + undefined, + 'undefined is returned from Get with .call' + ); + st.throws( + function () { o.foo2 = 42; }, + TypeError, + 'foo2: nonwritable property throws on Set' + ); + st.equal(o.foo2, undefined, 'undefined is still returned from Get'); + results2.restore(); + + var resultsThrowGet = st.intercept(o, 'fooThrowGet', { get: function () { throw up; } }); + st.throws( + function () { return o.fooThrowGet; }, + SyntaxError, + 'throwing get implementation throws' + ); + st.throws( + function () { Object.getOwnPropertyDescriptor(o, 'fooThrowGet').get.call(sentinel, 1, 2, 3); }, + SyntaxError, + 'throwing get implementation throws with .call' + ); + + o.fooThrowGet = 42; + + resultsThrowGet.restore(); + + var resultsThrowSet = st.intercept(o, 'fooThrowSet', { set: function () { throw up; } }); + st.throws( + function () { o.fooThrowSet = 42; }, + SyntaxError, + 'throwing set implementation throws' + ); + st.throws( + function () { Object.getOwnPropertyDescriptor(o, 'fooThrowSet').set.call(sentinel, 4, 5, 6); }, + SyntaxError, + 'throwing set implementation throws with .call' + ); + st.equal( + o.fooThrowSet, + undefined, + 'fooThrowSet: get is undefined' + ); + resultsThrowSet.restore(); + + var resultsGetter = st.intercept(o, 'getter', { get: function () { return sentinel; } }); + st.equal(o.getter, sentinel, 'getter: sentinel is returned from Get'); + st.equal(Object.getOwnPropertyDescriptor(o, 'getter').get.call(sentinel, 1, 2, 3), sentinel, 'getter: sentinel is returned from Get with .call'); + resultsGetter.restore(); + + var val; + var resultsSetter = st.intercept(o, 'setter', { + get: function () { return val; }, + set: function (x) { val = x; } + }); + o.setter = sentinel; + st.equal(o.setter, sentinel, 'setter: setted value is returned from Get'); + Object.getOwnPropertyDescriptor(o, 'setter').set.call(sentinel, 1, 2, 3); + st.equal(o.setter, 1, 'setter: setted value is returned from Get with .call'); + resultsSetter.restore(); + + var sloppy = t.intercept(o, 'slops', {}, false); + st.equal(o.slops, undefined, 'sloppy: undefined is returned from Get'); + st.doesNotThrow( + function () { o.slops = 42; }, + 'nonwritable data property in sloppy mode does not throw on Set' + ); + st.equal(o.slops, undefined, 'sloppy: undefined is still returned from Get'); + sloppy.restore(); + + st.deepEqual( + resultsThrowGet(), + [ + { type: 'get', success: false, threw: true, args: [], receiver: o }, + { type: 'get', success: false, threw: true, args: [1, 2, 3], receiver: sentinel }, + { type: 'set', value: 42, success: true, args: [42], receiver: o } + ], + 'resultsThrowGet: results are correct' + ); + + st.deepEqual( + resultsThrowSet(), + [ + { type: 'set', success: false, threw: true, args: [42], receiver: o }, + { type: 'set', success: false, threw: true, args: [4, 5, 6], receiver: sentinel }, + { type: 'get', success: true, value: undefined, args: [], receiver: o } + ], + 'resultsThrowSet: results are correct' + ); + + st.deepEqual( + results(), + [ + { type: 'get', success: true, value: sentinel, args: [], receiver: o }, + { type: 'get', success: true, value: sentinel, args: [], receiver: o }, + { type: 'get', success: true, value: sentinel, args: [1, 2, 3], receiver: o }, + { type: 'set', value: 42, success: true, args: [42], receiver: o } + ], + 'foo: results are correct' + ); + st.deepEqual( + results2(), + [ + { type: 'get', success: true, value: undefined, args: [], receiver: o }, + { type: 'get', success: true, value: undefined, args: [4, 5], receiver: o }, + { type: 'set', success: false, value: undefined, args: [42], receiver: o }, + { type: 'get', success: true, value: undefined, args: [], receiver: o } + ], + 'foo2: results are correct' + ); + + st.deepEqual( + sloppy(), + [ + { type: 'get', success: true, value: undefined, args: [], receiver: o }, + { type: 'set', success: false, value: undefined, args: [42], receiver: o }, + { type: 'get', success: true, value: undefined, args: [], receiver: o } + ], + 'sloppy: results are correct' + ); + + st.deepEqual( + resultsGetter(), + [ + { type: 'get', success: true, value: sentinel, args: [], receiver: o }, + { type: 'get', success: true, value: sentinel, args: [1, 2, 3], receiver: sentinel } + ], + 'getter: results are correct' + ); + + st.deepEqual( + resultsSetter(), + [ + { type: 'set', success: true, value: sentinel, args: [sentinel], receiver: o }, + { type: 'get', success: true, value: sentinel, args: [], receiver: o }, + { type: 'set', success: true, value: 1, args: [1, 2, 3], receiver: sentinel }, + { type: 'get', success: true, value: 1, args: [], receiver: o } + ], + 'setter: results are correct' + ); + + st.end(); + }); + + t.test('post-intercepting', function (st) { + st.equal(o.foo, sentinel, 'property is restored'); + st.notOk('foo2' in o, 'added foo2 property is removed'); + st.notOk('fooThrowGet' in o, 'added fooThrowGet property is removed'); + st.notOk('fooThrowSet' in o, 'added fooThrowSet property is removed'); + st.notOk('slops' in o, 'added slops property is removed'); + st.notOk('getter' in o, 'added getter property is removed'); + st.notOk('setter' in o, 'added setter property is removed'); + + st.end(); + }); + + t.end(); + }); +}); From 9851ca25c5a67aa1ce538724bf219e2977e8be8e Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Thu, 21 Sep 2023 21:45:28 -0700 Subject: [PATCH 10/10] v4.17.0 --- CHANGELOG.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac483eb1..7a9f3219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v5.7.0](https://github.com/ljharb/tape/compare/v5.6.6...v5.7.0) - 2023-09-21 + +### Commits + +- [New] add `t.intercept()` [`5d37060`](https://github.com/ljharb/tape/commit/5d37060b844ea455c80eb305050168a632998603) +- [New] add `t.capture` and `t.captureFn`, modeled after tap [`9e21f7a`](https://github.com/ljharb/tape/commit/9e21f7a3106fcc4d4e7c057633ce4516d53978d2) +- [Refactor] prefer second `.then` arg over `.catch` [`135a952`](https://github.com/ljharb/tape/commit/135a952e55372855b1510a6381e5a5757758b452) +- [Performance] use inline `typeof` [`5ba89c9`](https://github.com/ljharb/tape/commit/5ba89c993ea0a4c9a880d86af5c268deb239aa70) +- [Deps] update `array.prototype.every`, `glob`, `string.prototype.trim` [`4e2db4d`](https://github.com/ljharb/tape/commit/4e2db4d0699be4034a577479b902885fcc0f2a6c) +- [Dev Deps] update `array.prototype.flatmap` [`df46769`](https://github.com/ljharb/tape/commit/df467693328f7771b2db639ff4aaaf2c64ad16d6) +- Revert "[meta] ensure `not-in-publish`‘s absence does not fail anything" [`1b3e0b1`](https://github.com/ljharb/tape/commit/1b3e0b10dc934a529297f11fa6ccd1693e5416b3) + +## [v5.6.6](https://github.com/ljharb/tape/compare/v5.6.5...v5.6.6) - 2023-07-18 + +### Commits + +- [Deps] switch from `through` and `resumer` to `@ljharb/through` and `@ljharb/resumer` [`c99680a`](https://github.com/ljharb/tape/commit/c99680a661867f0db81d830cb4214e526a4cdec4) + +## [v5.6.5](https://github.com/ljharb/tape/compare/v5.6.4...v5.6.5) - 2023-07-12 + +### Commits + +- [Fix] Results: show a skip string on tests, not just on assertions [`9bbbcfe`](https://github.com/ljharb/tape/commit/9bbbcfe6a28a969dcde53850ebb7673837bdfcb7) +- [Deps] update `deep-equal` [`109a791`](https://github.com/ljharb/tape/commit/109a791cc28b931de1545ba7cb8e5599634190d7) + +## [v5.6.4](https://github.com/ljharb/tape/compare/v5.6.3...v5.6.4) - 2023-07-01 + +### Commits + +- [Fix] `throws`: avoid crashing on a nonconfigurable or nonextensible `expected` [`0731b5f`](https://github.com/ljharb/tape/commit/0731b5f64311b168ac941ce3e547bb1a32766783) +- [Tests] simplify tests [`c656ee5`](https://github.com/ljharb/tape/commit/c656ee52626e0e4992e893e41e1ae81ecb5d68a2) +- [Refactor] `Test`: skip binding for a non-function value [`e244e64`](https://github.com/ljharb/tape/commit/e244e64eab7529c4e0d2391b989152b84229939e) +- [Performance] use `call-bind` for autobinding [`70de437`](https://github.com/ljharb/tape/commit/70de43727d191c10d8ba9542bd0cdabaf272360a) +- [actions] update rebase action [`834453c`](https://github.com/ljharb/tape/commit/834453cdd4cb95b71d2180a3b28a1ce8e51357b3) +- [Deps] update `defined`, `minimist`, `object-inspect`, `string.prototype.trim` [`01edce8`](https://github.com/ljharb/tape/commit/01edce8073efe1134c5fff58638b350afb7c6d22) +- [Dev Deps] update `@ljharb/eslint-config`, `array.prototype.flatmap`, `aud` [`1b3ad24`](https://github.com/ljharb/tape/commit/1b3ad2429b553f7e029fe9fd5977b07e76740e42) +- [Dev Deps] update `@ljharb/eslint-config`, `aud` [`a6a5eee`](https://github.com/ljharb/tape/commit/a6a5eee0991a55f001d6468930e20e486a5e308e) +- [Deps] update `deep-equal` [`2043b2e`](https://github.com/ljharb/tape/commit/2043b2e72be80f477293eefb44f226bb06aeafc0) +- [readme] Link to explain what TAP is [`26a75bb`](https://github.com/ljharb/tape/commit/26a75bbb8b1854fa730eaaf1407aea1409640fd5) +- [Deps] update `minimist` [`7e7c3d0`](https://github.com/ljharb/tape/commit/7e7c3d054449ac44f18d90db790705db4bcef37c) +- [readme] improve t.throws description for Function [`c1b619d`](https://github.com/ljharb/tape/commit/c1b619db02f5b51e4c1379debeb0bac90daa3b93) +- [Dev Deps] pin `jackspeak` since 2.1.2+ depends on npm aliases, which kill the install process in npm < 6 [`0e80800`](https://github.com/ljharb/tape/commit/0e80800b4d287e6cae7bd4f46b13fa8ac5aa1197) +- Merge tag 'v4.16.2' [`d5d675d`](https://github.com/ljharb/tape/commit/d5d675dbd841f411960dbe60946744fae4cd2bcb) +- [meta] add missing npmrc config [`15e2175`](https://github.com/ljharb/tape/commit/15e2175b927010657c66f0a30e44f5266147311f) + ## [v5.6.3](https://github.com/ljharb/tape/compare/v5.6.2...v5.6.3) - 2023-01-15 ## [v5.6.2](https://github.com/ljharb/tape/compare/v5.6.1...v5.6.2) - 2023-01-15 @@ -469,7 +514,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Deps] update `resolve` [`b765bba`](https://github.com/ljharb/tape/commit/b765bba1ea56075d5382b203b6902c8fc5f2d5a6) - [Dev Deps] update `eslint` [`949781f`](https://github.com/ljharb/tape/commit/949781faf753d5481085f993210738e7b93b3172) -## [v5.0.0-next.0](https://github.com/ljharb/tape/compare/v4.16.2...v5.0.0-next.0) - 2019-12-20 +## [v5.0.0-next.0](https://github.com/ljharb/tape/compare/v4.17.0...v5.0.0-next.0) - 2019-12-20 ### Commits @@ -482,6 +527,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [meta] change dep semver prefix from ~ to ^ [`c3924d3`](https://github.com/ljharb/tape/commit/c3924d34476247e2ba0d6e0781ca89b7d25f2a2b) - [Breaking] update `deep-equal` to v2 [`898a6e7`](https://github.com/ljharb/tape/commit/898a6e70aadff95f23eb6f7b4e7a1fd24baacc7d) +## [v4.17.0](https://github.com/ljharb/tape/compare/v4.16.2...v4.17.0) - 2023-09-21 + +### Commits + +- [New] add `t.intercept()` [`e60aeca`](https://github.com/ljharb/tape/commit/e60aeca688fe1d3a363f74f31c83d816035aca4c) +- [New] add `t.capture` and `t.captureFn`, modeled after tap [`3d96d69`](https://github.com/ljharb/tape/commit/3d96d6945ea1cda7780fb3fc6bc04c275ace594a) +- [Deps] switch from `through` and `resumer` to `@ljharb/through` and `@ljharb/resumer` [`a8a7d67`](https://github.com/ljharb/tape/commit/a8a7d67c9ffe1daa46407f35685a1ef8d53df66a) +- [Tests] simplify tests [`83bc381`](https://github.com/ljharb/tape/commit/83bc3810a930c60973e60a7a02b677c2fb1f444c) +- [Performance] use inline `typeof` [`c45db4e`](https://github.com/ljharb/tape/commit/c45db4e0978999cece915e7f1a223aa9eb445ae0) +- [Deps] update `minimist`, `resolve`, `string.prototype.trim` [`feee094`](https://github.com/ljharb/tape/commit/feee0949f1f23ef4f13c9847c20284f7864cd67e) +- [Dev Deps] update `@ljharb/eslint-config`, `array.prototype.flatmap`, `aud` [`7123111`](https://github.com/ljharb/tape/commit/71231114c92b4a093b468875c4d8f741ab1a49d4) +- Revert "[meta] ensure `not-in-publish`‘s absence does not fail anything" [`92aaa51`](https://github.com/ljharb/tape/commit/92aaa5106b65824d82ce72c0d1dfcc6df6cff753) +- [Dev Deps] pin `jackspeak` since 2.1.2+ depends on npm aliases, which kill the install process in npm < 6 [`a576f8d`](https://github.com/ljharb/tape/commit/a576f8d8b24e84324b7ee0498c37759423c7ca87) + ## [v4.16.2](https://github.com/ljharb/tape/compare/v4.16.1...v4.16.2) - 2023-01-15 ### Commits diff --git a/package.json b/package.json index 777ed6b3..96ef6435 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tape", - "version": "4.16.2", + "version": "4.17.0", "description": "tap-producing test harness for node and browsers", "main": "index.js", "browser": {