Repository: unexpectedjs/unexpected
Pull request #377 (2017-02-25 01:14:19)

Child expect + exportAssertion
Add the ability to create a "child" (or "subsidiary") expect that can have its own plugins, assertions, and styles installed. Addresses #334

My basic litmus test is whether this works:

```js
const expect = require('unexpected')
    .use(require('unexpected-messy'))
    .use(require('unexpected-http'));

it('should do whatever', function () {
    return expect('http://google.com/', 'to yield HTTP response satisfying', 418);
});
```

... and also if unexpected-messy and unexpected-http are `use`d in the opposite order.

Open issues:

* Consider rename the `child` method to `inherit` as suggested by @sunesimonsen?
* Should `expect.child().use` allow plugins listed in `plugin.dependencies` to be satisfied by plugins installed in the parent expect? My gut says yes, and I've implemented it like that, but maybe we just want to cut support for `plugin.dependencies`, as I don't think it's in use anywhere?

papandreou: Working through the plugins that depend on secondary plugins now. Ran into an annoyance with unexpected-messy, which has a bunch of its output generation in custom styles.

Turns out that because those custom styles are referenced by the `inspect` function of types that are added to the parent (eg. [here](https://github.com/unexpectedjs/unexpected-messy/blob/feature/child/lib/unexpectedMessy.js#L446)), [those common styles along with magicpen-media also have to be installed in the parent expect](https://github.com/unexpectedjs/unexpected-messy/blob/feature/child/lib/unexpectedMessy.js#L135-L145), which defeats the purpose in this particular case.

This might be the case that we need `exportType` to fix :(. Otherwise we can only rely on those custom styles in the assertions.
sunesimonsen: @papandreou my initial gut feeling tells me that we should support `exportAssertion`, `exportType` and maybe `exportStyle`.
sunesimonsen: I'll take a look at this PR when I have some time available.
papandreou: Okay, I now have `exportType` working well enough that unexpected-messy's test suite passes after a [relatively small changeset](https://github.com/unexpectedjs/unexpected-messy/compare/feature/child).
papandreou: What's not covered here (yet):

1) There's still no `childExpect.exportStyle` (would have access to "private" styles only installed in the child, but itself be available on the parent expect as part of the "API" made available by a plugin). Something like magicpen-prism could have a use case for this if we wanted to only export the `code` style and keep the others as implementation details.
2) Magicpen themes only installed in the child expect aren't active when the parent serializes an error message. I haven't thought it through, but I think it would be quite hard to fix that.
sunesimonsen: Thanks for all that hard work, and for documenting the progress. I'll have a look at it in the weekend.
papandreou: Tested and found working with the One.com Webmail server suite. It uses unexpected-{mitm,express,messy}. Turns out the test suite itself was relying on one of the other modules installing unexpected-messy, which made it break after I updated to the feature/child branches :). I updated it to explicitly depend on and install unexpected-messy in the "parent expect", and now it works with no issues.
papandreou: I've run into some trouble with a middle-of-the-rocket assertion in the unexpected-mitm test suite (the recording tests) that I haven't quite figured out yet. Could be that I'm still missing something here. I hope I'll figure it out during the weekend. Until then, don't be in a hurry to review this PR in its current state.
papandreou: Fixed, back to 0 known bugs (apart from the above mentioned limitations).
sunesimonsen: I think it is fair that the magicpen styles it not inherited, if it becomes a problem, we can introduce the concept in magicpen.
sunesimonsen: I had a look through the code - as always we need to make sure that this will not hurt performance too much for the normal case. It is a bit hard to know if that would be the case or not.

You official won the prize for being the master of evil boolean logic :-) Before this goes into master, we should try to simplify some of the boolean logic as it is a bit hard to follow. I know this is not currently a concern, as this is still a POC.

Thanks a lot for creating so many tests, that really increases the confidence in the solution.

Great work!
papandreou: > I think it is fair that the magicpen styles it not inherited, if it becomes a problem, we can introduce the concept in magicpen.

I took a shot at it, and implementing `exportStyle` wasn't hard. Didn't attempt `exportTheme`.
papandreou: > as always we need to make sure that this will not hurt performance too much for the normal case

```
$ for n in `seq 1 7`; do ./node_modules/.bin/chewbacca master -- test/benchmark.spec.js ; done | grep 'working dir is'
working dir is 5.07% slower than master on average
working dir is 3.58% slower than master on average
working dir is 4.25% slower than master on average
working dir is 2.44% slower than master on average
working dir is 3.96% slower than master on average
working dir is 5.52% slower than master on average
working dir is 5.65% slower than master on average
```

It seems to be within the acceptable range, but I'll see if I can improve on it a bit.
papandreou: @alexjeffburke Could you help me out by testing this work with unexpected-mxhr's browser build?
papandreou: What do you know, that actually worked ;)
alexjeffburke: @papandreou I'd be happy to - if not this evening will sit down later in the week :)
alexjeffburke: Hello,

I've just managed to test this stuff using unexpected-mxhr and I'm relatively certain I've just seen two different version of unexpected-messy being loaded completely happily.

Just to recap, what caused the problem we're trying to solve was the following situation:
* unexpected-mxhr has a direct dependency on unexpected-messy
* unexpected-http also directly depends on unexpected-messy
When bundling those units into the browser, both include unexpected-messy within their bundles so thy can be used.

Let me explain the conditions of the test so I can get peer review of the validity of the test:
* unexpected-mxhr was _not_ changed and the bundle regenerated with unexpected-messy version 6.2.1 that included console.log('OLDER') in it's installInto() method
* unexpected-http was updated and its bundle regenerated against unexpected-messy 6.3.0 that included console.log('NEWER') and both had expect.child() changes

Observing the console while running the unexpected-mxhr test file in Safari resulted in 'NEWER' followed by 'OLDER' bring logged in the console, which is looks correct because the test suite does a use() call on -http and then -mxhr.

AJB
alexjeffburke: As an additional note, I think I prefer the child() naming - there's something quite nice in the language about first taking a child() but then exporting from it (has a natural feeling of to the parent for me).
alexjeffburke: ![test output](https://cloud.githubusercontent.com/assets/721814/23824417/95cc6d98-0676-11e7-99b4-9ce13ea9d037.png)

papandreou: @alexjeffburke, thanks. That sounds really promising! Can you verify that both unexpected-mxhr and unexpected-http are able to generate error messages that utilize their respective unexpected-messy instances in that setup?
papandreou: Wrt. `child` vs. `inherit` I think I agree that `child` is a bit better. Do we have any other suggestions? In teepee I've used the term "subsidiary" to mean essentially the same thing -- you use it to create an HTTP client that shares its basic options, certs, agents etc. with the main one, but with eg. additional headers.
alexjeffburke: @papandreou sure, the setup is all intact so will try that when I next sit down with my machine
papandreou: Note to self: Passing `unexpected.assertions` here won't work: https://github.com/unexpectedjs/unexpected/blob/feature/child/lib/createWrappedExpectProto.js#L188

Write a test and fix it :)
papandreou: > Write a test and fix it :)

