'use strict';
let RootNode = require('./RootNode.js');
let TerminusNode = require('./TerminusNode.js');
let ExpectedCallNode = require('./ExpectedCallNode.js');
let AndNode = require('./AndNode.js');
let NotAllCallsOccurredError = require('../Error/NotAllCallsOccurredError.js');
let OutOfOrderCallError = require('../Error/OutOfOrderCallError.js');
let UnexpectedArgumentsError = require('../Error/UnexpectedArgumentsError.js');
let UnexpectedFunctionCallError = require('../Error/UnexpectedFunctionCallError.js');
/**
* Classes the make up the expected execution path.
* @namespace Tree
*/
/**
* Represents the expected execution path of all {@link ExpectedCall}s.
* @memberof Tree
*/
class Tree {
/**
* Creates a new {@link Tree.Tree}
* @param {Tree.AndNode|Tree.ExpectedCallNode} node Inital node in this tree.
*/
constructor(node) {
this._root = new RootNode();
this._ignoreOtherCalls = false;
this._chainNodes(this._root, node);
this._chainNodes(node, new TerminusNode());
}
/**
* Tells this tree to ignore unexpected and out of order calls during execution
* and focus only on the required ones.
*/
ignoreOtherCalls() {
this._ignoreOtherCalls = true;
this._calls[0].mock.ignoreOtherCalls = true;
}
/**
* Chains two nodes together by making `a` the parent of `b`
* @param {Tree.Node} a Node that will be the parent of b
* @param {Tree.Node} b Node that will be the child of a
*/
_chainNodes(a, b) {
a.child = b;
b.parent = a;
}
/**
* Gets last non-{@link Tree.TerminusNode} node in this tree.
* @returns {Tree.Node} Last non-terminus node
*/
get _lastNode() {
let node = this._root;
while(!(node.child instanceof TerminusNode)) {
node = node.child;
}
return node;
}
/**
* Combines two execution {@link Tree.Tree}s together via an `AND` operation.
* @param {Tree.Tree} tree Tree to combine with this tree.
*/
and(tree) {
let andNode;
let lastNode = this._lastNode;
if(lastNode instanceof ExpectedCallNode) {
andNode = new AndNode(lastNode.expectedCall);
}
else if(lastNode instanceof AndNode) {
andNode = lastNode;
}
else {
throw new Error('Unexpected type for this node, expected AndNode or ExpectedCallNode');
}
let node = tree._root.child;
andNode.merge(node);
this._chainNodes(lastNode.parent, andNode);
this._chainNodes(andNode, node.child);
this._ignoreOtherCalls = tree._ignoreOtherCalls;
}
/**
* Combines two execution {@link Tree.Tree}s together via a `THEN` operation.
* @param {Tree.Tree} tree Tree to combine with this tree.
*/
then(tree) {
let node = tree._root.child;
this._chainNodes(this._lastNode, node);
this._ignoreOtherCalls = tree._ignoreOtherCalls;
}
/**
* Gets all {@link ExpectedCall}s that come after the specified node.
* @param {Tree.AndNode|Tree.ExpectedCallNode} node Current node in the tree.
* @returns {ExpectedCall[]} List of expected calls that come after the specified node.
*/
_callsAfter(node) {
let calls = [];
node = node.child;
while(!(node instanceof TerminusNode)) {
if(node instanceof ExpectedCallNode) {
calls.push(node.expectedCall);
}
else if(node instanceof AndNode) {
for(let expectedCall of node.expectedCalls) {
calls.push(expectedCall);
}
}
else {
throw new Error('Unexpected type for node, expected AndNode or ExpectedCallNode');
}
node = node.child;
}
return calls;
}
/**
* Gets all {@link ExpectedCall}s in this tree.
* @return {ExpectedCall[]} All the expected calls in this tree.
*/
get _calls() {
return this._callsAfter(this._root);
}
/**
* Checks to see if all required {@link ExpectedCall}s in this tree were completed during execution.
* @throws {Errors.NotAllCallsOccurredError} Will throw an error if any required expected calls are incomplete.
*/
_checkCalls() {
let result = true;
for(let expectedCall of this._calls) {
if(expectedCall.required && !expectedCall.completed) {
result = false;
break;
}
}
if(!result) {
throw new NotAllCallsOccurredError(this._calls);
}
}
/**
* Resets all {@link Mock} globals and handlers
*/
_resetMocks() {
for(let call of this._calls) {
call.mock.reset();
}
}
_checkRemainingExpectations(mock, args) {
if(!this._ignoreOtherCalls) {
if(this._executingNode.partialMatch(mock)) {
throw new UnexpectedArgumentsError(mock, args, this._calls);
}
if(this._callsAfter(this._executingNode).filter((ec) => ec.matches(mock, args)).length > 0) {
throw new OutOfOrderCallError(mock, args, this._calls);
}
// no match
throw new UnexpectedFunctionCallError(mock, args, this._calls);
}
}
/**
* Attempts to execute a call to a {@link Mock}.
* @param {Mock} mock Mock that was called.
* @param {object[]} args Arguments for the call.
* @returns {object|undefined} Will return a value if the mock as a return value; otherwise undefined.
* @throws {Error} Will throw an error if the mock has a throw value. This is normal and should be handled by the code under test.
* @throws {Errors.OutOfOrderCallError} Will throw an error if an expected call is made out of order.
* @throws {Errors.UnexpectedArgumentsError} Will throw an error if an expected call is made with the wrong arguments.
* @throws {Errors.UnexpectedFunctionCallError} Will throw an error if a call is made and there is not matching expected call.
*/
_executeNode(mock, args) {
if(this._executingNode instanceof ExpectedCallNode) {
let expectedCall = this._executingNode.expectedCall;
if(expectedCall.matches(mock, args)) {
this._executingNode = this._executingNode.child;
return expectedCall.execute(args);
}
if(!expectedCall.required) {
this._executingNode = this._executingNode.child;
return this._executeNode(mock, args);
}
this._checkRemainingExpectations(mock, args);
return;
}
if(this._executingNode instanceof AndNode) {
let matchedExpectedCall = this._executingNode.match(mock, args);
if(matchedExpectedCall !== undefined) {
return matchedExpectedCall.execute(args);
}
if(this._executingNode.onlyOptionalRemain()) {
this._executingNode = this._executingNode.child;
return this._executeNode(mock, args);
}
this._checkRemainingExpectations(mock, args);
return;
}
if(this._executingNode instanceof TerminusNode) {
if(!this._ignoreOtherCalls) {
throw new UnexpectedFunctionCallError(mock, args, this._calls);
}
return;
}
}
/**
* Sets execution handlers of all {@link Mock}s in this tree to this trees {@link Tree.Tree#executeNode}.
*/
_setMockExecutionHandler() {
this._calls[0].mock.tree = this;
for(let call of this._calls) {
call.mock.handler = (args) => this._executeNode(call.mock, args);
}
}
/**
* Executes the specifed test code.
* @param {function} thunk Test code.
* @returns {Promise|undefined} Promise if thunk has a callback argument; otherwise undefined.
*/
execute(thunk) {
this._setMockExecutionHandler();
this._executingNode = this._root.child;
let sync = true;
try {
let t = thunk();
if(t instanceof Promise) {
sync = false;
return t
.then((value) => {
this._resetMocks();
this._checkCalls();
return value;
})
.catch((error) => {
this._resetMocks();
throw error;
});
}
}
finally {
if(sync) {
this._resetMocks();
}
}
this._checkCalls();
}
/**
* Converts this tree into a string.
* @returns {string} This tree in string form.
*/
toString() {
return this._root.toString();
}
}
module.exports = Tree;