Repository: unexpectedjs/unexpected
Pull request #565 (2019-01-05 12:58:40)

Merge the Unexpected instance with the expect function, and make wrapped/nested expects have the parent as the prototype


Patch:
diff --git a/lib/Context.js b/lib/Context.js
new file mode 100644
index 00000000..5726afe3
--- /dev/null
+++ b/lib/Context.js
@@ -0,0 +1,14 @@
+class Context {
+  constructor(expect) {
+    this.expect = expect;
+    this.level = 0;
+  }
+
+  child() {
+    const child = Object.create(this);
+    child.level++;
+    return child;
+  }
+}
+
+module.exports = Context;
diff --git a/lib/Unexpected.js b/lib/createTopLevelExpect.js
similarity index 67%
rename from lib/Unexpected.js
rename to lib/createTopLevelExpect.js
index f11a9149..8d3d74a4 100644
--- a/lib/Unexpected.js
+++ b/lib/createTopLevelExpect.js
@@ -10,26 +10,17 @@ const oathbreaker = require('./oathbreaker');
 const UnexpectedError = require('./UnexpectedError');
 const notifyPendingPromise = require('./notifyPendingPromise');
 const defaultDepth = require('./defaultDepth');
-const createWrappedExpectProto = require('./createWrappedExpectProto');
 const AssertionString = require('./AssertionString');
+const Context = require('./Context');
 const throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError');
+const ensureValidUseOfParenthesesOrBrackets = require('./ensureValidUseOfParenthesesOrBrackets');
+const expandAssertion = require('./expandAssertion');
 const nodeJsCustomInspect = require('./nodeJsCustomInspect');
 
 function isAssertionArg({ type }) {
   return type.is('assertion');
 }
 
-function Context(unexpected) {
-  this.expect = unexpected;
-  this.level = 0;
-}
-
-Context.prototype.child = function() {
-  const child = Object.create(this);
-  child.level++;
-  return child;
-};
-
 const anyType = {
   _unexpectedType: true,
   name: 'any',
@@ -80,58 +71,6 @@ if (nodeJsCustomInspect !== 'inspect') {
   };
 }
 
-function Unexpected(options = {}) {
-  this.assertions = options.assertions || {};
-  this.typeByName = options.typeByName || { any: anyType };
-  this.types = options.types || [anyType];
-  if (options.output) {
-    this.output = options.output;
-  } else {
-    this.output = magicpen();
-    this.output.inline = false;
-    this.output.diff = false;
-  }
-  this._outputFormat = options.format || magicpen.defaultFormat;
-  this.installedPlugins = options.installedPlugins || [];
-
-  // Make bound versions of these two helpers up front to save a bit when creating wrapped expects:
-  const that = this;
-  this.getType = typeName =>
-    that.typeByName[typeName] || (that.parent && that.parent.getType(typeName));
-  this.findTypeOf = obj =>
-    utils.findFirst(
-      that.types || [],
-      type => type.identify && type.identify(obj)
-    ) ||
-    (that.parent && that.parent.findTypeOf(obj));
-  this.findTypeOfWithParentType = (obj, requiredParentType) =>
-    utils.findFirst(
-      that.types || [],
-      type =>
-        type.identify &&
-        type.identify(obj) &&
-        (!requiredParentType || type.is(requiredParentType))
-    ) ||
-    (that.parent &&
-      that.parent.findTypeOfWithParentType(obj, requiredParentType));
-  this.findCommonType = function(a, b) {
-    const aAncestorIndex = {};
-    let current = this.findTypeOf(a);
-    while (current) {
-      aAncestorIndex[current.name] = current;
-      current = current.baseType;
-    }
-    current = this.findTypeOf(b);
-    while (current) {
-      if (aAncestorIndex[current.name]) {
-        return current;
-      }
-      current = current.baseType;
-    }
-  };
-  this._wrappedExpectProto = createWrappedExpectProto(this);
-}
-
 const OR = {};
 function getOrGroups(expectations) {
   const orGroups = [[]];
@@ -232,14 +171,14 @@ function writeGroupEvaluationsToOutput(output, groupEvaluations) {
   });
 }
 