Done.
papandreou: This is ready for a fair and balanced review now, @gustavnikolaj & @sunesimonsen.
papandreou: @alexjeffburke, did you get a chance to try out the experiment with failing tests?
papandreou: A loosely related anecdote: I just redid the fixpect + unexpected-react experiment on top of the proposed [expect.hook feature](https://github.com/unexpectedjs/unexpected/pull/385) and ran into a case where something like `expect.child` could also lend a hand: https://github.com/papandreou/fixpect/commit/5e003dab1e7855a147f55a8c40fa99c73d86e98e#diff-2bff68b367051713af5955bb827d4fd7R6

To summarize: Fixpect needs to create an expect instance that it can use to render the fixed arguments, relying on `inspect` of all the installed types. The current solution relies on unexpected-react being installed before fixpect, but with the `expect.child` feature it could be immune to that.
sunesimonsen: Let's use `child` I definitely `subsidiary`, most people wont be able to remember how to spell that.
papandreou: > I'm doubting more and more that you are not actually an alien or something kind of cyborg :-)

A cyborg powered by stubbornness and a queue of problems worth solving :)

Thanks for taking the time to review. I'll fix when I'm back in DK!
sunesimonsen: Yes let's get this done, it seems like it is almost ready.
papandreou: > Let's use child I definitely don't like subsidiary, most people wont be able to remember how to spell that.

I agree. Reverted the rename :)
papandreou: > I feel like we should deprecate the dependency check when this has landed. This basically removes the need for it.

I agree. I guess that has to wait for Unexpected 11 even though I'd be extremely surprised if it's being used anywhere.

Patch:
diff --git a/documentation/api/child.md b/documentation/api/child.md
new file mode 100644
index 00000000..ac08366d
--- /dev/null
+++ b/documentation/api/child.md
@@ -0,0 +1,69 @@
+# expect.child()
+
+Create a new `expect` function that maintains a close relationship
+to the instance it is created from. The child instance has access to
+all assertions, types, and styles defined in the parent, even ones
+that are added after `child()` is called.
+
+This is mostly useful when creating plugins that have secondary dependencies
+that can aid in implementing the plugin itself, but which should not
+leak out into the "primary" instance belonging to the user. This can avoid
+a class of dependency hell-like problems -- for instance when a plugin
+depends on another plugin that's also installed directly by the user at
+the top level, and most likely in an incompatible version.
+
+The child instance has the ability to export assertions, types, and styles
+to the parent using the `exportAssertion`, `exportType`, and `exportStyle`
+methods. These methods have the same signature as `addAssertion`, `addType`,
+and `addStyle`, respectively, but affect the parent `expect`:
+
+```js
+var childExpect = expect.child();
+
+// Only available in childExpect:
+childExpect.addAssertion('<string> to begin with foo', function (expect, subject) {
+    expect(subject, 'to begin with', 'foo');
+});
+
+// Available in parentExpect, but has access to "to begin with foo" internally:
+childExpect.exportAssertion('<string> to foobar', function (expect, subject) {
+    expect.errorMode = 'nested';
+    expect(subject, 'to begin with foo').and('to end with', 'bar');
+});
+
+expect('fo0bar', 'to foobar');
+```
+
+```output
+expected 'fo0bar' to foobar
+  expected 'fo0bar' to begin with foo
+
+  fo0bar
+  ^^
+```
+
+Note that in order to reference a type in the signature used with
+`exportAssertion`, the type itself must also be exported to the parent `expect`.
+In other words, this won't work:
+
+```js
+var childExpect = expect.child();
+
+childExpect.addType({
+    name: 'foosomething',
+    identify: function (value) {
+        return /^foo/.test(String(value));
+    }
+});
+
+childExpect.exportAssertion('<foosomething> to end with bar', function (expect, subject) {
+    expect(subject, 'to end with', 'bar');
+});
+```
+
+```output
+Unknown type: foosomething in <foosomething> to end with bar
+```
+
+It can be fixed by exporting the `foosomething` type instead of just adding it
+to the child.
diff --git a/documentation/plugins.md b/documentation/plugins.md
index b038ec29..480c391c 100644
--- a/documentation/plugins.md
+++ b/documentation/plugins.md
@@ -73,30 +73,6 @@ use browserify or a script loader instead of the Common.js `require` in the abov
 Please consult the documentation for each individual plugin.
 
 
-## Caveats with plugins that depend on other plugins
-
-The unexpected-express, unexpected-mitm, and unexpected-http plugins all depend
-on unexpected-messy being available. If you use more than one of these in the same
-test suite, it's important that only one version of unexpected-messy is installed.
-
-All three plugins list `unexpected-messy` under both `peerDependencies` and `dependencies`
-in their package.json. This strategy is carefully thought out to be forward compatible
-with how `peerDependencies` work with npm 3. Unfortunately, users of npm 1 and 2 will
-sometimes be in for a bit of a rough ride.
-
-Unexpected's [use method](/api/use/) will throw an error if you install two different
-versions of unexpected-messy, so there's a stop gap that prevents `expect` from
-ending up in a broken state. Still, recovering from that error condition or an
-`EPEERINVALID` error can be tricky. We recommend trying the following:
-
-1. Upgrade to `npm 3`, then remove the `node_modules` folder and run a fresh `npm install`.
-2. If you're stuck on a previous npm version, you should still try to remove `node_modules`
-   and run a fresh `npm install`.
-3. If that doesn't work, upgrade unexpected and the plugins you're using to the newest
-   versions at once. The newest versions should be using the same version of
-   unexpected-messy, which will resolve the problem in most cases.
-
-
 ## Mixing plugins
 
 All of these plugins should be able coexist in the same Unexpected instance and
