Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ Both methods are called with a single `event` argument, it will have the followi

- `event.originalMessage` - The actual value the user provided for optional `message` parameter. This will be `undefined` if the user did not provide a value, even if the underlying assertion provides a default message.

- `event.defaultMessage` - If you use objects instead of strings to specify patterns (see below), the `defaultMessage` metadata will be copied directly on the event object.

- `event.matcherSpec` - This contains the complete parsed matcher spec as supplied, as well as any additional metadata you may have supplied (see patterns section below for details on how to supply additional metadata).

- `event.args` - An array of the actual arguments passed to the assertion.

- `event.assertionThrew` - Whether or not the underlying assertion threw an error. This will always be `true` in an `onError` callback, and always `false` in an `onSuccess` callback.
Expand Down Expand Up @@ -118,7 +122,7 @@ TBD

| type | default value |
|:--------------------|:--------------------|
| `Array` of `string` | objects shown below |
| `Array` of `string` or `objects`| objects shown below |

```javascript
[
Expand All @@ -134,11 +138,23 @@ TBD
'assert.notDeepStrictEqual(actual, expected, [message])'
]
```

Target patterns for power assert feature instrumentation.

Pattern detection is done by [call-signature](https://github.com/jamestalmage/call-signature). Any arguments enclosed in bracket (for example, `[message]`) means optional parameters. Without bracket means mandatory parameters.

Instead of a string, you may alternatively specify an object with a `pattern` property, and any other arbitrary data.
Currently only `defaultMessage` is formally recommended, but you can attach any data here and it will be passed to the `onSuccess` and `onError` handlers.

```javascript
[
{
pattern: 'assert.fail([message])',
defaultMessage:'assert.fail() was called!!'
},
...
]
```

#### options.wrapOnlyPatterns

| type | default value |
Expand All @@ -147,6 +163,7 @@ Pattern detection is done by [call-signature](https://github.com/jamestalmage/ca

Methods matching these patterns will not be instrumented by the code transform, but they will be wrapped at runtime and trigger events in the `onSuccess` and `onError` callbacks. Note that "wrap only" events will never have a `powerAssertContext` property.

Similar to the `options.patterns`, you may supply objects with a `pattern` member, and the additional metadata will be passed to the assertion listeners.

### var options = empowerCore.defaultOptions();

Expand Down
12 changes: 3 additions & 9 deletions lib/decorate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ var map = require('core-js/library/fn/array/map');
var slice = Array.prototype.slice;

function decorate (callSpec, decorator) {
var func = callSpec.func;
var thisObj = callSpec.thisObj;
var numArgsToCapture = callSpec.numArgsToCapture;
var enhanced = callSpec.enhanced;

return function decoratedAssert () {
var context, message, hasMessage = false, args = slice.apply(arguments);
Expand All @@ -19,12 +16,9 @@ function decorate (callSpec, decorator) {
}

var invocation = {
thisObj: thisObj,
func: func,
values: args,
message: message,
hasMessage: hasMessage,
enhanced: enhanced
hasMessage: hasMessage
};

if (some(args, isCaptured)) {
Expand All @@ -45,9 +39,9 @@ function decorate (callSpec, decorator) {
return arg.powerAssertContext.value;
});

return decorator.concreteAssert(invocation, context);
return decorator.concreteAssert(callSpec, invocation, context);
} else {
return decorator.concreteAssert(invocation);
return decorator.concreteAssert(callSpec, invocation);
}
};
}
Expand Down
52 changes: 37 additions & 15 deletions lib/decorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,37 @@ var filter = require('core-js/library/fn/array/filter');
var map = require('core-js/library/fn/array/map');
var signature = require('call-signature');
var decorate = require('./decorate');
var keys = require('core-js/library/fn/object/keys');


function Decorator (receiver, config) {
this.receiver = receiver;
this.config = config;
this.onError = config.onError;
this.onSuccess = config.onSuccess;
this.signatures = map(config.patterns, signature.parse);
this.wrapOnlySignatures = map(config.wrapOnlyPatterns, signature.parse);
this.signatures = map(config.patterns, parse);
this.wrapOnlySignatures = map(config.wrapOnlyPatterns, parse);
}

Decorator.prototype.enhancement = function () {
var that = this;
var container = this.container();
var wrappedMethods = [];

function attach(matcher, enhanced) {
function attach(matcherSpec, enhanced) {
var matcher = matcherSpec.parsed;
var methodName = detectMethodName(matcher.callee);
if (typeof that.receiver[methodName] !== 'function' || wrappedMethods.indexOf(methodName) !== -1) {
return;
}
var callSpec = {
thisObj: that.receiver,
func: that.receiver[methodName],
numArgsToCapture: numberOfArgumentsToCapture(matcher),
numArgsToCapture: numberOfArgumentsToCapture(matcherSpec),
matcherSpec: matcherSpec,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've attached it here.

enhanced: enhanced
};
container[methodName] = decorate(callSpec, that);
container[methodName] = callSpec.enhancedFunc = decorate(callSpec, that);
wrappedMethods.push(methodName);
}

Expand Down Expand Up @@ -61,27 +64,33 @@ Decorator.prototype.container = function () {
thisObj: null,
func: this.receiver,
numArgsToCapture: numberOfArgumentsToCapture(candidates[0]),
matcherSpec: candidates[0],
enhanced: enhanced
};
basement = decorate(callSpec, this);
basement = callSpec.enhancedFunc = decorate(callSpec, this);
}
}
return basement;
};

