Source: br-test/src/br/test/GwtTestRunner.js

'use strict';

/**
 * @module br/test/GwtTestRunner
 */

require('jasmine');

var br = require('br/Core');
var Errors = require('br/Errors');
var TestFixture = require('br/test/TestFixture');
var TimeFixture = require('br/test/TimeFixture');
var TimeUtility = require('br/test/TimeUtility');
var FixtureFactory = require('br/test/FixtureFactory');
var FixtureRegistry = require('br/test/FixtureRegistry');

/**
 * @private
 * @class
 * @alias module:br/test/GwtTestRunner
 * @implements module:br/test/FixtureRegistry
 */
function GwtTestRunner(fixtureFactoryClass) {
	var Utility = require('br/core/Utility');

	this.m_pFixtures = [];

	if (typeof fixtureFactoryClass === "undefined") {
		throw new Error("fixtureFactoryClass must be provided and cannot be undefined");
	}

	var fFixtureFactoryClass;
	try {
		if (typeof fixtureFactoryClass === "function") {
			fFixtureFactoryClass = fixtureFactoryClass;
		} else if (fixtureFactoryClass.indexOf("/") > -1) {
			fFixtureFactoryClass = require(fixtureFactoryClass);
		} else {
			try {
				var requirePath = fixtureFactoryClass.replace(/\./g, "/");
				fFixtureFactoryClass = require(requirePath);
			} catch (e) {
				fFixtureFactoryClass = Utility.locate(fixtureFactoryClass);
			}
		}
	} catch (e) {
		throw new Errors.CustomError("InvalidFactoryError", "An error occured in br.test.GwtTestRunner when creating the fixture factory " +
				"(" + fixtureFactoryClass + "): " + e.message);
	}

	if (typeof fFixtureFactoryClass === 'undefined') {
		throw new Errors.CustomError("InvalidFactoryError", "Fixture factory class '" + fixtureFactoryClass + "' does not exist.");
	}

	try {
		this.m_oFixtureFactory = new fFixtureFactoryClass();
	} catch (e) {
		throw new Errors.CustomError("InvalidFactoryError", "An error occured in br.test.GwtTestRunner when creating the fixture factory " +
				"(" + fixtureFactoryClass + "): " + e.message);
	}

	if (!br.fulfills(this.m_oFixtureFactory, FixtureFactory)) {
		throw new Errors.CustomError("InvalidFactoryError", "The provided fixture factory (" + fixtureFactoryClass +
				") does not implement br.test.FixtureFactory");
	}

	this.m_oFixtureFactory.addFixtures(this);
	this.addFixture("test", new TestFixture(this));
	this.addFixture("time", new TimeFixture(TimeUtility));

	this.m_fDoGiven = GwtTestRunner.createTestMethod(this, "doGiven"),
	this.m_fDoWhen = GwtTestRunner.createTestMethod(this, "doWhen"),
	this.m_fDoThen = GwtTestRunner.createTestMethod(this, "doThen"),
	this.m_fDoAnd = GwtTestRunner.createTestMethod(this, "doAnd");
	this.m_fStartingContinuesFrom = GwtTestRunner.createTestMethod(this, "startingContinuesFrom");
	this.m_fFinishedContinuesFrom = GwtTestRunner.createTestMethod(this, "finishedContinuesFrom");
};

br.inherit(GwtTestRunner, FixtureRegistry);

GwtTestRunner.m_mTests = {};
GwtTestRunner.m_mSuites = {};
GwtTestRunner.INIT_PHASE = 1;
GwtTestRunner.GIVEN_PHASE = 2;
GwtTestRunner.WHEN_PHASE = 3;
GwtTestRunner.THEN_PHASE = 4;


// *** Static Methods ***

/**
 * Static method that needs to be called before any Jasmine tests will execute.
 */
GwtTestRunner.initialize = function() {
	if (!window.fixtures) {
		window.fixtures = GwtTestRunner.createTestMethod(GwtTestRunner, "initializeTest");
	}
};

/** @private */
GwtTestRunner.createTestMethod = function(oTestRunner, sMethod) {
	return function(sStatement) {
		oTestRunner[sMethod](sStatement);
	};
};