diff --git a/lib/Unexpected.js b/lib/Unexpected.js
index a839cf71..1eac0d21 100644
--- a/lib/Unexpected.js
+++ b/lib/Unexpected.js
@@ -82,14 +82,17 @@ function Unexpected(options) {
     // Make bound versions of these two helpers up front to save a bit when creating wrapped expects:
     var that = this;
     this.getType = function (typeName) {
-        return utils.findFirst(that.types, function (type) {
-            return type.name === typeName;
-        });
+        return that.typeByName[typeName] || (that.parent && that.parent.getType(typeName));
     };
     this.findTypeOf = function (obj) {
         return utils.findFirst(that.types || [], function (type) {
-            return type.identify(obj);
-        });
+            return type.identify && type.identify(obj);
+        }) || (that.parent && that.parent.findTypeOf(obj));
+    };
+    this.findTypeOfWithParentType = function (obj, requiredParentType) {
+        return utils.findFirst(that.types || [], function (type) {
+            return type.identify && type.identify(obj) && (!requiredParentType || type.is(requiredParentType));
+        }) || (that.parent && that.parent.findTypeOfWithParentType(obj, requiredParentType));
     };
     this.findCommonType = function (a, b) {
         var aAncestorIndex = {};
@@ -316,10 +319,10 @@ Unexpected.prototype.expandTypeAlternations = function (assertion) {
             } else if (arg.type.is('assertion')) {
                 result.push([
                     { type: arg.type, minimum: 1, maximum: 1 },
-                    { type: that.typeByName.any, minimum: 0, maximum: Infinity }
+                    { type: that.getType('any'), minimum: 0, maximum: Infinity }
                 ]);
                 result.push([
-                    { type: that.typeByName['expect.it'], minimum: 1, maximum: 1 }
+                    { type: that.getType('expect.it'), minimum: 1, maximum: 1 }
                 ]);
                 if (arg.minimum === 0) { // <assertion?>
                     result.push([]);
@@ -354,25 +357,21 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
     var tokens = [];
     var nextIndex = 0;
 
-    function lookupType(typeName) {
-        var result = that.typeByName[typeName];
-        if (!result) {
-            throw new Error('Unknown type: ' + typeName + ' in ' + assertionString);
-        }
-        return result;
-    }
-
-    function parseType(assertionString) {
-        return assertionString.split('|').map(function (type) {
-            var matchNameAndOperator = type.match(/^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i);
+    function parseTypeToken(typeToken) {
+        return typeToken.split('|').map(function (typeDeclaration) {
+            var matchNameAndOperator = typeDeclaration.match(/^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i);
             if (!matchNameAndOperator) {
-                throw new SyntaxError('Cannot parse type declaration:' + type);
+                throw new SyntaxError('Cannot parse type declaration:' + typeDeclaration);
+            }
+            var type = that.getType(matchNameAndOperator[1]);
+            if (!type) {
+                throw new Error('Unknown type: ' + matchNameAndOperator[1] + ' in ' + assertionString);
             }
             var operator = matchNameAndOperator[2];
             return {
                 minimum: !operator || operator === '+' ? 1 : 0,
                 maximum: operator === '*' || operator === '+' ? Infinity : 1,
-                type: lookupType(matchNameAndOperator[1])
+                type: type
             };
         });
     }
@@ -387,7 +386,7 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
             throw new SyntaxError('Cannot parse token at index ' + nextIndex + ' in ' + assertionString);
         }
         if ($1) {
-            tokens.push(parseType($1));
+            tokens.push(parseTypeToken($1));
         } else {
             tokens.push($2.trim());
         }
@@ -397,9 +396,9 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
     var assertion;
     if (tokens.length === 1 && typeof tokens[0] === 'string') {
         assertion = {
-            subject: parseType('any'),
+            subject: parseTypeToken('any'),
             assertion: tokens[0],
-            args: [parseType('any*')]
+            args: [parseTypeToken('any*')]
         };
     } else {
         assertion = {
@@ -447,7 +446,6 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
     })) {
         throw new SyntaxError('<assertion+> and <assertion*> are not allowed: ' + assertionString);
     }
-
     return this.expandTypeAlternations(assertion);
 };
 
@@ -484,14 +482,30 @@ Unexpected.prototype.fail = function (arg) {
                 output.error('Explicit failure');
             }
         };
+        var expect = this.expect;
         Object.keys(arg).forEach(function (key) {
             var value = arg[key];
             if (key === 'diff') {
-                error.createDiff = value;
+                if (typeof value === 'function' && this.parent) {
+                    error.createDiff = function (output, diff, inspect, equal) {
+                        var childOutput = expect.createOutput(output.format);
+                        childOutput.inline = output.inline;
+                        childOutput.output = output.output;
+                        return value(childOutput, function diff(actual, expected) {
+                            return expect.diff(actual, expected, childOutput.clone());
+                        }, function inspect(v, depth) {
+                            return childOutput.clone().appendInspected(v, (depth || defaultDepth) - 1);
+                        }, function (actual, expected) {
+                            return expect.equal(actual, expected);
+                        });
+                    };
+                } else {
+                    error.createDiff = value;
+                }
             } else if (key !== 'message') {
                 error[key] = value;
             }
-        });
+        }, this);
     } else {
         var placeholderArgs;
         if (arguments.length > 0) {
@@ -547,10 +561,14 @@ function calculateAssertionSpecificity(assertion) {
     }));
 }
 
-// expect.addAssertion(pattern, handler)
-// expect.addAssertion([pattern, ...], handler)
-Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
-    if (arguments.length > 2 || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns))) {
+Unexpected.prototype.addAssertion = function (patternOrPatterns, handler, childUnexpected) {
+    var maxArguments;
+    if (typeof childUnexpected === 'object') {
+        maxArguments = 3;
+    } else {
+        maxArguments = 2;
+    }
+    if (arguments.length > maxArguments || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns))) {
         var errorMessage = "Syntax: expect.addAssertion(<string|array[string]>, function (expect, subject, ...) { ... });";
         if ((typeof handler === 'string' || Array.isArray(handler)) && typeof arguments[2] === 'function') {
             errorMessage +=
@@ -595,7 +613,8 @@ Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
                     subject: assertionDeclaration.subject,
                     args: assertionDeclaration.args,
                     testDescriptionString: expandedAssertion.text,
-                    declaration: pattern
+                    declaration: pattern,
+                    unexpected: childUnexpected
                 });
             });
         });
@@ -624,7 +643,7 @@ Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
     return this.expect; // for chaining
 };
 