-function createExpectIt(unexpected, expectations) {
+function createExpectIt(expect, expectations) {
   const orGroups = getOrGroups(expectations);
 
   function expectIt(subject, context) {
     context =
       context && typeof context === 'object' && context instanceof Context
         ? context
-        : new Context(unexpected);
+        : new Context(expect);
 
     if (
       orGroups.length === 1 &&
@@ -254,7 +193,7 @@ function createExpectIt(unexpected, expectations) {
     const groupEvaluations = [];
     const promises = [];
     orGroups.forEach(orGroup => {
-      const evaluations = evaluateGroup(unexpected, context, subject, orGroup);
+      const evaluations = evaluateGroup(expect, context, subject, orGroup);
       evaluations.forEach(({ promise }) => {
         promises.push(promise);
       });
@@ -279,7 +218,7 @@ function createExpectIt(unexpected, expectations) {
             groupEvaluation.every(({ promise }) => promise.isFulfilled())
           )
         ) {
-          unexpected.fail(output => {
+          expect.fail(output => {
             writeGroupEvaluationsToOutput(output, groupEvaluations);
           });
         }
@@ -292,24 +231,31 @@ function createExpectIt(unexpected, expectations) {
   expectIt.and = function(...args) {
     const copiedExpectations = expectations.slice();
     copiedExpectations.push(args);
-    return createExpectIt(unexpected, copiedExpectations);
+    return createExpectIt(expect, copiedExpectations);
   };
   expectIt.or = function(...args) {
     const copiedExpectations = expectations.slice();
     copiedExpectations.push(OR, args);
-    return createExpectIt(unexpected, copiedExpectations);
+    return createExpectIt(expect, copiedExpectations);
   };
   return expectIt;
 }
 
-Unexpected.prototype.it = function(...args) {
-  // ...
-  return createExpectIt(this, [args]);
+const expectPrototype = {
+  promise: makePromise,
+  notifyPendingPromise,
+  errorMode: 'default'
 };
+utils.setPrototypeOfOrExtend(expectPrototype, Function.prototype);
 
-Unexpected.prototype.equal = function(actual, expected, depth, seen) {
-  const that = this;
+expectPrototype.it = function(...args) {
+  if (this.flags && typeof args[0] === 'string') {
+    args[0] = utils.forwardFlags(args[0], this.flags);
+  }
+  return createExpectIt(this._topLevelExpect, [args]);
+};
 
+expectPrototype.equal = function(actual, expected, depth, seen) {
   depth = typeof depth === 'number' ? depth : 100;
   if (depth <= 0) {
     // detect recursive loops in the structure
@@ -321,15 +267,14 @@ Unexpected.prototype.equal = function(actual, expected, depth, seen) {
   }
 
   return this.findCommonType(actual, expected).equal(actual, expected, (a, b) =>
-    that.equal(a, b, depth - 1, seen)
+    this.equal(a, b, depth - 1, seen)
   );
 };
 
-Unexpected.prototype.inspect = function(obj, depth, outputOrFormat) {
+expectPrototype.inspect = function(obj, depth, outputOrFormat) {
   let seen = [];
-  const that = this;
-  function printOutput(obj, currentDepth, output) {
-    const objType = that.findTypeOf(obj);
+  let printOutput = (obj, currentDepth, output) => {
+    const objType = this.findTypeOf(obj);
     if (currentDepth <= 0 && objType.is('object') && !objType.is('expect.it')) {
       return output.text('...');
     }
@@ -349,7 +294,7 @@ Unexpected.prototype.inspect = function(obj, depth, outputOrFormat) {
       seen.pop();
       return output;
     });
-  }
+  };
 
   let output =
     typeof outputOrFormat === 'string'
@@ -366,12 +311,11 @@ Unexpected.prototype.inspect = function(obj, depth, outputOrFormat) {
 };
 
 if (nodeJsCustomInspect !== 'inspect') {
-  Unexpected.prototype[nodeJsCustomInspect] = Unexpected.prototype.inspect;
+  expectPrototype[nodeJsCustomInspect] = expectPrototype.inspect;
 }
 
-Unexpected.prototype.expandTypeAlternations = function(assertion) {
-  const that = this;
-  function createPermutations(args, i) {
+expectPrototype.expandTypeAlternations = function(assertion) {
+  let createPermutations = (args, i) => {
     if (i === args.length) {
       return [];
     }
@@ -386,10 +330,10 @@ Unexpected.prototype.expandTypeAlternations = function(assertion) {
       } else if (arg.type.is('assertion')) {
         result.push([
           { type: arg.type, minimum: 1, maximum: 1 },
-          { type: that.getType('any'), minimum: 0, maximum: Infinity }
+          { type: this.getType('any'), minimum: 0, maximum: Infinity }
         ]);
         result.push([
-          { type: that.getType('expect.it'), minimum: 1, maximum: 1 }
+          { type: this.getType('expect.it'), minimum: 1, maximum: 1 }
         ]);
         if (arg.minimum === 0) {
           // <assertion?>
@@ -400,7 +344,7 @@ Unexpected.prototype.expandTypeAlternations = function(assertion) {
       }
     });
     return result;
-  }
+  };
   const result = [];
   assertion.subject.forEach(subjectRequirement => {
     if (assertion.args.length) {
@@ -424,12 +368,11 @@ Unexpected.prototype.expandTypeAlternations = function(assertion) {
   return result;
 };
 
-Unexpected.prototype.parseAssertion = function(assertionString) {
-  const that = this;
+expectPrototype.parseAssertion = function(assertionString) {
   const tokens = [];
   let nextIndex = 0;
 
-  function parseTypeToken(typeToken) {
+  let parseTypeToken = typeToken => {
     return typeToken.split('|').map(typeDeclaration => {
       const matchNameAndOperator = typeDeclaration.match(
         /^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i
@@ -439,7 +382,7 @@ Unexpected.prototype.parseAssertion = function(assertionString) {
           `Cannot parse type declaration:${typeDeclaration}`
         );
       }
-      const type = that.getType(matchNameAndOperator[1]);
+      const type = this.getType(matchNameAndOperator[1]);
       if (!type) {
         throw new Error(
           `Unknown type: ${matchNameAndOperator[1]} in ${assertionString}`
@@ -452,7 +395,7 @@ Unexpected.prototype.parseAssertion = function(assertionString) {
         type
       };
     });
-  }
+  };
 
   function hasVarargs(types) {
     return types.some(({ minimum, maximum }) => minimum !== 1 || maximum !== 1);
@@ -549,7 +492,7 @@ Unexpected.prototype.parseAssertion = function(assertionString) {
 
 const placeholderSplitRegexp = /(\{(?:\d+)\})/g;
 const placeholderRegexp = /\{(\d+)\}/;
-Unexpected.prototype.fail = function(arg) {
+expectPrototype._fail = function(arg) {
   if (arg instanceof UnexpectedError) {
     arg._hasSerializedErrorMessage = false;
     throw arg;
@@ -559,7 +502,7 @@ Unexpected.prototype.fail = function(arg) {
     throw arg;
   }
 
-  const error = new UnexpectedError(this.expect);
+  const error = new UnexpectedError(this);
   if (typeof arg === 'function') {
     error.errorMode = 'bubble';
     error.output = arg;
@@ -580,26 +523,24 @@ Unexpected.prototype.fail = function(arg) {
         output.error('Explicit failure');
       }
     };
-    const expect = this.expect;
     Object.keys(arg).forEach(function(key) {
       const value = arg[key];
       if (key === 'diff') {
         if (typeof value === 'function' && this.parent) {
           error.createDiff = (output, diff, inspect, equal) => {
-            const childOutput = expect.createOutput(output.format);
+            const childOutput = this.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());
+              (actual, expected) => {
+                return this.diff(actual, expected, childOutput.clone());
               },
-              function inspect(v, depth) {
-                return childOutput
+              (v, depth) =>
+                childOutput
                   .clone()
-                  .appendInspected(v, (depth || defaultDepth) - 1);
-              },
-              (actual, expected) => expect.equal(actual, expected)
+                  .appendInspected(v, (depth || defaultDepth) - 1),
+              (actual, expected) => this.equal(actual, expected)
             );
           };
         } else {
@@ -664,10 +605,10 @@ function calculateAssertionSpecificity({ subject, args }) {
   );
 }
 
-Unexpected.prototype.addAssertion = function(
+expectPrototype.addAssertion = function(
   patternOrPatterns,
   handler,
-  childUnexpected
+  childExpect
 ) {
   if (this._frozen) {
     throw new Error(
@@ -675,7 +616,7 @@ Unexpected.prototype.addAssertion = function(
     );
   }
   let maxArguments;
-  if (typeof childUnexpected === 'object') {
+  if (typeof childExpect === 'function') {
     maxArguments = 3;
   } else {
     maxArguments = 2;
@@ -714,14 +655,13 @@ Unexpected.prototype.addAssertion = function(
     }
   });
 
-  const that = this;
   const assertions = this.assertions;
 
   const defaultValueByFlag = {};
   const assertionHandlers = [];
   let maxNumberOfArgs = 0;
   patterns.forEach(pattern => {
-    const assertionDeclarations = that.parseAssertion(pattern);
+    const assertionDeclarations = this.parseAssertion(pattern);
     assertionDeclarations.forEach(({ assertion, args, subject }) => {
       ensureValidUseOfParenthesesOrBrackets(assertion);
       const expandedAssertions = expandAssertion(assertion);
@@ -739,13 +679,13 @@ Unexpected.prototype.addAssertion = function(
         );
         assertionHandlers.push({
           handler,
-          alternations: alternations,
-          flags: flags,
-          subject: subject,
-          args: args,
+          alternations,
+          flags,
+          subject,
+          args,
           testDescriptionString: text,
           declaration: pattern,
-          unexpected: childUnexpected
+          expect: childExpect
         });
       });
     });
@@ -782,16 +722,16 @@ Unexpected.prototype.addAssertion = function(
     }
   });
 
-  return this.expect; // for chaining
+  return this; // for chaining
 };
 
-Unexpected.prototype.addType = function(type, childUnexpected) {
+expectPrototype.addType = function(type, childExpect) {
   if (this._frozen) {
     throw new Error(
       'Cannot add a type to a frozen instance, please run .clone() first'
     );
   }
-  const that = this;
+
   let baseType;
   if (
     typeof type.name !== 'string' ||
@@ -852,14 +792,14 @@ Unexpected.prototype.addType = function(type, childUnexpected) {
       actual,
       expected,
       output.clone(),
-      (actual, expected) => that.diff(actual, expected, output.clone()),
+      (actual, expected) => this.diff(actual, expected, output.clone()),
       (value, depth) => output.clone().appendInspected(value, depth),
-      that.equal.bind(that)
+      this.equal.bind(this)
     );
   };
 
   extendedBaseType.equal = (actual, expected) =>
-    baseType.equal(actual, expected, that.equal.bind(that));
+    baseType.equal(actual, expected, this.equal.bind(this));
 
   const extendedType = extend({}, baseType, type, {
     baseType: extendedBaseType
@@ -876,8 +816,8 @@ Unexpected.prototype.addType = function(type, childUnexpected) {
   extendedType.inspect = function(obj, depth, output, inspect) {
     if (arguments.length < 2 || (!output || !output.isMagicPen)) {
       return `type: ${type.name}`;
-    } else if (childUnexpected) {
-      const childOutput = childUnexpected.createOutput(output.format);
+    } else if (childExpect) {
+      const childOutput = childExpect.createOutput(output.format);
       return (
         originalInspect.call(this, obj, depth, childOutput, inspect) ||
         childOutput
@@ -887,8 +827,8 @@ Unexpected.prototype.addType = function(type, childUnexpected) {
     }
   };
 
-  if (childUnexpected) {
-    extendedType.childUnexpected = childUnexpected;
+  if (childExpect) {
+    extendedType.childExpect = childExpect;
     const originalDiff = extendedType.diff;
     extendedType.diff = function(
       actual,
@@ -898,7 +838,7 @@ Unexpected.prototype.addType = function(type, childUnexpected) {
       diff,
       equal
     ) {
-      const childOutput = childUnexpected.createOutput(output.format);
+      const childOutput = childExpect.createOutput(output.format);
       // Make sure that already buffered up output is preserved:
       childOutput.output = output.output;
       return (
@@ -925,27 +865,73 @@ Unexpected.prototype.addType = function(type, childUnexpected) {
   extendedType.typeEqualityCache = {};
   this.typeByName[extendedType.name] = extendedType;
 
-  return this.expect;
+  return this;
+};
+
+expectPrototype.getType = function(typeName) {
+  return (
+    this.typeByName[typeName] || (this.parent && this.parent.getType(typeName))
+  );
 };
 
-Unexpected.prototype.addStyle = function(...args) {
+expectPrototype.findTypeOf = function(obj) {
+  return (
+    utils.findFirst(
+      this.types || [],
+      type => type.identify && type.identify(obj)
+    ) ||
+    (this.parent && this.parent.findTypeOf(obj))
+  );
+};
+
+expectPrototype.findTypeOfWithParentType = function(obj, requiredParentType) {
+  return (
+    utils.findFirst(
+      this.types || [],
+      type =>
+        type.identify &&
+        type.identify(obj) &&
+        (!requiredParentType || type.is(requiredParentType))
+    ) ||
+    (this.parent &&
+      this.parent.findTypeOfWithParentType(obj, requiredParentType))
+  );
+};
+
+expectPrototype.findCommonType = function(a, b) {
+  const aAncestorIndex = {};
+  let current = this.findTypeOf(a);
+  while (current) {
+    aAncestorIndex[current.name] = current;
+    current = current.baseType;
+  }
+  current = this.findTypeOf(b);
+  while (current) {
+    if (aAncestorIndex[current.name]) {
+      return current;
+    }
+    current = current.baseType;
+  }
+};
+
+expectPrototype.addStyle = function(...args) {
   if (this._frozen) {
     throw new Error(
       'Cannot add a style to a frozen instance, please run .clone() first'
     );
   }
   this.output.addStyle(...args);
-  return this.expect;
+  return this;
 };
 
-Unexpected.prototype.installTheme = function(...args) {
+expectPrototype.installTheme = function(...args) {
   if (this._frozen) {
     throw new Error(
       'Cannot install a theme into a frozen instance, please run .clone() first'
     );
   }
   this.output.installTheme(...args);
-  return this.expect;
+  return this;
 };
 
 function getPluginName(plugin) {
@@ -956,7 +942,8 @@ function getPluginName(plugin) {
   }
 }
 
-Unexpected.prototype.use = function(plugin) {
+expectPrototype.use = function(plugin) {
+  this._assertTopLevelExpect();
   if (this._frozen) {
     throw new Error(
       'Cannot install a plugin into a frozen instance, please run .clone() first'
@@ -998,7 +985,7 @@ Unexpected.prototype.use = function(plugin) {
         plugin.version === existingPlugin.version)
     ) {
       // No-op
-      return this.expect;
+      return this;
     } else {
       throw new Error(
         `Another instance of the plugin '${pluginName}' is already installed${
@@ -1022,15 +1009,15 @@ Unexpected.prototype.use = function(plugin) {
 
   this.installedPlugins.push(plugin);
   if (typeof plugin === 'function') {
-    plugin(this.expect);
+    plugin(this);
   } else {
-    plugin.installInto(this.expect);
+    plugin.installInto(this);
   }
 
-  return this.expect; // for chaining
+  return this; // for chaining
 };
 
-Unexpected.prototype.withError = (body, handler) =>
+expectPrototype.withError = (body, handler) =>
   oathbreaker(
     makePromise(body).caught(e => {
       throwIfNonUnexpectedError(e);
@@ -1038,59 +1025,7 @@ Unexpected.prototype.withError = (body, handler) =>
     })
   );
 
-Unexpected.prototype.installPlugin = Unexpected.prototype.use; // Legacy alias
-
-function installExpectMethods(unexpected) {
-  const expect = function(...args) {
-    /// ...
-    return unexpected._expect(new Context(unexpected), args);
-  };
-  expect.it = unexpected.it.bind(unexpected);
-  expect.equal = unexpected.equal.bind(unexpected);
-  expect.inspect = unexpected.inspect.bind(unexpected);
-  if (nodeJsCustomInspect !== 'inspect') {
-    expect[nodeJsCustomInspect] = unexpected[nodeJsCustomInspect].bind(
-      unexpected
-    );
-  }
-  expect.findTypeOf = unexpected.findTypeOf; // Already bound
-  expect.fail = (...args) => {
-    try {
-      unexpected.fail(...args);
-    } catch (e) {
-      if (e && e._isUnexpected) {
-        unexpected.setErrorMessage(e);
-      }
-      throw e;
-    }
-  };
-
-  expect.createOutput = unexpected.createOutput.bind(unexpected);
-  expect.diff = unexpected.diff.bind(unexpected);
-  expect.promise = makePromise;
-  expect.withError = unexpected.withError;
-  expect.addAssertion = unexpected.addAssertion.bind(unexpected);
-  expect.addStyle = unexpected.addStyle.bind(unexpected);
-  expect.installTheme = unexpected.installTheme.bind(unexpected);
-  expect.addType = unexpected.addType.bind(unexpected);
-  expect.getType = unexpected.getType;
-  expect.clone = unexpected.clone.bind(unexpected);
-  expect.child = unexpected.child.bind(unexpected);
-  expect.freeze = unexpected.freeze.bind(unexpected);
-  expect.toString = unexpected.toString.bind(unexpected);
-  expect.assertions = unexpected.assertions;
-  expect.use = expect.installPlugin = unexpected.use.bind(unexpected);
-  expect.output = unexpected.output;
-  expect.outputFormat = unexpected.outputFormat.bind(unexpected);
-  expect.notifyPendingPromise = notifyPendingPromise;
-  expect.hook = fn => {
-    unexpected._expect = fn(unexpected._expect.bind(unexpected));
-  };
-  // TODO For testing purpose while we don't have all the pieces yet
-  expect.parseAssertion = unexpected.parseAssertion.bind(unexpected);
-
-  return expect;
-}
+expectPrototype.installPlugin = expectPrototype.use; // Legacy alias
 
 function calculateLimits(items) {
   return items.reduce(
@@ -1103,13 +1038,12 @@ function calculateLimits(items) {
   );
 }
 
-Unexpected.prototype.throwAssertionNotFoundError = function(
+expectPrototype.throwAssertionNotFoundError = function(
   subject,
   testDescriptionString,
   args
 ) {
   let candidateHandlers = this.assertions[testDescriptionString];
-  const that = this;
 
   let instance = this;
   while (instance && !candidateHandlers) {
@@ -1119,7 +1053,7 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
 
   if (candidateHandlers) {
     this.fail({
-      message(output) {
+      message: output => {
         const subjectOutput = output => {
           output.appendInspected(subject);
         };
@@ -1144,7 +1078,7 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
           .indentLines()
           .i()
           .text('<')
-          .text(that.findTypeOf(subject).name)
+          .text(this.findTypeOf(subject).name)
           .text('>')
           .sp()
           .text(testDescriptionString);
@@ -1153,7 +1087,7 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
           output
             .sp()
             .text('<')
-            .text(that.findTypeOf(arg).name)
+            .text(this.findTypeOf(arg).name)
             .text('>');
         });
 
@@ -1189,9 +1123,9 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
     instance = instance.parent;
   }
 
-  function compareAssertions(a, b) {
-    const aAssertion = that.lookupAssertionRule(subject, a, args);
-    const bAssertion = that.lookupAssertionRule(subject, b, args);
+  let compareAssertions = (a, b) => {
+    const aAssertion = this.lookupAssertionRule(subject, a, args);
+    const bAssertion = this.lookupAssertionRule(subject, b, args);
     if (!aAssertion && !bAssertion) {
       return 0;
     }
@@ -1203,7 +1137,7 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
     }
 
     return compareSpecificities(aAssertion.specificity, bAssertion.specificity);
-  }
+  };
 
   assertionStrings.forEach(assertionString => {
     const score = leven(testDescriptionString, assertionString);
@@ -1212,7 +1146,7 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
       assertion: assertionString,
       score
     });
-  }, this);
+  });
 
   const bestMatch = assertionsWithScore
     .sort((a, b) => {
@@ -1251,13 +1185,12 @@ Unexpected.prototype.throwAssertionNotFoundError = function(
   });
 };
 
-Unexpected.prototype.lookupAssertionRule = function(
+expectPrototype.lookupAssertionRule = function(
   subject,
   testDescriptionString,
   args,
   requireAssertionSuffix
 ) {
-  const that = this;
   if (typeof testDescriptionString !== 'string') {
     throw new Error(
       'The expect function requires the second parameter to be a string or an expect.it.'
@@ -1279,23 +1212,23 @@ Unexpected.prototype.lookupAssertionRule = function(
   }
   const cachedTypes = {};
 
-  function findTypeOf(value, key) {
+  let findTypeOf = (value, key) => {
     let type = cachedTypes[key];
     if (!type) {
-      type = that.findTypeOf(value);
+      type = this.findTypeOf(value);
       cachedTypes[key] = type;
     }
     return type;
-  }
+  };
 
-  function matches(value, assertionType, key, relaxed) {
+  let matches = (value, assertionType, key, relaxed) => {
     if (assertionType.is('assertion') && typeof value === 'string') {
       return true;
     }
 
     if (relaxed) {
       if (assertionType.identify === false) {
-        return that.types.some(
+        return this.types.some(
           type =>
             type.identify && type.is(assertionType) && type.identify(value)
         );
@@ -1304,7 +1237,7 @@ Unexpected.prototype.lookupAssertionRule = function(
     } else {
       return findTypeOf(value, key).is(assertionType);
     }
-  }
+  };
 
   function matchesHandler(handler, relaxed) {
     if (!matches(subject, handler.subject.type, 'subject', relaxed)) {
@@ -1352,159 +1285,182 @@ Unexpected.prototype.lookupAssertionRule = function(
   return null;
 };
 
-function makeExpectFunction(unexpected) {
-  const expect = installExpectMethods(unexpected);
-  unexpected.expect = expect;
-  return expect;
-}
+expectPrototype._assertTopLevelExpect = function() {
+  if (this !== this._topLevelExpect) {
+    throw new Error('This method only works on the top level expect function');
+  }
+};
 
-Unexpected.prototype.setErrorMessage = function(err) {
+expectPrototype._assertWrappedExpect = function() {
+  if (this === this._topLevelExpect) {
+    throw new Error(
+      'This method only works on the expect function handed to an assertion'
+    );
+  }
+};
+
+expectPrototype.setErrorMessage = function(err) {
   err.serializeMessage(this.outputFormat());
 };
 
-Unexpected.prototype._expect = function expect(context, args) {
-  const that = this;
-  const subject = args[0];
-  const testDescriptionString = args[1];
+expectPrototype._createWrappedExpect = function(
+  assertionRule,
+  subject,
+  args,
+  testDescriptionString,
+  context
+) {
+  const flags = extend({}, assertionRule.flags);
+  const parentExpect = this;
+
+  function wrappedExpect(subject, testDescriptionString) {
+    if (arguments.length === 0) {
+      throw new Error('The expect function requires at least one parameter.');
+    } else if (arguments.length === 1) {
+      return addAdditionalPromiseMethods(
+        makePromise.resolve(subject),
+        wrappedExpect,
+        subject
+      );
+    } else if (typeof testDescriptionString === 'function') {
+      wrappedExpect.errorMode = 'nested';
+      return wrappedExpect.withError(
+        () => testDescriptionString(subject),
+        err => {
+          wrappedExpect.fail(err);
+        }
+      );
+    }
+    testDescriptionString = utils.forwardFlags(testDescriptionString, flags);
 
-  if (args.length < 2) {
-    throw new Error('The expect function requires at least two parameters.');
-  } else if (typeof testDescriptionString === 'function') {
-    return that.expect.withError(
-      () => testDescriptionString(subject),
-      err => {
-        that.fail(err);
-      }
+    const args = new Array(arguments.length - 2);
+    for (let i = 2; i < arguments.length; i += 1) {
+      args[i - 2] = arguments[i];
+    }
+    return wrappedExpect._callInNestedContext(() =>
+      parentExpect._executeExpect(
+        context.child(),
+        subject,
+        testDescriptionString,
+        args
+      )
     );
   }
 
-  function executeExpect(context, subject, testDescriptionString, args) {
-    let assertionRule = that.lookupAssertionRule(
-      subject,
-      testDescriptionString,
-      args
-    );
+  utils.setPrototypeOfOrExtend(wrappedExpect, this);
 
-    if (!assertionRule) {
-      const tokens = testDescriptionString.split(' ');
-      // eslint-disable-next-line no-labels
-      OUTER: for (let n = tokens.length - 1; n > 0; n -= 1) {
-        const prefix = tokens.slice(0, n).join(' ');
-        const remainingTokens = tokens.slice(n);
-        const argsWithAssertionPrepended = [remainingTokens.join(' ')].concat(
-          args
-        );
-        assertionRule = that.lookupAssertionRule(
-          subject,
-          prefix,
-          argsWithAssertionPrepended,
-          true
-        );
-        if (assertionRule) {
-          // Found the longest prefix of the string that yielded a suitable assertion for the given subject and args
-          // To avoid bogus error messages when shifting later (#394) we require some prefix of the remaining tokens
-          // to be a valid assertion name:
-          for (let i = 1; i < remainingTokens.length; i += 1) {
-            if (
-              that.assertions.hasOwnProperty(
-                remainingTokens.slice(0, i + 1).join(' ')
-              )
-            ) {
-              testDescriptionString = prefix;
-              args = argsWithAssertionPrepended;
-              // eslint-disable-next-line no-labels
-              break OUTER;
-            }
-          }
-        }
-      }
-      if (!assertionRule) {
-        that.throwAssertionNotFoundError(subject, testDescriptionString, args);
-      }
-    }
+  wrappedExpect.context = context;
+  wrappedExpect.execute = wrappedExpect;
+  wrappedExpect.alternations = assertionRule.alternations;
+  wrappedExpect.flags = flags;
+  wrappedExpect.subject = subject;
+  wrappedExpect.testDescription = testDescriptionString;
+  wrappedExpect.args = args;
+  wrappedExpect.assertionRule = assertionRule;
+
+  wrappedExpect.subjectOutput = output => {
+    output.appendInspected(subject);
+  };
+  wrappedExpect.argsOutput = args.map((arg, i) => {
+    const argRule = wrappedExpect.assertionRule.args[i];
     if (
-      assertionRule &&
-      assertionRule.unexpected &&
-      assertionRule.unexpected !== that
+      typeof arg === 'string' &&
+      ((argRule && argRule.type.is('assertion')) ||
+        wrappedExpect._getAssertionIndices().indexOf(i) >= 0)
     ) {
-      return assertionRule.unexpected.expect(
-        subject,
-        testDescriptionString,
-        ...args
-      );
+      return new AssertionString(arg);
     }
 
-    const flags = extend({}, assertionRule.flags);
-    const wrappedExpect = function(subject, testDescriptionString) {
-      if (arguments.length === 0) {
-        throw new Error('The expect function requires at least one parameter.');
-      } else if (arguments.length === 1) {
-        return addAdditionalPromiseMethods(
-          makePromise.resolve(subject),
-          wrappedExpect,
-          subject
-        );
-      } else if (typeof testDescriptionString === 'function') {
-        wrappedExpect.errorMode = 'nested';
-        return wrappedExpect.withError(
-          () => testDescriptionString(subject),
-          err => {
-            wrappedExpect.fail(err);
-          }
-        );
-      }
-      testDescriptionString = utils.forwardFlags(testDescriptionString, flags);
-
-      const args = new Array(arguments.length - 2);
-      for (let i = 2; i < arguments.length; i += 1) {
-        args[i - 2] = arguments[i];
-      }
-      return wrappedExpect.callInNestedContext(() =>
-        executeExpect(context.child(), subject, testDescriptionString, args)
-      );
+    return output => {
+      output.appendInspected(arg);
     };
+  });
 
-    utils.setPrototypeOfOrExtend(wrappedExpect, that._wrappedExpectProto);
+  return wrappedExpect;
+};
 
-    wrappedExpect.context = context;
-    wrappedExpect.execute = wrappedExpect;
-    wrappedExpect.alternations = assertionRule.alternations;
-    wrappedExpect.flags = flags;
-    wrappedExpect.subject = subject;
-    wrappedExpect.testDescription = testDescriptionString;
-    wrappedExpect.args = args;
-    wrappedExpect.assertionRule = assertionRule;
+expectPrototype._executeExpect = function(
+  context,
+  subject,
+  testDescriptionString,
+  args
+) {
+  let assertionRule = this.lookupAssertionRule(
+    subject,
+    testDescriptionString,
+    args
+  );
 
-    wrappedExpect.subjectOutput = output => {
-      output.appendInspected(subject);
-    };
-    wrappedExpect.argsOutput = args.map((arg, i) => {
-      const argRule = wrappedExpect.assertionRule.args[i];
-      if (
-        typeof arg === 'string' &&
-        ((argRule && argRule.type.is('assertion')) ||
-          wrappedExpect._getAssertionIndices().indexOf(i) >= 0)
-      ) {
-        return new AssertionString(arg);
+  if (!assertionRule) {
+    const tokens = testDescriptionString.split(' ');
+    // eslint-disable-next-line no-labels
+    OUTER: for (let n = tokens.length - 1; n > 0; n -= 1) {
+      const prefix = tokens.slice(0, n).join(' ');
+      const remainingTokens = tokens.slice(n);
+      const argsWithAssertionPrepended = [remainingTokens.join(' ')].concat(
+        args
+      );
+      assertionRule = this.lookupAssertionRule(
+        subject,
+        prefix,
+        argsWithAssertionPrepended,
+        true
+      );
+      if (assertionRule) {
+        // Found the longest prefix of the string that yielded a suitable assertion for the given subject and args
+        // To avoid bogus error messages when shifting later (#394) we require some prefix of the remaining tokens
+        // to be a valid assertion name:
+        for (let i = 1; i < remainingTokens.length; i += 1) {
+          if (
+            this.assertions.hasOwnProperty(
+              remainingTokens.slice(0, i + 1).join(' ')
+            )
+          ) {
+            testDescriptionString = prefix;
+            args = argsWithAssertionPrepended;
+            // eslint-disable-next-line no-labels
+            break OUTER;
+          }
+        }
       }
+    }
+    if (!assertionRule) {
+      this.throwAssertionNotFoundError(subject, testDescriptionString, args);
+    }
+  }
 
-      return output => {
-        output.appendInspected(arg);
-      };
-    });
+  if (assertionRule.expect && assertionRule.expect !== this) {
+    return assertionRule.expect(subject, testDescriptionString, ...args);
+  }
 
-    // Eager-compute these properties in browsers that don't support getters
-    // (Object.defineProperty might be polyfilled by es5-sham):
-    if (!Object.__defineGetter__) {
-      wrappedExpect.subjectType = wrappedExpect._getSubjectType();
-      wrappedExpect.argTypes = wrappedExpect._getArgTypes();
-    }
+  const wrappedExpect = this._createWrappedExpect(
+    assertionRule,
+    subject,
+    args,
+    testDescriptionString,
+    context
+  );
+
+  return oathbreaker(assertionRule.handler(wrappedExpect, subject, ...args));
+};
 
-    return oathbreaker(assertionRule.handler(wrappedExpect, subject, ...args));
+expectPrototype._expect = function expect(context, args) {
+  const subject = args[0];
+  const testDescriptionString = args[1];
+
+  if (args.length < 2) {
+    throw new Error('The expect function requires at least two parameters.');
+  } else if (typeof testDescriptionString === 'function') {
+    return this.withError(
+      () => testDescriptionString(subject),
+      err => {
+        this.fail(err);
+      }
+    );
   }
 
   try {
-    let result = executeExpect(
+    let result = this._executeExpect(
       context,
       subject,
       testDescriptionString,
@@ -1513,10 +1469,10 @@ Unexpected.prototype._expect = function expect(context, args) {
     if (utils.isPromise(result)) {
       result = wrapPromiseIfNecessary(result);
       if (result.isPending()) {
-        that.expect.notifyPendingPromise(result);
+        this.notifyPendingPromise(result);
         result = result.then(undefined, e => {
           if (e && e._isUnexpected && context.level === 0) {
-            that.setErrorMessage(e);
+            this.setErrorMessage(e);
           }
           throw e;
         });
@@ -1524,7 +1480,7 @@ Unexpected.prototype._expect = function expect(context, args) {
     } else {
       result = makePromise.resolve(result);
     }
-    return addAdditionalPromiseMethods(result, that.expect, subject);
+    return addAdditionalPromiseMethods(result, this, subject);
   } catch (e) {
     if (e && e._isUnexpected) {
       let newError = e;
@@ -1532,7 +1488,7 @@ Unexpected.prototype._expect = function expect(context, args) {
         newError = e.clone();
       }
       if (context.level === 0) {
-        that.setErrorMessage(newError);
+        this.setErrorMessage(newError);
       }
       throw newError;
     }
@@ -1540,15 +1496,13 @@ Unexpected.prototype._expect = function expect(context, args) {
   }
 };
 
-Unexpected.prototype.diff = function(
+expectPrototype.diff = function(
   a,
   b,
   output = this.createOutput(),
   recursions,
   seen
 ) {
-  const that = this;
-
   const maxRecursions = 100;
   recursions = typeof recursions === 'number' ? recursions : maxRecursions;
   if (recursions <= 0) {
@@ -1565,13 +1519,13 @@ Unexpected.prototype.diff = function(
     b,
     output,
     (actual, expected) =>
-      that.diff(actual, expected, output.clone(), recursions - 1, seen),
+      this.diff(actual, expected, output.clone(), recursions - 1, seen),
     (v, depth) => output.clone().appendInspected(v, depth),
-    (actual, expected) => that.equal(actual, expected)
+    (actual, expected) => this.equal(actual, expected)
   );
 };
 
-Unexpected.prototype.toString = function() {
+expectPrototype.toString = function() {
   const assertions = this.assertions;
 
   const seen = {};
@@ -1594,12 +1548,13 @@ Unexpected.prototype.toString = function() {
   return pen.toString();
 };
 
-Unexpected.prototype.clone = function() {
+expectPrototype.clone = function() {
+  this._assertTopLevelExpect();
   const clonedAssertions = {};
   Object.keys(this.assertions).forEach(function(assertion) {
     clonedAssertions[assertion] = [].concat(this.assertions[assertion]);
   }, this);
-  const unexpected = new Unexpected({
+  const expect = createTopLevelExpect({
     assertions: clonedAssertions,
     types: [].concat(this.types),
     typeByName: extend({}, this.typeByName),
@@ -1608,14 +1563,15 @@ Unexpected.prototype.clone = function() {
     installedPlugins: [].concat(this.installedPlugins)
   });
   // Install the hooks:
-  unexpected._expect = this._expect;
+  expect._expect = this._expect;
   // Make sure that changes to the parent's preferredWidth doesn't propagate:
-  unexpected.output.preferredWidth = this.output.preferredWidth;
-  return makeExpectFunction(unexpected);
+  expect.output.preferredWidth = this.output.preferredWidth;
+  return expect;
 };
 
-Unexpected.prototype.child = function() {
-  const childUnexpected = new Unexpected({
+expectPrototype.child = function() {
+  this._assertTopLevelExpect();
+  const childExpect = createTopLevelExpect({
     assertions: {},
     types: [],
     typeByName: {},
@@ -1623,11 +1579,10 @@ Unexpected.prototype.child = function() {
     format: this.outputFormat(),
     installedPlugins: []
   });
-  const parent = (childUnexpected.parent = this);
-  const childExpect = makeExpectFunction(childUnexpected);
+  const parent = (childExpect.parent = this);
 
   childExpect.exportAssertion = function(testDescription, handler) {
-    parent.addAssertion(testDescription, handler, childUnexpected);
+    parent.addAssertion(testDescription, handler, childExpect);
     return this;
   };
   childExpect.exportType = function(type) {
@@ -1635,7 +1590,7 @@ Unexpected.prototype.child = function() {
       childExpect.addType(type);
     }
 
-    parent.addType(type, childUnexpected);
+    parent.addType(type, childExpect);
     return this;
   };
   childExpect.exportStyle = function(name, handler) {
@@ -1648,21 +1603,23 @@ Unexpected.prototype.child = function() {
   return childExpect;
 };
 
-Unexpected.prototype.freeze = function() {
+expectPrototype.freeze = function() {
+  this._assertTopLevelExpect();
   this._frozen = true;
-  return this.expect;
+  return this;
 };
 
-Unexpected.prototype.outputFormat = function(format) {
+expectPrototype.outputFormat = function(format) {
+  this._assertTopLevelExpect();
   if (typeof format === 'undefined') {
     return this._outputFormat;
   } else {
     this._outputFormat = format;
-    return this.expect;
+    return this;
   }
 };
 
-Unexpected.prototype.createOutput = function(format) {
+expectPrototype.createOutput = function(format) {
   const that = this;
   const output = this.output.clone(format || 'text');
   output.addStyle('appendInspected', function(value, depth) {
@@ -1671,157 +1628,288 @@ Unexpected.prototype.createOutput = function(format) {
   return output;
 };
 
-Unexpected.create = () => {
-  const unexpected = new Unexpected();
-  return makeExpectFunction(unexpected);
+expectPrototype.hook = function(fn) {
+  this._assertTopLevelExpect();
+  this._expect = fn(this._expect.bind(this));
 };
 
-var expandAssertion = (() => {
-  function isFlag(token) {
-    return token.slice(0, 1) === '[' && token.slice(-1) === ']';
+// This is not super elegant, but wrappedExpect.fail was different:
+expectPrototype.fail = function(...args) {
+  if (this === this._topLevelExpect) {
+    try {
+      this._fail(...args);
+    } catch (e) {
+      if (e && e._isUnexpected) {
+        this.setErrorMessage(e);
+      }
+      throw e;
+    }
+  } else {
+    this._callInNestedContext(() => {
+      this.context.expect.fail(...args);
+    });
   }
-  function isAlternation(token) {
-    return token.slice(0, 1) === '(' && token.slice(-1) === ')';
+};
+
+function lookupAssertionsInParentChain(assertionString, expect) {
+  const assertions = [];
+  for (let instance = expect; instance; instance = instance.parent) {
+    if (instance.assertions[assertionString]) {
+      assertions.push(...instance.assertions[assertionString]);
+    }
   }
-  function removeEmptyStrings(texts) {
-    return texts.filter(text => text !== '');
+  return assertions;
+}
+
+function findSuffixAssertions(assertionString, expect) {
+  if (typeof assertionString !== 'string') {
+    return null;
   }
-  function createPermutations(tokens, index) {
-    if (index === tokens.length) {
-      return [{ text: '', flags: {}, alternations: [] }];
+  const straightforwardAssertions = lookupAssertionsInParentChain(
+    assertionString,
+    expect
+  );
+  if (straightforwardAssertions.length > 0) {
+    return straightforwardAssertions;
+  }
+  const tokens = assertionString.split(' ');
+  for (let n = tokens.length - 1; n > 0; n -= 1) {
+    const suffix = tokens.slice(n).join(' ');
+    const suffixAssertions = lookupAssertionsInParentChain(suffix, expect);
+    if (
+      findSuffixAssertions(tokens.slice(0, n).join(' '), expect) &&
+      suffixAssertions.length > 0
+    ) {
+      return suffixAssertions;
     }
+  }
+  return null;
+}
 
-    const token = tokens[index];
-    const tail = createPermutations(tokens, index + 1);
-    if (isFlag(token)) {
-      const flag = token.slice(1, -1);
-      return tail
-        .map(pattern => {
-          const flags = {};
-          flags[flag] = true;
-          return {
-            text: `${flag} ${pattern.text}`,
-            flags: extend(flags, pattern.flags),
-            alternations: pattern.alternations
-          };
-        })
-        .concat(
-          tail.map(pattern => {
-            const flags = {};
-            flags[flag] = false;
-            return {
-              text: pattern.text,
-              flags: extend(flags, pattern.flags),
-              alternations: pattern.alternations
-            };
-          })
-        );
-    } else if (isAlternation(token)) {
-      return token
-        .substr(1, token.length - 2) // Remove parentheses
-        .split(/\|/)
-        .reduce(
-          (result, alternation) =>
-            result.concat(
-              tail.map(({ text, flags, alternations }) => ({
-                // Make sure that an empty alternation doesn't produce two spaces:
-                text: alternation ? alternation + text : text.replace(/^ /, ''),
-
-                flags: flags,
-                alternations: [alternation].concat(alternations)
-              }))
-            ),
-          []
-        );
+expectPrototype.standardErrorMessage = function(output, options) {
+  this._assertWrappedExpect();
+  options = typeof options === 'object' ? options : {};
+
+  if ('omitSubject' in output) {
+    options.subject = this.subject;
+  }
+
+  if (options && options.compact) {
+    options.compactSubject = output => {
+      output.jsFunctionName(this.subjectType.name);
+    };
+  }
+  return createStandardErrorMessage(
+    output,
+    this.subjectOutput,
+    this.testDescription,
+    this.argsOutput,
+    options
+  );
+};
+
+expectPrototype._callInNestedContext = function(callback) {
+  this._assertWrappedExpect();
+  try {
+    let result = oathbreaker(callback());
+    if (utils.isPromise(result)) {
+      result = wrapPromiseIfNecessary(result);
+      if (result.isPending()) {
+        result = result.then(undefined, e => {
+          if (e && e._isUnexpected) {
+            const wrappedError = new UnexpectedError(this, e);
+            wrappedError.originalError = e.originalError;
+            throw wrappedError;
+          }
+          throw e;
+        });
+      }
     } else {
-      return tail.map(({ text, flags, alternations }) => ({
-        text: token + text,
-        flags: flags,
-        alternations: alternations
-      }));
+      result = makePromise.resolve(result);
     }
+    return addAdditionalPromiseMethods(result, this.execute, this.subject);
+  } catch (e) {
+    if (e && e._isUnexpected) {
+      const wrappedError = new UnexpectedError(this, e);
+      wrappedError.originalError = e.originalError;
+      throw wrappedError;
+    }
+    throw e;
   }
-  return pattern => {
-    pattern = pattern.replace(/(\[[^\]]+\]) ?/g, '$1');
-    const splitRegex = /\[[^\]]+\]|\([^)]+\)/g;
-    let tokens = [];
-    let m;
-    let lastIndex = 0;
-    while ((m = splitRegex.exec(pattern))) {
-      tokens.push(pattern.slice(lastIndex, m.index));
-      tokens.push(pattern.slice(m.index, splitRegex.lastIndex));
-      lastIndex = splitRegex.lastIndex;
+};
+
+expectPrototype.shift = function(subject, assertionIndex) {
+  this._assertWrappedExpect();
+  if (arguments.length <= 1) {
+    if (arguments.length === 0) {
+      subject = this.subject;
     }
-    tokens.push(pattern.slice(lastIndex));
-    tokens = removeEmptyStrings(tokens);
-    const permutations = createPermutations(tokens, 0);
-    permutations.forEach(permutation => {
-      permutation.text = permutation.text.trim();
-      if (permutation.text === '') {
-        // This can only happen if the pattern only contains flags
-        throw new Error('Assertion patterns must not only contain flags');
+    assertionIndex = -1;
+    for (let i = 0; i < this.assertionRule.args.length; i += 1) {
+      const type = this.assertionRule.args[i].type;
+      if (type.is('assertion') || type.is('expect.it')) {
+        assertionIndex = i;
+        break;
       }
-    });
-    return permutations;
-  };
-})();
-
-function ensureValidUseOfParenthesesOrBrackets(pattern) {
-  const counts = {
-    '[': 0,
-    ']': 0,
-    '(': 0,
-    ')': 0
-  };
-  for (let i = 0; i < pattern.length; i += 1) {
-    const c = pattern.charAt(i);
-    if (c in counts) {
-      counts[c] += 1;
     }
-    if (c === ']' && counts['['] >= counts[']']) {
-      if (counts['['] === counts[']'] + 1) {
-        throw new Error(
-          `Assertion patterns must not contain flags with brackets: '${pattern}'`
-        );
-      }
+  } else if (arguments.length === 3) {
+    // The 3-argument syntax for wrappedExpect.shift is deprecated, please omit the first (expect) arg
+    subject = arguments[1];
+    assertionIndex = arguments[2];
+  }
+
+  if (assertionIndex !== -1) {
+    const args = this.args.slice(0, assertionIndex);
+    const rest = this.args.slice(assertionIndex);
+    const nextArgumentType = this.findTypeOf(rest[0]);
+    if (arguments.length > 1) {
+      // Legacy
+      this.argsOutput = output => {
+        args.forEach((arg, index) => {
+          if (index > 0) {
+            output.text(', ');
+          }
+          output.appendInspected(arg);
+        });
 
-      if (counts['('] !== counts[')']) {
-        throw new Error(
-          `Assertion patterns must not contain flags with parentheses: '${pattern}'`
-        );
-      }
+        if (args.length > 0) {
+          output.sp();
+        }
+        if (nextArgumentType.is('string')) {
+          output.error(rest[0]);
+        } else if (rest.length > 0) {
+          output.appendInspected(rest[0]);
+        }
+        if (rest.length > 1) {
+          output.sp();
+        }
+        rest.slice(1).forEach((arg, index) => {
+          if (index > 0) {
+            output.text(', ');
+          }
+          output.appendInspected(arg);
+        });
+      };
+    }
+    if (nextArgumentType.is('expect.it')) {
+      return this.withError(
+        () => rest[0](subject),
+        err => {
+          this.fail(err);
+        }
+      );
+    } else if (nextArgumentType.is('string')) {
+      return this.execute(subject, ...rest);
+    } else {
+      return subject;
+    }
+  } else {
+    // No assertion to delegate to. Provide the new subject as the fulfillment value:
+    return subject;
+  }
+};
 
-      if (pattern.charAt(i - 1) === '[') {
-        throw new Error(
-          `Assertion patterns must not contain empty flags: '${pattern}'`
-        );
-      }
-    } else if (c === ')' && counts['('] >= counts[')']) {
-      if (counts['('] === counts[')'] + 1) {
-        throw new Error(
-          `Assertion patterns must not contain alternations with parentheses: '${pattern}'`
-        );
-      }
+expectPrototype._getSubjectType = function() {
+  this._assertWrappedExpect();
+  return this.findTypeOfWithParentType(
+    this.subject,
+    this.assertionRule.subject.type
+  );
+};
 
-      if (counts['['] !== counts[']']) {
-        throw new Error(
-          `Assertion patterns must not contain alternations with brackets: '${pattern}'`
+expectPrototype._getArgTypes = function(index) {
+  this._assertWrappedExpect();
+  const lastIndex = this.assertionRule.args.length - 1;
+  return this.args.map((arg, index) => {
+    return this.findTypeOfWithParentType(
+      arg,
+      this.assertionRule.args[Math.min(index, lastIndex)].type
+    );
+  });
+};
+
+expectPrototype._getAssertionIndices = function() {
+  this._assertWrappedExpect();
+  if (!this._assertionIndices) {
+    const assertionIndices = [];
+    const args = this.args;
+    let currentAssertionRule = this.assertionRule;
+    let offset = 0;
+    // eslint-disable-next-line no-labels
+    OUTER: while (true) {
+      if (
+        currentAssertionRule.args.length > 1 &&
+        isAssertionArg(
+          currentAssertionRule.args[currentAssertionRule.args.length - 2]
+        )
+      ) {
+        assertionIndices.push(offset + currentAssertionRule.args.length - 2);
+        const suffixAssertions = findSuffixAssertions(
+          args[offset + currentAssertionRule.args.length - 2],
+          this
         );
+        if (suffixAssertions) {
+          for (let i = 0; i < suffixAssertions.length; i += 1) {
+            if (suffixAssertions[i].args.some(isAssertionArg)) {
+              offset += currentAssertionRule.args.length - 1;
+              currentAssertionRule = suffixAssertions[i];
+              // eslint-disable-next-line no-labels
+              continue OUTER;
+            }
+          }
+        }
       }
+      // No further assertions found,
+      break;
     }
+    this._assertionIndices = assertionIndices;
   }
+  return this._assertionIndices;
+};
 
-  if (counts['['] !== counts[']']) {
-    throw new Error(
-      `Assertion patterns must not contain unbalanced brackets: '${pattern}'`
-    );
-  }
-
-  if (counts['('] !== counts[')']) {
-    throw new Error(
-      `Assertion patterns must not contain unbalanced parentheses: '${pattern}'`
-    );
-  }
+Object.defineProperty(expectPrototype, 'subjectType', {
+  enumerable: true,
+  get() {
+    this._assertWrappedExpect();
+    return this._getSubjectType();
+  }
+});
+
+Object.defineProperty(expectPrototype, 'argTypes', {
+  enumerable: true,
+  get() {
+    this._assertWrappedExpect();
+    return this._getArgTypes();
+  }
+});
+
+function createTopLevelExpect({
+  assertions = {},
+  typeByName = { any: anyType },
+  types = [anyType],
+  output,
+  format = magicpen.defaultFormat,
+  installedPlugins = []
+} = {}) {
+  const expect = function(...args) {
+    return expect._expect(new Context(expect), args);
+  };
+  utils.setPrototypeOfOrExtend(expect, expectPrototype);
+
+  if (!output) {
+    output = magicpen();
+    output.inline = false;
+    output.diff = false;
+  }
+  return extend(expect, {
+    _topLevelExpect: expect,
+    _outputFormat: format,
+    assertions,
+    typeByName,
+    installedPlugins,
+    types,
+    output
+  });
 }
 
-module.exports = Unexpected;
+module.exports = createTopLevelExpect;
diff --git a/lib/createWrappedExpectProto.js b/lib/createWrappedExpectProto.js
deleted file mode 100644
index 2b96c814..00000000
--- a/lib/createWrappedExpectProto.js
+++ /dev/null
@@ -1,280 +0,0 @@
-const createStandardErrorMessage = require('./createStandardErrorMessage');
-const makePromise = require('./makePromise');
-const addAdditionalPromiseMethods = require('./addAdditionalPromiseMethods');
-const wrapPromiseIfNecessary = require('./wrapPromiseIfNecessary');
-const oathbreaker = require('./oathbreaker');
-const UnexpectedError = require('./UnexpectedError');
-const utils = require('./utils');
-const nodeJsCustomInspect = require('./nodeJsCustomInspect');
-
-function isAssertionArg({ type }) {
-  return type.is('assertion');
-}
-
-function lookupAssertionsInParentChain(assertionString, unexpected) {
-  const assertions = [];
-  for (let instance = unexpected; instance; instance = instance.parent) {
-    if (instance.assertions[assertionString]) {
-      assertions.push(...instance.assertions[assertionString]);
-    }
-  }
-  return assertions;
-}
-
-function findSuffixAssertions(assertionString, unexpected) {
-  if (typeof assertionString !== 'string') {
-    return null;
-  }
-  const straightforwardAssertions = lookupAssertionsInParentChain(
-    assertionString,
-    unexpected
-  );
-  if (straightforwardAssertions.length > 0) {
-    return straightforwardAssertions;
-  }
-  const tokens = assertionString.split(' ');
-  for (let n = tokens.length - 1; n > 0; n -= 1) {
-    const suffix = tokens.slice(n).join(' ');
-    const suffixAssertions = lookupAssertionsInParentChain(suffix, unexpected);
-    if (
-      findSuffixAssertions(tokens.slice(0, n).join(' '), unexpected) &&
-      suffixAssertions.length > 0
-    ) {
-      return suffixAssertions;
-    }
-  }
-  return null;
-}
-
-module.exports = function createWrappedExpectProto(unexpected) {
-  const wrappedExpectProto = {
-    promise: makePromise,
-    errorMode: 'default',
-
-    equal: unexpected.equal,
-    inspect: unexpected.inspect,
-    createOutput: unexpected.createOutput.bind(unexpected),
-    findTypeOf: unexpected.findTypeOf.bind(unexpected),
-    findTypeOfWithParentType: unexpected.findTypeOfWithParentType.bind(
-      unexpected
-    ),
-    findCommonType: unexpected.findCommonType.bind(unexpected),
-    it(...args) {
-      if (typeof args[0] === 'string') {
-        args[0] = utils.forwardFlags(args[0], this.flags);
-      }
-      return unexpected.it(...args);
-    },
-    diff: unexpected.diff,
-    getType: unexpected.getType,
-    output: unexpected.output,
-    outputFormat: unexpected.outputFormat.bind(unexpected),
-    format: unexpected.format,
-    withError: unexpected.withError,
-
-    fail(...args) {
-      const expect = this.context.expect;
-      this.callInNestedContext(() => {
-        expect.fail(...args);
-      });
-    },
-    standardErrorMessage(output, options) {
-      const that = this;
-      options = typeof options === 'object' ? options : {};
-
-      if ('omitSubject' in output) {
-        options.subject = this.subject;
-      }
-
-      if (options && options.compact) {
-        options.compactSubject = output => {
-          output.jsFunctionName(that.subjectType.name);
-        };
-      }
-      return createStandardErrorMessage(
-        output,
-        that.subjectOutput,
-        that.testDescription,
-        that.argsOutput,
-        options
-      );
-    },
-    callInNestedContext(callback) {
-      const that = this;
-
-      try {
-        let result = oathbreaker(callback());
-        if (utils.isPromise(result)) {
-          result = wrapPromiseIfNecessary(result);
-          if (result.isPending()) {
-            result = result.then(undefined, e => {
-              if (e && e._isUnexpected) {
-                const wrappedError = new UnexpectedError(that, e);
-                wrappedError.originalError = e.originalError;
-                throw wrappedError;
-              }
-              throw e;
-            });
-          }
-        } else {
-          result = makePromise.resolve(result);
-        }
-        return addAdditionalPromiseMethods(result, that.execute, that.subject);
-      } catch (e) {
-        if (e && e._isUnexpected) {
-          const wrappedError = new UnexpectedError(that, e);
-          wrappedError.originalError = e.originalError;
-          throw wrappedError;
-        }
-        throw e;
-      }
-    },
-    shift(subject, assertionIndex) {
-      if (arguments.length <= 1) {
-        if (arguments.length === 0) {
-          subject = this.subject;
-        }
-        assertionIndex = -1;
-        for (let i = 0; i < this.assertionRule.args.length; i += 1) {
-          const type = this.assertionRule.args[i].type;
-          if (type.is('assertion') || type.is('expect.it')) {
-            assertionIndex = i;
-            break;
-          }
-        }
-      } else if (arguments.length === 3) {
-        // The 3-argument syntax for wrappedExpect.shift is deprecated, please omit the first (expect) arg
-        subject = arguments[1];
-        assertionIndex = arguments[2];
-      }
-
-      if (assertionIndex !== -1) {
-        const args = this.args.slice(0, assertionIndex);
-        const rest = this.args.slice(assertionIndex);
-        const nextArgumentType = this.findTypeOf(rest[0]);
-        if (arguments.length > 1) {
-          // Legacy
-          this.argsOutput = output => {
-            args.forEach((arg, index) => {
-              if (index > 0) {
-                output.text(', ');
-              }
-              output.appendInspected(arg);
-            });
-
-            if (args.length > 0) {
-              output.sp();
-            }
-            if (nextArgumentType.is('string')) {
-              output.error(rest[0]);
-            } else if (rest.length > 0) {
-              output.appendInspected(rest[0]);
-            }
-            if (rest.length > 1) {
-              output.sp();
-            }
-            rest.slice(1).forEach((arg, index) => {
-              if (index > 0) {
-                output.text(', ');
-              }
-              output.appendInspected(arg);
-            });
-          };
-        }
-        if (nextArgumentType.is('expect.it')) {
-          const that = this;
-          return this.withError(
-            () => rest[0](subject),
-            err => {
-              that.fail(err);
-            }
-          );
-        } else if (nextArgumentType.is('string')) {
-          return this.execute(subject, ...rest);
-        } else {
-          return subject;
-        }
-      } else {
-        // No assertion to delegate to. Provide the new subject as the fulfillment value:
-        return subject;
-      }
-    },
-    _getSubjectType() {
-      return this.findTypeOfWithParentType(
-        this.subject,
-        this.assertionRule.subject.type
-      );
-    },
-    _getArgTypes(index) {
-      const lastIndex = this.assertionRule.args.length - 1;
-      return this.args.map(function(arg, index) {
-        return this.findTypeOfWithParentType(
-          arg,
-          this.assertionRule.args[Math.min(index, lastIndex)].type
-        );
-      }, this);
-    },
-    _getAssertionIndices() {
-      if (!this._assertionIndices) {
-        const assertionIndices = [];
-        const args = this.args;
-        let currentAssertionRule = this.assertionRule;
-        let offset = 0;
-        // eslint-disable-next-line no-labels
-        OUTER: while (true) {
-          if (
-            currentAssertionRule.args.length > 1 &&
-            isAssertionArg(
-              currentAssertionRule.args[currentAssertionRule.args.length - 2]
-            )
-          ) {
-            assertionIndices.push(
-              offset + currentAssertionRule.args.length - 2
-            );
-            const suffixAssertions = findSuffixAssertions(
-              args[offset + currentAssertionRule.args.length - 2],
-              unexpected
-            );
-            if (suffixAssertions) {
-              for (let i = 0; i < suffixAssertions.length; i += 1) {
-                if (suffixAssertions[i].args.some(isAssertionArg)) {
-                  offset += currentAssertionRule.args.length - 1;
-                  currentAssertionRule = suffixAssertions[i];
-                  // eslint-disable-next-line no-labels
-                  continue OUTER;
-                }
-              }
-            }
-          }
-          // No further assertions found,
-          break;
-        }
-        this._assertionIndices = assertionIndices;
-      }
-      return this._assertionIndices;
-    }
-  };
-
-  if (nodeJsCustomInspect !== 'inspect') {
-    wrappedExpectProto[nodeJsCustomInspect] = unexpected[nodeJsCustomInspect];
-  }
-
-  if (Object.__defineGetter__) {
-    Object.defineProperty(wrappedExpectProto, 'subjectType', {
-      enumerable: true,
-      get() {
-        return this.assertionRule && this._getSubjectType();
-      }
-    });
-
-    Object.defineProperty(wrappedExpectProto, 'argTypes', {
-      enumerable: true,
-      get() {
-        return this.assertionRule && this._getArgTypes();
-      }
-    });
-  }
-
-  utils.setPrototypeOfOrExtend(wrappedExpectProto, Function.prototype);
-  return wrappedExpectProto;
-};
diff --git a/lib/ensureValidUseOfParenthesesOrBrackets.js b/lib/ensureValidUseOfParenthesesOrBrackets.js
new file mode 100644
index 00000000..3ca7922e
--- /dev/null
+++ b/lib/ensureValidUseOfParenthesesOrBrackets.js
@@ -0,0 +1,59 @@
+function ensureValidUseOfParenthesesOrBrackets(pattern) {
+  const counts = {
+    '[': 0,
+    ']': 0,
+    '(': 0,
+    ')': 0
+  };
+  for (let i = 0; i < pattern.length; i += 1) {
+    const c = pattern.charAt(i);
+    if (c in counts) {
+      counts[c] += 1;
+    }
+    if (c === ']' && counts['['] >= counts[']']) {
+      if (counts['['] === counts[']'] + 1) {
+        throw new Error(
+          `Assertion patterns must not contain flags with brackets: '${pattern}'`
+        );
+      }
+
+      if (counts['('] !== counts[')']) {
+        throw new Error(
+          `Assertion patterns must not contain flags with parentheses: '${pattern}'`
+        );
+      }
+
+      if (pattern.charAt(i - 1) === '[') {
+        throw new Error(
+          `Assertion patterns must not contain empty flags: '${pattern}'`
+        );
+      }
+    } else if (c === ')' && counts['('] >= counts[')']) {
+      if (counts['('] === counts[')'] + 1) {
+        throw new Error(
+          `Assertion patterns must not contain alternations with parentheses: '${pattern}'`
+        );
+      }
+
+      if (counts['['] !== counts[']']) {
+        throw new Error(
+          `Assertion patterns must not contain alternations with brackets: '${pattern}'`
+        );
+      }
+    }
+  }
+
+  if (counts['['] !== counts[']']) {
+    throw new Error(
+      `Assertion patterns must not contain unbalanced brackets: '${pattern}'`
+    );
+  }
+
+  if (counts['('] !== counts[')']) {
+    throw new Error(
+      `Assertion patterns must not contain unbalanced parentheses: '${pattern}'`
+    );
+  }
+}
+
+module.exports = ensureValidUseOfParenthesesOrBrackets;
diff --git a/lib/expandAssertion.js b/lib/expandAssertion.js
new file mode 100644
index 00000000..ada5f4da
--- /dev/null
+++ b/lib/expandAssertion.js
@@ -0,0 +1,92 @@
+const extend = require('./utils').extend;
+
+function isFlag(token) {
+  return token.slice(0, 1) === '[' && token.slice(-1) === ']';
+}
+function isAlternation(token) {
+  return token.slice(0, 1) === '(' && token.slice(-1) === ')';
+}
+function removeEmptyStrings(texts) {
+  return texts.filter(text => text !== '');
+}
+function createPermutations(tokens, index) {
+  if (index === tokens.length) {
+    return [{ text: '', flags: {}, alternations: [] }];
+  }
+
+  const token = tokens[index];
+  const tail = createPermutations(tokens, index + 1);
+  if (isFlag(token)) {
+    const flag = token.slice(1, -1);
+    return tail
+      .map(pattern => {
+        const flags = {};
+        flags[flag] = true;
+        return {
+          text: `${flag} ${pattern.text}`,
+          flags: extend(flags, pattern.flags),
+          alternations: pattern.alternations
+        };
+      })
+      .concat(
+        tail.map(pattern => {
+          const flags = {};
+          flags[flag] = false;
+          return {
+            text: pattern.text,
+            flags: extend(flags, pattern.flags),
+            alternations: pattern.alternations
+          };
+        })
+      );
+  } else if (isAlternation(token)) {
+    return token
+      .substr(1, token.length - 2) // Remove parentheses
+      .split(/\|/)
+      .reduce(
+        (result, alternation) =>
+          result.concat(
+            tail.map(({ text, flags, alternations }) => ({
+              // Make sure that an empty alternation doesn't produce two spaces:
+              text: alternation ? alternation + text : text.replace(/^ /, ''),
+
+              flags,
+              alternations: [alternation, ...alternations]
+            }))
+          ),
+        []
+      );
+  } else {
+    return tail.map(({ text, flags, alternations }) => ({
+      text: token + text,
+      flags,
+      alternations
+    }));
+  }
+}
+
+function expandAssertion(pattern) {
+  pattern = pattern.replace(/(\[[^\]]+\]) ?/g, '$1');
+  const splitRegex = /\[[^\]]+\]|\([^)]+\)/g;
+  let tokens = [];
+  let m;
+  let lastIndex = 0;
+  while ((m = splitRegex.exec(pattern))) {
+    tokens.push(pattern.slice(lastIndex, m.index));
+    tokens.push(pattern.slice(m.index, splitRegex.lastIndex));
+    lastIndex = splitRegex.lastIndex;
+  }
+  tokens.push(pattern.slice(lastIndex));
+  tokens = removeEmptyStrings(tokens);
+  const permutations = createPermutations(tokens, 0);
+  permutations.forEach(permutation => {
+    permutation.text = permutation.text.trim();
+    if (permutation.text === '') {
+      // This can only happen if the pattern only contains flags
+      throw new Error('Assertion patterns must not only contain flags');
+    }
+  });
+  return permutations;
+}
+
+module.exports = expandAssertion;
diff --git a/lib/index.js b/lib/index.js
index c09f88e0..334df6e6 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,5 +1,4 @@
-module.exports = require('./Unexpected')
-  .create()
+const expect = require('./createTopLevelExpect')()
   .use(require('./styles'))
   .use(require('./types'))
   .use(require('./assertions'))
@@ -9,8 +8,10 @@ module.exports = require('./Unexpected')
 require('unexpected-bluebird').prototype[
   require('./nodeJsCustomInspect')
 ] = function() {
-  return module.exports
+  return expect
     .createOutput(require('magicpen').defaultFormat)
     .appendInspected(this)
     .toString();
 };
+
+module.exports = expect;
diff --git a/lib/styles.js b/lib/styles.js
index e58c8e7c..dbe9cf51 100644
--- a/lib/styles.js
+++ b/lib/styles.js
@@ -555,7 +555,7 @@ module.exports = expect => {
               top: topByChangeNumber[start],
               width: 1 + (length - i) * 2,
               height: topByChangeNumber[end] - topByChangeNumber[start] + 1,
-              direction: direction
+              direction
             })
           );
         });


Test patch:
diff --git a/test/unexpected.spec.js b/test/unexpected.spec.js
index 598f04a3..e2d95228 100644
--- a/test/unexpected.spec.js
+++ b/test/unexpected.spec.js
@@ -1353,4 +1353,44 @@ describe('unexpected', () => {
       );
     });
   });
+
+  it('throws when calling a function that only works on the expect handed to a function', () => {
+    expect(
+      () => expect.shift(),
+      'to throw',
+      'This method only works on the expect function handed to an assertion'
+    );
+  });
+
+  describe('with a cloned expect', () => {
+    it('throws when calling a function that only works on the expect handed to a function', () => {
+      expect(
+        () => expect.clone().shift(),
+        'to throw',
+        'This method only works on the expect function handed to an assertion'
+      );
+    });
+  });
+
+  describe('with a child expect', () => {
+    it('throws when calling a function that only works on the expect function handed to an assertion', () => {
+      expect(
+        () => expect.child().shift(),
+        'to throw',
+        'This method only works on the expect function handed to an assertion'
+      );
+    });
+  });
+
+  it('throws when calling a function that only works on the top-level expect', () => {
+    const clonedExpect = expect.clone().addAssertion('<any> to foo', expect => {
+      expect.use(() => {});
+    });
+
+    expect(
+      () => clonedExpect(123, 'to foo'),
+      'to throw',
+      'This method only works on the top level expect function'
+    );
+  });
 });