/** @private */
GwtTestRunner.createProxyDescribeFunction = function(fOrigDescribeFunction, bIsXDescribe) {
	return function(description, closure) {
		var pInvalidChars = ["\\","/",":","*","?","<",">"]
		for (var i = 0; i < pInvalidChars.length; i++) {
			var cInvalidChar = pInvalidChars[i];
			if (description.indexOf(cInvalidChar) > -1) {
				throw new Errors.CustomError("InvalidSuiteError", "Invalid character '" + cInvalidChar + "' in test suite '"+ description + "'.");
			}
		}

		if (GwtTestRunner.m_mSuites[description]) {
			throw new Errors.CustomError("InvalidSuiteError", "The test suite '" + description + "' has already been defined.");
		} else {
			GwtTestRunner.m_mSuites[description] = closure;
			var jasmineDescribeReturnValue = fOrigDescribeFunction.call(this, description, closure);

			if (bIsXDescribe) {
				var fOrigIt = it;
				var fOrigFixtures = fixtures;
				var fOrigGetEnv = jasmine.getEnv;

				try {
					it = GwtTestRunner.capturingItFunction;
					fixtures = function() {};
					jasmine.getEnv = function()
					{
						return {
							currentSuite: {
								getFullName: function() {
									return description;
								}
							}
						};
					};

					closure();
				} finally {
					it = fOrigIt;
					fixtures = fOrigFixtures;
					jasmine.getEnv = fOrigGetEnv;
				}
			}

			return jasmineDescribeReturnValue;
		}
	};
};

/** @private */
GwtTestRunner.createProxyItFunction = function(fOrigItFunction) {
	return function(description, closure) {
		var sSuiteFullName = jasmine.getEnv().currentSuite.getFullName();
		var sSuiteNamespacedTestName = sSuiteFullName + "::" + description;

		if (GwtTestRunner.m_mTests[sSuiteNamespacedTestName] && !description.match("encountered a declaration exception")) {
			throw new Errors.CustomError("DuplicateTestError", "The test '" + sSuiteNamespacedTestName + "' has already been defined.");
		} else {
			closure.suiteName = sSuiteFullName;
			GwtTestRunner.m_mTests[sSuiteNamespacedTestName] = closure;
			var jasmineItReturnValue = fOrigItFunction.call(this, description, closure);

			return jasmineItReturnValue;
		}
	};
};


// *** FixtureRegistry Interface ***

GwtTestRunner.prototype.addFixture = function(sScope, oFixture) {
	var SubFixtureRegistry = require('br/test/SubFixtureRegistry');

	this.m_pFixtures.push({scopeMatcher:new RegExp("^" + sScope + "(\\..+|$)"), scopeLength:sScope.length + 1, fixture:oFixture});
	oFixture.addSubFixtures(new SubFixtureRegistry(this, sScope));
};

// *** Public Methods ***

/** @private */
GwtTestRunner.initializeTest = function(fixtureFactoryClass) {
	var oTestRunner = new GwtTestRunner(fixtureFactoryClass);

	beforeEach(this.createTestMethod(oTestRunner, "startTest"));
	afterEach(this.createTestMethod(oTestRunner, "endTest"));
};

/** @private */
GwtTestRunner.prototype.startTest = function() {
	var ServiceRegistry = require("br/ServiceRegistry");
	if(ServiceRegistry.clear) {
		ServiceRegistry.clear();
	}

	window.given = this.m_fDoGiven;
	window.when = this.m_fDoWhen;
	window.then = this.m_fDoThen;
	window.and = this.m_fDoAnd;
	window.startingContinuesFrom = this.m_fStartingContinuesFrom;
	window.finishedContinuesFrom = this.m_fFinishedContinuesFrom;

	this.m_bTestFailed = false;
	this.m_nTestPhase = GwtTestRunner.INIT_PHASE;

	if (this.m_oFixtureFactory.setUp) {
		try {
			this.m_oFixtureFactory.setUp();
		} catch (e) {
			throw new Errors.CustomError("TestSetUpError", e.message,
					"Error occured in GwtTestRunner.prototype.startTest() calling this.m_oFixtureFactory.setUp()");
		}
	}

	for(var i = 0, l = this.m_pFixtures.length; i < l; ++i) {
		var oFixture = this.m_pFixtures[i].fixture;
		try {
			oFixture.setUp();
		}
		catch (e) {
			throw new Errors.CustomError("TestSetUpError", e.message,
					"Error occured in GwtTestRunner.prototype.startTest() calling oFixture.setUp()");
		}
	}
};

/** @private */
GwtTestRunner.prototype.endTest = function() {
	for(var i = 0, l = this.m_pFixtures.length; i < l; ++i) {
		var oFixture = this.m_pFixtures[i].fixture;
		try {
			oFixture.tearDown();
		}
		catch (e) {
			throw new Errors.CustomError("TestTearDownError", e.message,
					"Error occured in GwtTestRunner.prototype.endTest() calling oFixture.tearDown()");
		}

	}

	if (document.body.hasChildNodes()) {
		for (var i = 0, j = document.body.childNodes.length; i < j; i++) {
			document.body.removeChild(document.body.childNodes[0]);
		}
	}

	if (!this.m_bTestFailed && ((this.m_nTestPhase == GwtTestRunner.GIVEN_PHASE) ||
		(this.m_nTestPhase == GwtTestRunner.WHEN_PHASE))) {
		throw new Errors.CustomError("UnterminatedTestError", "Tests must finish with one or more 'THEN' statements");
	}
};