-Unexpected.prototype.addType = function (type) {
+Unexpected.prototype.addType = function (type, childUnexpected) {
     var that = this;
     var baseType;
     if (typeof type.name !== 'string' || !/^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$/i.test(type.name)) {
@@ -635,14 +654,12 @@ Unexpected.prototype.addType = function (type) {
         throw new Error('Type ' + type.name + ' must specify an identify function or be declared abstract by setting identify to false');
     }
 
-    if (this.getType(type.name)) {
+    if (this.typeByName[type.name]) {
         throw new Error('The type with the name ' + type.name + ' already exists');
     }
 
     if (type.base) {
-        baseType = utils.findFirst(this.types, function (t) {
-            return t.name === type.base;
-        });
+        baseType = this.getType(type.base);
 
         if (!baseType) {
             throw new Error('Unknown base type: ' + type.base);
@@ -657,7 +674,7 @@ Unexpected.prototype.addType = function (type) {
             throw new Error('You need to pass the output to baseType.inspect() as the third parameter');
         }
 
-        return baseType.inspect(value, depth, output.clone(), function (value, depth) {
+        return baseType.inspect(value, depth, output, function (value, depth) {
             return output.clone().appendInspected(value, depth);
         });
     };
@@ -688,10 +705,25 @@ Unexpected.prototype.addType = function (type) {
     extendedType.inspect = function (obj, depth, output, inspect) {
         if (arguments.length < 2 || (!output || !output.isMagicPen)) {
             return 'type: ' + type.name;
+        } else if (childUnexpected) {
+            var childOutput = childUnexpected.createOutput(output.format);
+            return originalInspect.call(this, obj, depth, childOutput, inspect) || childOutput;
         } else {
-            return originalInspect.call(this, obj, depth, output, inspect);
+            return originalInspect.call(this, obj, depth, output, inspect) || output;
         }
     };
+
+    if (childUnexpected) {
+        extendedType.childUnexpected = childUnexpected;
+        var originalDiff = extendedType.diff;
+        extendedType.diff = function (actual, expected, output, inspect, diff, equal) {
+            var childOutput = childUnexpected.createOutput(output.format);
+            // Make sure that already buffered up output is preserved:
+            childOutput.output = output.output;
+            return originalDiff.call(this, actual, expected, childOutput, inspect, diff, equal) || output;
+        };
+    }
+
     if (extendedType.identify === false) {
         this.types.push(extendedType);
     } else {
@@ -769,6 +801,11 @@ Unexpected.prototype.use = function (plugin) {
 
     if (plugin.dependencies) {
         var installedPlugins = this.installedPlugins;
+        var instance = this.parent;
+        while (instance) {
+            Array.prototype.push.apply(installedPlugins, instance.installedPlugins);
+            instance = instance.parent;
+        }
         var unfulfilledDependencies = plugin.dependencies.filter(function (dependency) {
             return !installedPlugins.some(function (plugin) {
                 return getPluginName(plugin) === dependency;
@@ -831,6 +868,7 @@ function installExpectMethods(unexpected, expectFunction) {
     expect.addType = unexpected.addType.bind(unexpected);
     expect.getType = unexpected.getType;
     expect.clone = unexpected.clone.bind(unexpected);
+    expect.child = unexpected.child.bind(unexpected);
     expect.toString = unexpected.toString.bind(unexpected);
     expect.assertions = unexpected.assertions;
     expect.use = expect.installPlugin = unexpected.use.bind(unexpected);
@@ -891,7 +929,12 @@ Unexpected.prototype.throwAssertionNotFoundError = function (subject, testDescri
     }
 
     var assertionsWithScore = [];
-    var assertionStrings = Object.keys(this.assertions);
+    var assertionStrings = [];
+    var instance = this;
+    while (instance) {
+        Array.prototype.push.apply(assertionStrings, Object.keys(instance.assertions));
+        instance = instance.parent;
+    }
 
     function compareAssertions(a, b) {
         var aAssertion = that.lookupAssertionRule(subject, a, args);
@@ -954,7 +997,15 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
     if (typeof testDescriptionString !== 'string') {
         throw new Error('The expect function requires the second parameter to be a string or an expect.it.');
     }
-    var handlers = this.assertions[testDescriptionString];
+    var handlers;
+    var instance = this;
+    while (instance) {
+        var instanceHandlers = instance.assertions[testDescriptionString];
+        if (instanceHandlers) {
+            handlers = handlers ? handlers.concat(instanceHandlers) : instanceHandlers;
+        }
+        instance = instance.parent;
+    }
     if (!handlers) {
         return null;
     }
@@ -1076,6 +1127,9 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) {
                 that.throwAssertionNotFoundError(subject, testDescriptionString, args);
             }
         }
+        if (assertionRule && assertionRule.unexpected && assertionRule.unexpected !== that) {
+            return assertionRule.unexpected.expect.apply(assertionRule.unexpected.expect, [subject, testDescriptionString].concat(args));
+        }
 
         var flags = extend({}, assertionRule.flags);
         var wrappedExpect = function (subject, testDescriptionString) {
@@ -1284,6 +1338,35 @@ Unexpected.prototype.clone = function () {
     return makeExpectFunction(unexpected);
 };
 
+Unexpected.prototype.child = function () {
+    var childUnexpected = new Unexpected({
+        assertions: {},
+        types: [],
+        typeByName: {},
+        output: this.output.clone(),
+        format: this.outputFormat(),
+        installedPlugins: []
+    });
+    var parent = childUnexpected.parent = this;
+    var childExpect = makeExpectFunction(childUnexpected);
+    childExpect.exportAssertion = function (testDescription, handler) {
+        parent.addAssertion(testDescription, handler, childUnexpected);
+        return this;
+    };
+    childExpect.exportType = function (type) {
+        parent.addType(type, childUnexpected);
+        return this;
+    };
+    childExpect.exportStyle = function (name, handler) {
+        parent.addStyle(name, function () { // ...
+            var childOutput = childExpect.createOutput(this.format);
+            this.append(handler.apply(childOutput, arguments) || childOutput);
+        });
+        return this;
+    };
+    return childExpect;
+};
+
 Unexpected.prototype.outputFormat = function (format) {
     if (typeof format === 'undefined') {
         return this._outputFormat;
diff --git a/lib/createWrappedExpectProto.js b/lib/createWrappedExpectProto.js
index 219b9cdb..72b88276 100644
--- a/lib/createWrappedExpectProto.js
+++ b/lib/createWrappedExpectProto.js
@@ -10,17 +10,29 @@ function isAssertionArg(arg) {
     return arg.type.is('assertion');
 }
 
-function findSuffixAssertions(assertionString, assertions) {
+function lookupAssertionsInParentChain(assertionString, unexpected) {
+    var assertions = [];
+    for (var instance = unexpected ; instance ; instance = instance.parent) {
+        if (instance.assertions[assertionString]) {
+            Array.prototype.push.apply(assertions, instance.assertions[assertionString]);
+        }
+    }
+    return assertions;
+}
+
+function findSuffixAssertions(assertionString, unexpected) {
     if (typeof assertionString !== 'string') {
         return null;
     }
-    if (assertions[assertionString]) {
-        return assertions[assertionString];
+    var straightforwardAssertions = lookupAssertionsInParentChain(assertionString, unexpected);
+    if (straightforwardAssertions.length > 0) {
+        return straightforwardAssertions;
     }
     var tokens = assertionString.split(' ');
     for (var n = tokens.length - 1 ; n > 0 ; n -= 1) {
-        var suffixAssertions = assertions[tokens.slice(n).join(' ')];
-        if (findSuffixAssertions(tokens.slice(0, n).join(' '), assertions) && suffixAssertions) {
+        var suffix = tokens.slice(n).join(' ');
+        var suffixAssertions = lookupAssertionsInParentChain(suffix, unexpected);
+        if (findSuffixAssertions(tokens.slice(0, n).join(' '), unexpected) && suffixAssertions.length > 0) {
             return suffixAssertions;
         }
     }
@@ -36,6 +48,7 @@ module.exports = function createWrappedExpectProto(unexpected) {
         inspect: unexpected.inspect,
         createOutput: unexpected.createOutput.bind(unexpected),
         findTypeOf: unexpected.findTypeOf.bind(unexpected),
+        findTypeOfWithParentType: unexpected.findTypeOfWithParentType.bind(unexpected),
         findCommonType: unexpected.findCommonType.bind(unexpected),
         it: unexpected.it.bind(unexpected),
         diff: unexpected.diff,
@@ -135,7 +148,7 @@ module.exports = function createWrappedExpectProto(unexpected) {
                         }
                         if (nextArgumentType.is('string')) {
                             output.error(rest[0]);
-                        } else {
+                        } else if (rest.length > 0) {
                             output.appendInspected(rest[0]);
                         }
                         if (rest.length > 1) {
@@ -167,19 +180,12 @@ module.exports = function createWrappedExpectProto(unexpected) {
             }
         },
         _getSubjectType: function () {
-            var subject = this.subject;
-            var assertionSubjectType = this.assertionRule.subject.type;
-            return utils.findFirst(unexpected.types, function (type) {
-                return type.identify && type.is(assertionSubjectType) && type.identify(subject);
-            });
+            return this.findTypeOfWithParentType(this.subject, this.assertionRule.subject.type);
         },
         _getArgTypes: function (index) {
             var lastIndex = this.assertionRule.args.length - 1;
             return this.args.map(function (arg, index) {
-                var assertionArgType = this.assertionRule.args[Math.min(index, lastIndex)].type;
-                return utils.findFirst(unexpected.types, function (type) {
-                    return type.identify && type.is(assertionArgType) && type.identify(arg);
-                });
+                return this.findTypeOfWithParentType(arg, this.assertionRule.args[Math.min(index, lastIndex)].type);
             }, this);
         },
         _getAssertionIndices: function () {
@@ -191,7 +197,7 @@ module.exports = function createWrappedExpectProto(unexpected) {
                 OUTER: while (true) {
                     if (currentAssertionRule.args.length > 1 && isAssertionArg(currentAssertionRule.args[currentAssertionRule.args.length - 2])) {
                         assertionIndices.push(offset + currentAssertionRule.args.length - 2);
-                        var suffixAssertions = findSuffixAssertions(args[offset + currentAssertionRule.args.length - 2], unexpected.assertions);
+                        var suffixAssertions = findSuffixAssertions(args[offset + currentAssertionRule.args.length - 2], unexpected);
                         if (suffixAssertions) {
                             for (var i = 0 ; i < suffixAssertions.length ; i += 1) {
                                 if (suffixAssertions[i].args.some(isAssertionArg)) {
@@ -215,14 +221,14 @@ module.exports = function createWrappedExpectProto(unexpected) {
         Object.defineProperty(wrappedExpectProto, 'subjectType', {
             enumerable: true,
             get: function () {
-                return this._getSubjectType();
+                return this.assertionRule && this._getSubjectType();
             }
         });
 
         Object.defineProperty(wrappedExpectProto, 'argTypes', {
             enumerable: true,
             get: function () {
-                return this._getArgTypes();
+                return this.assertionRule && this._getArgTypes();
             }
         });
     }
diff --git a/lib/utils.js b/lib/utils.js
index 975809d5..f926ba69 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -44,10 +44,9 @@ var utils = module.exports = {
         return target;
     },
 
-    findFirst: function (arr, predicate, thisObj) {
-        var scope = thisObj || null;
+    findFirst: function (arr, predicate) {
         for (var i = 0 ; i < arr.length ; i += 1) {
-            if (predicate.call(scope, arr[i], i, arr)) {
+            if (predicate(arr[i])) {
                 return arr[i];
             }
         }
diff --git a/package.json b/package.json
index af6b903c..12e076c5 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
     "diff": "1.1.0",
     "greedy-interval-packer": "1.2.0",
     "leven": "2.0.0",
-    "magicpen": "5.10.0",
+    "magicpen": "5.12.0",
     "unexpected-bluebird": "2.9.34-longstack2"
   },
   "devDependencies": {


Test patch:
diff --git a/test/api/child.spec.js b/test/api/child.spec.js
new file mode 100644
index 00000000..183481f1
--- /dev/null
+++ b/test/api/child.spec.js
@@ -0,0 +1,276 @@
+/*global expect*/
+describe('#child', function () {
+    var parentExpect;
+    var childExpect;
+    beforeEach(function () {
+        parentExpect = expect.clone();
+        childExpect = parentExpect.child();
+    });
+
+    it('should not leak a "private" assertion into the parent', function () {
+        childExpect.addAssertion('<string> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 'foo');
+        });
+        expect(function () {
+            parentExpect('foo', 'to foo');
+        }, 'to throw', /^Unknown assertion 'to foo'/);
+    });
+
+    it('should not leak a "private" style into the parent', function () {
+        childExpect.addStyle('foo', function () {});
+        expect(parentExpect.createOutput().foo, 'to be undefined');
+    });
+
+    it('should not leak a "private" type into the parent', function () {
+        childExpect.addType({
+            name: 'abc',
+            identify: function (obj) {
+                return obj === 'abc';
+            }
+        });
+        expect(parentExpect.findTypeOf('abc'), 'to satisfy', { name: 'string' });
+    });
+
+    it('should have access to assertions defined in the parent after the child was created', function () {
+        parentExpect.addAssertion('<string> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 'foo');
+        });
+        childExpect('foo', 'to foo');
+    });
+
+    it('should prefer an assertion defined in the child, even if it was added before an identically named one in the parent', function () {
+        childExpect.addAssertion('<string> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 'foo');
+        });
+        parentExpect.addAssertion('<string> to foo', function (expect, subject) {
+            expect.fail('Wrong one!');
+        });
+        childExpect('foo', 'to foo');
+    });
+
+    it('should have access to identically named assertions with different type signatures in child and parent', function () {
+        childExpect.addAssertion('<string> to foo', function (expect, subject) {
+            expect.errorMode = 'nested';
+            expect(subject, 'to equal', 'foo');
+            expect(subject.length, 'to foo');
+        });
+        parentExpect.addAssertion('<number> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 3);
+        });
+        childExpect('foo', 'to foo');
+    });
+
+    it('should have access to styles defined in the parent after the child was created', function () {
+        parentExpect.addStyle('yadda', function () {
+            this.text('yaddafoo');
+        });
+        expect(childExpect.createOutput('text').yadda().toString(), 'to equal', 'yaddafoo');
+    });
+
+    it('should prefer a style defined in the child, even if it was added before an identically named one in the parent', function () {
+        childExpect.addStyle('yadda', function () {
+            this.text('yaddagood');
+        });
+        parentExpect.addStyle('yadda', function () {
+            this.text('yaddabad');
+        });
+        expect(childExpect.createOutput('text').yadda().toString(), 'to equal', 'yaddagood');
+    });
+
+    it('should have access to types defined in the parent after the child was created', function () {
+        parentExpect.addType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            }
+        });
+        childExpect.addAssertion('<yadda> to foo', function (expect, subject) {
+            expect(subject, 'to contain', 'foo');
+        });
+        childExpect('yaddafoo', 'to foo');
+    });
+
+    it('should have access to types defined in the parent after the child was created, also in the wrapped expect', function () {
+        parentExpect.addType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            }
+        });
+        childExpect.addAssertion('<yadda> to bar', function (expect, subject) {
+            expect(subject, 'to contain', 'bar');
+        });
+        childExpect.addAssertion('<yadda> to foobar', function (expect, subject) {
+            expect(subject, 'to bar');
+            expect(subject, 'to contain', 'foo');
+        });
+        childExpect('yaddafoobar', 'to foobar');
+    });
+
+    it('should prefer a style defined in the child, even if it was added before an identically named one in the parent', function () {
+        childExpect.addStyle('yadda', function () {
+            this.text('yaddagood');
+        });
+        parentExpect.addStyle('yadda', function () {
+            this.text('yaddabad');
+        });
+        expect(childExpect.createOutput('text').yadda().toString(), 'to equal', 'yaddagood');
+    });
+
+    it('should allow installing an identically named plugin', function () {
+        parentExpect.use({
+            name: 'foo',
+            version: '1.2.3',
+            installInto: function () {}
+        });
+        childExpect.use({
+            name: 'foo',
+            version: '4.5.6',
+            installInto: function () {}
+        });
+    });
+
+    it('should allow a plugin dependency to be satisfied by a plugin installed in the parent', function () {
+        parentExpect.use({
+            name: 'foo',
+            installInto: function () {}
+        });
+        childExpect.use({
+            name: 'bar',
+            dependencies: ['foo'],
+            installInto: function () {}
+        });
+    });
+
+    describe('with identically named types added to the parent and the child', function () {
+        beforeEach(function () {
+            parentExpect.addType({
+                name: 'foo',
+                identify: function (obj) {
+                    return /^foo/.test(obj);
+                },
+                parent: true // marker so the type can be detected in the tests
+            });
+            childExpect.addType({
+                name: 'foo',
+                identify: function (obj) {
+                    return obj === 'foo';
+                },
+                child: true // marker so the type can be detected in the tests
+            });
+        });
+
+        describe('#findTypeOf', function () {
+            describe('in the child', function () {
+                it('should find the type defined in the child', function () {
+                    expect(childExpect.findTypeOf('foo'), 'to satisfy', { child: true });
+                });
+
+                it('should find the type defined in the parent when the object is not identified by the type added to the child', function () {
+                    expect(childExpect.findTypeOf('foobar'), 'to satisfy', { parent: true });
+                });
+            });
+
+            describe('in the parent', function () {
+                it('should find the type defined in the parent', function () {
+                    expect(parentExpect.findTypeOf('foo'), 'to satisfy', { parent: true });
+                });
+            });
+        });
+
+        describe('#getType', function () {
+            describe('in the child', function () {
+                it('should find the type defined in the child', function () {
+                    expect(childExpect.getType('foo'), 'to satisfy', { child: true });
+                });
+            });
+
+            describe('in the parent', function () {
+                it('should find the type defined in the parent', function () {
+                    expect(parentExpect.getType('foo'), 'to satisfy', { parent: true });
+                });
+            });
+        });
+    });
+
+    describe('with identically named styles added to the parent, then the child', function () {
+        beforeEach(function () {
+            parentExpect.addStyle('foo', function () {
+                this.text('parentfoo');
+            });
+            childExpect.addStyle('foo', function () {
+                this.text('childfoo');
+            });
+        });
+
+        it('should use the style defined in the parent when an output is created by the parent', function () {
+            expect(parentExpect.createOutput('text').foo().toString(), 'to equal', 'parentfoo');
+        });
+
+        it('should find the type defined in the parent when parentExpect.findTypeOf is used', function () {
+            expect(childExpect.createOutput('text').foo().toString(), 'to equal', 'childfoo');
+        });
+    });
+
+    describe('with identically named styles added to the child, then the parent', function () {
+        beforeEach(function () {
+            childExpect.addStyle('foo', function () {
+                this.text('childfoo');
+            });
+            parentExpect.addStyle('foo', function () {
+                this.text('parentfoo');
+            });
+        });
+
+        it('should use the style defined in the parent when an output is created by the parent', function () {
+            expect(parentExpect.createOutput('text').foo().toString(), 'to equal', 'parentfoo');
+        });
+
+        it('should find the type defined in the parent when parentExpect.findTypeOf is used', function () {
+            expect(childExpect.createOutput('text').foo().toString(), 'to equal', 'childfoo');
+        });
+    });
+
+    it('should allow adding an assertion referencing the assertion and any types to the child expect', function () {
+        childExpect.addAssertion('<any> foo <assertion>', function (expect, subject) {
+            return expect.shift(String(subject) + 'foo');
+        });
+        expect(childExpect.assertions.foo, 'to satisfy', {
+            1: {
+                subject: { type: { name: 'any' } },
+                args: [ { type: { name: 'assertion' } }, { type: { name: 'any' } } ]
+            }
+        });
+        childExpect('abc', 'foo', 'to equal', 'abcfoo');
+    });
+
+    it('#inspect should be able to delegate to another private type', function () {
+        childExpect.addType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            },
+            inspect: function (value, depth, output) {
+                output.fancyQuotes(value);
+            }
+        });
+        childExpect.addType({
+            name: 'yaddayadda',
+            base: 'yadda',
+            identify: function (obj) {
+                return /^yaddayadda/.test(obj);
+            },
+            inspect: function (value, depth, output, inspect) {
+                this.baseType.inspect(value, depth, output);
+                output.fancyQuotes(value);
+            }
+        });
+
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+
+        expect(childExpect.createOutput('text').appendInspected('yaddayaddafoo').toString(), 'to equal', '>>yaddayaddafoo<<>>yaddayaddafoo<<');
+    });
+});
+
diff --git a/test/api/exportAssertion.spec.js b/test/api/exportAssertion.spec.js
new file mode 100644
index 00000000..7253fcbf
--- /dev/null
+++ b/test/api/exportAssertion.spec.js
@@ -0,0 +1,237 @@
+/*global expect*/
+describe('exportAssertion', function () {
+    var parentExpect;
+    var childExpect;
+    beforeEach(function () {
+        parentExpect = expect.clone();
+        childExpect = parentExpect.child();
+    });
+
+    it('is chainable', function () {
+        childExpect.exportAssertion('foo', function () {})
+            .exportAssertion('bar', function () {});
+
+        expect(parentExpect.assertions, 'to have keys',
+               'foo',
+               'bar');
+    });
+
+    it('makes the assertion available to the parent expect', function () {
+        childExpect.exportAssertion('<string> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 'foo');
+        });
+
+        parentExpect('foo', 'to foo');
+    });
+
+    it('does not make the assertion available to a parent parent expect', function () {
+        childExpect.child().exportAssertion('<string> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 'foo');
+        });
+        expect(function () {
+            parentExpect('foo', 'to foo');
+        }, 'to throw', /^Unknown assertion 'to foo'/);
+    });
+
+    it('binds the assertion to the child expect so custom types are available', function () {
+        childExpect.addType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            },
+            inspect: function (value, depth, output, inspect) {
+                output.text('>>').text(value).text('<<');
+            }
+        });
+        childExpect.addAssertion('<yadda> to foo', function (expect, subject) {
+            expect(subject, 'to contain', 'foo');
+        });
+        childExpect.exportAssertion('<string> to be silly', function (expect, subject) {
+            expect(subject, 'to foo');
+        });
+        parentExpect('yaddafoo', 'to be silly');
+
+        expect(function () {
+            parentExpect('yaddafo', 'to be silly');
+        }, 'to throw',
+            'expected >>yaddafo<< to be silly\n' +
+            '\n' +
+            'yaddafo\n' +
+            '     ^^'
+        );
+    });
+
+    it('picks up type definitions from the parent expect when setting expect.subjectType and expect.argTypes', function () {
+        childExpect.exportAssertion('<any> to foo <any>', function (expect, subject, value) {
+            expect(expect.subjectType.name, 'to equal', 'number');
+            expect(expect.argTypes, 'to satisfy', [ { name: 'string' } ]);
+        });
+
+        parentExpect(123, 'to foo', 'bar');
+    });
+
+    it('should make custom styles available to the output generation code called by expect.fail', function () {
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+        childExpect.exportAssertion('<any> to foo', function (expect, subject) {
+            if (subject !== 'foo') {
+                expect.fail({
+                    diff: function (output, diff, inspect, equal) {
+                        return output
+                            .text('got ')
+                            .fancyQuotes(subject)
+                            .text(' but expected ')
+                            .fancyQuotes('foo')
+                            .text(' and once again ')
+                            .append(inspect(subject))
+                            .text(' the difference being')
+                            .nl()
+                            .append(diff(subject, 'foo'))
+                            .nl()
+                            .text('and they really are different, see: ' + equal(subject, 'foo'));
+                    }
+                });
+            }
+        });
+
+        expect(function () {
+            parentExpect('bar', 'to foo');
+        }, 'to error',
+            "expected 'bar' to foo\n" +
+            "\n" +
+            "got >>bar<< but expected >>foo<< and once again 'bar' the difference being\n" +
+            "-bar\n" +
+            "+foo\n" +
+            "and they really are different, see: false"
+        );
+    });
+
+    it('should make custom styles available when a parent expect appends an error associated with a child expect', function () {
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+        childExpect.exportAssertion('<any> to foo', function (expect, subject) {
+            if (subject !== 'foo') {
+                expect.fail({
+                    diff: function (output, diff, inspect, equal) {
+                        return output.text('got ').fancyQuotes(subject).text(' but expected ').fancyQuotes('foo');
+                    }
+                });
+            }
+        });
+
+        expect(function () {
+            expect(function () {
+                parentExpect('bar', 'to foo');
+            }, 'to error',
+                "expected 'barf' to foo\n" +
+                "\n" +
+                "got >>bar<< but expected >>foo<<"
+            );
+        }, 'to error',
+            "expected function () { parentExpect('bar', 'to foo'); }\n" +
+            "to error 'expected \\'barf\\' to foo\\n\\ngot >>bar<< but expected >>foo<<'\n" +
+            "  expected\n" +
+            "  UnexpectedError(\n" +
+            "    expected 'bar' to foo\n" +
+            "\n" +
+            "    got >>bar<< but expected >>foo<<\n" +
+            "  )\n" +
+            "  to have message 'expected \\'barf\\' to foo\\n\\ngot >>bar<< but expected >>foo<<'\n" +
+            "    expected 'expected \\'bar\\' to foo\\n\\ngot >>bar<< but expected >>foo<<'\n" +
+            "    to equal 'expected \\'barf\\' to foo\\n\\ngot >>bar<< but expected >>foo<<'\n" +
+            "\n" +
+            "    -expected 'bar' to foo\n" +
+            "    +expected 'barf' to foo\n" +
+            "     \n" +
+            "     got >>bar<< but expected >>foo<<"
+        );
+    });
+
+    describe('when shifting from an exported middle-of-the-rocket assertion in an inherited expect to an assertion defined in the parent expect', function () {
+        beforeEach(function () {
+            childExpect.exportAssertion('<string> when prepended with foo <assertion?>', function (expect, subject) {
+                return expect.shift('foo' + subject);
+            });
+
+            parentExpect.addAssertion('<string> to foo', function (expect, subject) {
+                expect(subject, 'to equal', 'foo');
+            });
+        });
+
+        it('should succeed', function () {
+            parentExpect('', 'when prepended with foo', 'to foo');
+        });
+
+        it('should fail with a diff', function () {
+            expect(function () {
+                parentExpect('bar', 'when prepended with foo', 'to foo');
+            }, 'to throw',
+                "expected 'bar' when prepended with foo to foo\n" +
+                "\n" +
+                "-foobar\n" +
+                "+foo"
+            );
+        });
+    });
+
+    describe('when shifting from an exported middle-of-the-rocket assertion in an inherited expect to an assertion exported from another inherit', function () {
+        beforeEach(function () {
+            childExpect.exportAssertion('<string> when prepended with foo <assertion?>', function (expect, subject) {
+                return expect.shift('foo' + subject);
+            });
+
+            parentExpect.child().exportAssertion('<string> to foo', function (expect, subject) {
+                expect(subject, 'to equal', 'foo');
+            });
+        });
+
+        it('should succeed', function () {
+            parentExpect('', 'when prepended with foo', 'to foo');
+        });
+
+        it('should fail with a diff', function () {
+            expect(function () {
+                parentExpect('bar', 'when prepended with foo', 'to foo');
+            }, 'to throw',
+                "expected 'bar' when prepended with foo to foo\n" +
+                "\n" +
+                "-foobar\n" +
+                "+foo"
+            );
+        });
+    });
+
+
+    describe('when delegating to an exported assertion from the parent', function () {
+        beforeEach(function () {
+            childExpect.exportAssertion('<string> when prepended with foo <assertion?>', function (expect, subject) {
+                return expect.shift('foo' + subject);
+            });
+
+            childExpect.exportAssertion('<string> when trimmed <assertion?>', function (expect, subject) {
+                return expect.shift(subject.trim());
+            });
+
+            parentExpect.child().exportAssertion('<string> to foo', function (expect, subject) {
+                expect(subject, 'to equal', 'foo');
+            });
+        });
+
+        it('should succeed', function () {
+            parentExpect('', 'when prepended with foo when trimmed', 'to foo');
+        });
+
+        it('should fail with a diff', function () {
+            expect(function () {
+                parentExpect('bar', 'when prepended with foo when prepended with foo', 'to foo');
+            }, 'to throw',
+                "expected 'bar' when prepended with foo when prepended with foo to foo\n" +
+                "\n" +
+                "-foofoobar\n" +
+                "+foo"
+            );
+        });
+    });
+});
diff --git a/test/api/exportStyle.spec.js b/test/api/exportStyle.spec.js
new file mode 100644
index 00000000..6b25aa27
--- /dev/null
+++ b/test/api/exportStyle.spec.js
@@ -0,0 +1,53 @@
+/*global expect*/
+describe('styleType', function () {
+    var parentExpect;
+    var childExpect;
+    beforeEach(function () {
+        parentExpect = expect.clone();
+        childExpect = parentExpect.child();
+    });
+
+    it('is chainable', function () {
+        childExpect
+            .exportStyle('firstStyle', function () {})
+            .exportStyle('secondStyle', function () {});
+
+        expect(parentExpect.output.firstStyle, 'to be a function');
+        expect(parentExpect.output.secondStyle, 'to be a function');
+    });
+
+    it('makes the style available to the parent expect', function () {
+        childExpect.exportStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+
+        expect(
+            parentExpect.createOutput().fancyQuotes('yadda').toString(),
+            'to equal',
+            '>>yadda<<'
+        );
+    });
+
+    it('does not make the style available to a parent parent expect', function () {
+        childExpect.child().exportStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+        expect(parentExpect.createOutput(), 'to satisfy', { fancyQuotes: undefined });
+    });
+
+    it('binds the style handler to a child expect output so custom types are available inside the exported style', function () {
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+
+        childExpect.exportStyle('emphasize', function (text) {
+            this.fancyQuotes(text);
+        });
+
+        expect(
+            parentExpect.createOutput().emphasize('yadda').toString(),
+            'to equal',
+            '>>yadda<<'
+        );
+    });
+});
diff --git a/test/api/exportType.spec.js b/test/api/exportType.spec.js
new file mode 100644
index 00000000..6a03de35
--- /dev/null
+++ b/test/api/exportType.spec.js
@@ -0,0 +1,119 @@
+/*global expect*/
+describe('exportType', function () {
+    var parentExpect;
+    var childExpect;
+    beforeEach(function () {
+        parentExpect = expect.clone();
+        childExpect = parentExpect.child();
+    });
+
+    it('is chainable', function () {
+        childExpect.exportType({name: 'abc', identify: false})
+            .exportType({name: 'def', identify: false});
+
+        expect(parentExpect.getType('abc'), 'to satisfy', {name: 'abc'});
+        expect(parentExpect.getType('def'), 'to satisfy', {name: 'def'});
+    });
+
+    it('makes the type available to the parent expect', function () {
+        childExpect.exportType({
+            name: 'fooString',
+            identify: function (obj) {
+                return typeof obj === 'string' && /^foo/.test(obj);
+            }
+        });
+
+        parentExpect.addAssertion('<fooString> to foo', function (expect, subject) {
+            expect(subject, 'to equal', 'foo');
+        });
+    });
+
+    it('does not make the type available to a parent parent expect', function () {
+        childExpect.child().exportType({
+            name: 'abc',
+            identify: function (obj) {
+                return obj === 'abc';
+            }
+        });
+        expect(parentExpect.findTypeOf('abc'), 'to satisfy', { name: 'string' });
+    });
+
+    it('binds the type to the child expect so custom types are available to the inspect function', function () {
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+
+        childExpect.exportType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            },
+            inspect: function (value, depth, output, inspect) {
+                return output.fancyQuotes(value);
+            }
+        });
+        expect(parentExpect.createOutput('text').appendInspected('yaddablah').toString(), 'to equal', '>>yaddablah<<');
+    });
+
+    it('binds the type to the child expect so custom types are available to the diff function', function () {
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+
+        childExpect.exportType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            },
+            inspect: function (value, depth, output, inspect) {
+                return output.fancyQuotes(value);
+            },
+            diff: function (actual, expected, output) {
+                output.text('got ').fancyQuotes(actual).text(' but expected ').fancyQuotes(expected);
+            }
+        });
+        expect(
+            parentExpect.getType('yadda').diff('yaddafoo', 'yaddabar', parentExpect.createOutput('text').text('hey, ')).toString(),
+            'to equal',
+            'hey, got >>yaddafoo<< but expected >>yaddabar<<'
+        );
+    });
+
+    it('should correctly append output generated by a inspect function that does not return the output', function () {
+        childExpect.addStyle('fancyQuotes', function (text) {
+            this.text('>>').text(text).text('<<');
+        });
+
+        childExpect.exportType({
+            name: 'yadda',
+            identify: function (obj) {
+                return /^yadda/.test(obj);
+            },
+            inspect: function (value, depth, output, inspect) {
+                output.fancyQuotes(value);
+                if (value.length > 5) {
+                    output.append(value.substr(0, value.length - 1));
+                    output.appendInspected(value.substr(0, value.length - 1));
+                }
+            }
+        });
+        expect(
+            parentExpect.createOutput().appendInspected('yaddafoo').toString(),
+            'to equal',
+            '>>yaddafoo<<yaddafo>>yaddafo<<yaddaf>>yaddaf<<yadda>>yadda<<'
+        );
+    });
+
+    // Weird regression test, used throw TypeError: Cannot read property 'subject' of undefined
+    it('should not break when inspecting the exported type in a diff', function () {
+        childExpect.exportType({
+            name: 'abc',
+            identify: function (obj) {
+                return obj === 'abc';
+            }
+        });
+        expect(function () {
+            expect(parentExpect.findTypeOf('abc'), 'to satisfy', { name: 'string' });
+        }, 'to throw', /^expected/);
+    });
+});
diff --git a/test/api/shift.spec.js b/test/api/shift.spec.js
index 03e9bb87..34dd81c3 100644
--- a/test/api/shift.spec.js
+++ b/test/api/shift.spec.js
@@ -140,4 +140,66 @@ describe('expect.shift', function () {
             );
         });
     });
+
+    describe('in legacy mode where the assertion index is passed as the second parameter', function () {
+        it('should get the assertion string from that index', function () {
+            var clonedExpect = expect.clone().addAssertion('<string> when prepended with <string> <assertion>', function (expect, subject, value) {
+                return expect.shift(value + subject, 1);
+            });
+
+            expect(function () {
+                clonedExpect('bar', 'when prepended with', 'foo', 'to equal', 'foobarquux');
+            }, 'to throw',
+                "expected 'bar' when prepended with 'foo' to equal 'foobarquux'\n" +
+                "\n" +
+                "-foobar\n" +
+                "+foobarquux"
+            );
+        });
+
+        it('should render the correct error message when there is several non-string parameters following the assertion index', function () {
+            var clonedExpect = expect.clone().addAssertion('<string> when prepended with foo <string> <number+>', function (expect, subject) {
+                expect.shift('foo' + subject, 0);
+                expect('abc', 'to equal', 'def');
+            });
+
+            expect(function () {
+                clonedExpect('bar', 'when prepended with foo', 'to equal', 123, 456);
+            }, 'to throw',
+                "expected 'bar' when prepended with foo to equal 123, 456"
+            );
+        });
+
+        it('should render the correct error message when the assertion being shifted to is not a string', function () {
+            var clonedExpect = expect.clone().addAssertion('<string> when prepended with foo <number+>', function (expect, subject) {
+                expect.shift('foo' + subject, 0);
+                expect('abc', 'to equal', 'def');
+            });
+
+            expect(function () {
+                clonedExpect('bar', 'when prepended with foo', 123, 456);
+            }, 'to throw',
+                "expected 'bar' when prepended with foo 123 456\n" +
+                "\n" +
+                "-abc\n" +
+                "+def"
+            );
+        });
+
+        it('should render the correct error message when there are no parameters following the assertion index', function () {
+            var clonedExpect = expect.clone().addAssertion('<string> when prepended with foo', function (expect, subject) {
+                expect.shift('foo' + subject, 1);
+                expect('abc', 'to equal', 'def');
+            });
+
+            expect(function () {
+                clonedExpect('bar', 'when prepended with foo');
+            }, 'to throw',
+                "expected 'bar' when prepended with foo\n" +
+                "\n" +
+                "-abc\n" +
+                "+def"
+            );
+        });
+    });
 });