Decorator.prototype.concreteAssert = function (invocation, context) {
var func = invocation.func;
var thisObj = invocation.thisObj;
Decorator.prototype.concreteAssert = function (callSpec, invocation, context) {
var func = callSpec.func;
var thisObj = callSpec.thisObj;
var enhanced = callSpec.enhanced;
var args = invocation.values;
var message = invocation.message;
var enhanced = invocation.enhanced;
var matcherSpec = callSpec.matcherSpec;

if (context && typeof this.config.modifyMessageBeforeAssert === 'function') {
message = this.config.modifyMessageBeforeAssert({originalMessage: message, powerAssertContext: context});
}
args = args.concat(message);

var data = {
assertionFunction: callSpec.enhancedFunc,
originalMessage: message,
defaultMessage: matcherSpec.defaultMessage,
matcherSpec: matcherSpec,
enhanced: enhanced,
args: args
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this is the event object, and it doesn't have it.

};
Expand All @@ -103,7 +112,8 @@ Decorator.prototype.concreteAssert = function (invocation, context) {
return this.onSuccess(data);
};

function numberOfArgumentsToCapture (matcher) {
function numberOfArgumentsToCapture (matcherSpec) {
var matcher = matcherSpec.parsed;
var len = matcher.args.length;
var lastArg;
if (0 < len) {
Expand All @@ -124,13 +134,25 @@ function detectMethodName (callee) {
}


function functionCall (matcher) {
return matcher.callee.type === 'Identifier';
function functionCall (matcherSpec) {
return matcherSpec.parsed.callee.type === 'Identifier';
}


function methodCall (matcher) {
return matcher.callee.type === 'MemberExpression';
function methodCall (matcherSpec) {
return matcherSpec.parsed.callee.type === 'MemberExpression';
}

function parse(matcherSpec) {
if (typeof matcherSpec === 'string') {
matcherSpec = {pattern: matcherSpec};
}
var ret = {};
keys(matcherSpec).forEach(function (key) {
ret[key] = matcherSpec[key];
});
ret.parsed = signature.parse(matcherSpec.pattern);
return ret;
}


Expand Down
63 changes: 62 additions & 1 deletion test/empower_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,58 @@ suite('onSuccess can throw', function () {
});
});

suite('metadata for enhanced methods', function () {
var assert = empower(
{
fail: function (message) {
baseAssert.ok(false, message);
},
pass: function (message) {
// noop
}
},
{
patterns: [
{
pattern: 'assert.fail([message])',
defaultMessage: 'User! You have failed this assertion!'
},
'assert.pass([message])'
],
onError: function (event) {
baseAssert.equal(event.assertionThrew, true);
return event;
},
onSuccess: function (event) {
baseAssert.equal(event.assertionThrew, false);
return event;
}
}
);

test('instrumented', function () {
var event = eval(weave("assert.fail('doh!');"));
baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!');
baseAssert.strictEqual(event.enhanced, true);
baseAssert.strictEqual(event.assertionFunction, assert.fail);
baseAssert.deepEqual(event.matcherSpec, {
pattern: 'assert.fail([message])',
defaultMessage: 'User! You have failed this assertion!',
parsed: {
args: [{name: 'message', optional: true}],
callee: {object: 'assert', member: 'fail', type: 'MemberExpression'}
}
});
});

test('non-instrumented', function () {
var event = assert.fail('doh!');
baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!');
baseAssert.strictEqual(event.enhanced, true);
baseAssert.strictEqual(event.assertionFunction, assert.fail);
});
});

suite('wrapOnlyPatterns', function () {
var assert = empower(
{
Expand All @@ -529,7 +581,10 @@ suite('wrapOnlyPatterns', function () {
},
{
wrapOnlyPatterns: [
'assert.fail([message])',
{
pattern: 'assert.fail([message])',
defaultMessage: 'User! You have failed this assertion!'
},
'assert.pass([message])'
],
onError: function (event) {
Expand All @@ -549,14 +604,17 @@ suite('wrapOnlyPatterns', function () {
baseAssert.strictEqual(event.enhanced, false);
baseAssert.equal(event.originalMessage, 'woot!');
baseAssert.deepEqual(event.args, ['woot!']);
baseAssert.strictEqual(event.assertionFunction, assert.pass);
});

test('instrumented code: error', function () {
var event = eval(weave('assert.fail("Oh no!")'));
baseAssert.equal(event.assertionThrew, true);
baseAssert.strictEqual(event.enhanced, false);
baseAssert.equal(event.originalMessage, 'Oh no!');
baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!');
baseAssert.deepEqual(event.args, ['Oh no!']);
baseAssert.strictEqual(event.assertionFunction, assert.fail);
});

test('non-instrumented code: success', function () {
Expand All @@ -565,14 +623,17 @@ suite('wrapOnlyPatterns', function () {
baseAssert.strictEqual(event.enhanced, false);
baseAssert.equal(event.originalMessage, 'woot!');
baseAssert.deepEqual(event.args, ['woot!']);
baseAssert.strictEqual(event.assertionFunction, assert.pass);
});

test('non-instrumented code: error', function () {
var event = assert.fail('Oh no!');
baseAssert.equal(event.assertionThrew, true);
baseAssert.strictEqual(event.enhanced, false);
baseAssert.equal(event.originalMessage, 'Oh no!');
baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!');
baseAssert.deepEqual(event.args, ['Oh no!']);
baseAssert.strictEqual(event.assertionFunction, assert.fail);
});
});

Expand Down