/** @private */
GwtTestRunner.prototype.startingContinuesFrom = function(description) {
	this.m_nTestPhase = GwtTestRunner.INIT_PHASE;

	var sSuiteNamespacedTestName;
	if (description.match(/::/)) {
		sSuiteNamespacedTestName = description;
	} else {
		var sSuiteFullName = (this.currentSuiteName) ? this.currentSuiteName : jasmine.getEnv().currentSpec.suite.getFullName();
		sSuiteNamespacedTestName = sSuiteFullName + "::" + description;
	}

	var fTest = GwtTestRunner.m_mTests[sSuiteNamespacedTestName];

	if (!fTest) {
		throw new Errors.InvalidTestError("attempt to continue from a test that doesn't exist: '" + sSuiteNamespacedTestName + "'");
	}

	this.currentSuiteName = fTest.suiteName;
	fTest();
	this.currentSuiteName = null;
};

/** @private */
GwtTestRunner.prototype.finishedContinuesFrom = function() {
	this.m_nTestPhase = GwtTestRunner.GIVEN_PHASE;
};

/** @private */
GwtTestRunner.prototype.doGiven = function(sStatement) {
	try {
		TimeUtility.captureTimerFunctions();

		var oStatement = this._parseStatement(sStatement, GwtTestRunner.GIVEN_PHASE);
		oStatement.fixture.doGiven(oStatement.propertyName, oStatement.propertyValue);

		TimeUtility.nextStep();
	} catch (e) {
		this._handleError(e);
	} finally {
		TimeUtility.releaseTimerFunctions();
	}
};

/** @private */
GwtTestRunner.prototype.doWhen = function(sStatement) {
	try {
		TimeUtility.captureTimerFunctions();

		var oStatement = this._parseStatement(sStatement, GwtTestRunner.WHEN_PHASE);
		oStatement.fixture.doWhen(oStatement.propertyName, oStatement.propertyValue);

		TimeUtility.nextStep();
	} catch (e) {
		this._handleError(e);
	} finally {
		TimeUtility.releaseTimerFunctions();
	}
};

/** @private */
GwtTestRunner.prototype.doThen = function(sStatement) {

	try {
		TimeUtility.captureTimerFunctions();

		var oStatement = this._parseStatement(sStatement, GwtTestRunner.THEN_PHASE);
		oStatement.fixture.doThen(oStatement.propertyName, oStatement.propertyValue);

		TimeUtility.nextStep();
	} catch (e) {
		this._handleError(e);
	} finally {
		TimeUtility.releaseTimerFunctions();
	}
};

/** @private */
GwtTestRunner.prototype.doAnd = function(sStatement, oMessage) {
	switch (this.m_nTestPhase) {
		case GwtTestRunner.GIVEN_PHASE:
			this.doGiven(sStatement);
			break;

		case GwtTestRunner.WHEN_PHASE:
			this.doWhen(sStatement);
			break;

		case GwtTestRunner.THEN_PHASE:
			this.doThen(sStatement, oMessage);
			break;

		default:
			this._throwError("InvalidPhaseError", sStatement, "'AND' statements can not occur until a 'GIVEN', 'WHEN' or 'THEN' statement has been made.");
	}
};

/** @private */
GwtTestRunner.prototype._handleError = function(e) {
	this.m_bTestFailed = true;

	if (e.getMessage) {
		fail(e.getMessage());
	} else {
		throw(e);
	}
};

/** @private */
GwtTestRunner.prototype._updatePhase = function(nPhase, sStatement) {
	if (nPhase == GwtTestRunner.GIVEN_PHASE) {
		if (this.m_nTestPhase == GwtTestRunner.INIT_PHASE) {
			this.m_nTestPhase = GwtTestRunner.GIVEN_PHASE;
		} else if (this.m_nTestPhase != GwtTestRunner.GIVEN_PHASE) {
			this._throwError("InvalidPhaseError", sStatement, "'GIVEN' statements must occur before 'WHEN' and 'THEN' statements.");
		}
	} else if (nPhase == GwtTestRunner.WHEN_PHASE) {
		if (this.m_nTestPhase == GwtTestRunner.GIVEN_PHASE) {
			this.m_nTestPhase = GwtTestRunner.WHEN_PHASE;
		} else if (this.m_nTestPhase != GwtTestRunner.WHEN_PHASE) {
			this._throwError("InvalidPhaseError", sStatement, "'WHEN' statements must occur after 'GIVEN' statements, but before 'THEN' statements.");
		}
	} else if (nPhase == GwtTestRunner.THEN_PHASE) {
		if ((this.m_nTestPhase == GwtTestRunner.GIVEN_PHASE) ||
			(this.m_nTestPhase == GwtTestRunner.WHEN_PHASE)) {
			this.m_nTestPhase = GwtTestRunner.THEN_PHASE;
		} else if (this.m_nTestPhase != GwtTestRunner.THEN_PHASE) {
			this._throwError("InvalidPhaseError", sStatement, "'THEN' statements must occur after 'GIVEN' and 'WHEN' statements.");
		}
	}
};

/** @private */
GwtTestRunner.prototype._parseStatement = function(sStatement, nPhase) {

	var newlinePlaceholder = "<!--space--!>";

	sStatement = sStatement.replace(new RegExp("\n", "g"), newlinePlaceholder);

	this._updatePhase(nPhase, sStatement);

	/**
	 * Parses Statements in the format <fixtureName>.<propertyName> <operator> <propertyValue>
	 * uses '[\x21-\x7E]' rather than '.' to match any character so that newlines can be included too
	 */
	var pStatement = /(.+) (\=\>|\=) (.+)/i.exec(sStatement);

	for (var i = 0; i < pStatement.length; i++) {
		pStatement[i] = (pStatement[i].trim());
	}

	if (!pStatement || (pStatement.length != 4) || !pStatement[1] || !pStatement[2] || !pStatement[3]) {
		this._throwError("IllegalStatementError", sStatement, "Statement should have the form <fixtureName>.<propertyName> <operator> <propertyValue>");
	}

	var oStatement = {
		property:(pStatement[1].trim()),
		operator:pStatement[2],
		propertyValue:this._getTypedPropertyValue(pStatement[3].replace(new RegExp(newlinePlaceholder, "g"), "\n"))
	};

	if (nPhase === GwtTestRunner.WHEN_PHASE && oStatement.operator != "=>") {
		this._throwError("IllegalStatementError", sStatement, "'When Statements should use => as an Operator");
	}

	this._addFixtureToStatement(oStatement);
	if (!oStatement.fixture) {
		this._throwError("InvalidFixtureNameError", sStatement, "No Fixture has been specified matching '" + oStatement.propertyName + "'");
	}

	return oStatement;
};

/** @private */
GwtTestRunner.prototype._addFixtureToStatement = function(oStatement) {
	for(var i = 0, l = this.m_pFixtures.length; i < l; ++i) {
		var oNextFixture = this.m_pFixtures[i];

		if (oStatement.property.match(oNextFixture.scopeMatcher)) {
			var sFixtureProperty = oStatement.property.substr(oNextFixture.scopeLength);
			var bCanHandleProperty = (sFixtureProperty.length > 0) ? oNextFixture.fixture.canHandleProperty(sFixtureProperty) :
				oNextFixture.fixture.canHandleExactMatch();

			if (bCanHandleProperty) {
				oStatement.fixture = oNextFixture.fixture;
				oStatement.propertyName = sFixtureProperty;
				break;
			}
		}
	}
};

/** @private */
GwtTestRunner.prototype._getTypedPropertyValue = function(sValue) {
	var vValue = null;

	if (sValue == "true") {
		vValue = true;
	} else if (sValue == "false") {
		vValue = false;
	} else if (sValue == "undefined") {
		vValue = undefined;
	} else if (sValue.match(/^'[.\s\S]*'$/)) {
		vValue = sValue.substr(1, sValue.length - 2);
	} else if (!isNaN(sValue)) {
		vValue = Number(sValue);
	} else if (sValue.match(/^\[.*\]$/)) {
		var pItems = sValue.substr(1, sValue.length - 2).split(/ *, */);

		vValue = [];
		for(var i = 0, l = pItems.length; i < l; ++i) {
			vValue[i] = this._getTypedPropertyValue(pItems[i]);
		}
	}

	return vValue;
};

/** @private */
GwtTestRunner.prototype._throwError = function(sType, sStatement, sMessage) {
	throw new Errors.CustomError(sType, "Error handling statement '" + sStatement + "':\n\t" + sMessage);
};

// JASMINE OVERRIDES.
if (window.jasmine) {
	describe = GwtTestRunner.createProxyDescribeFunction(describe);
	xdescribe = GwtTestRunner.createProxyDescribeFunction(xdescribe, true);

	jasmine.Env.prototype.it = GwtTestRunner.createProxyItFunction(jasmine.Env.prototype.it);
	jasmine.Env.prototype.xit = GwtTestRunner.createProxyItFunction(jasmine.Env.prototype.xit);

	GwtTestRunner.capturingItFunction = GwtTestRunner.createProxyItFunction(function() {});
}

module.exports = GwtTestRunner;