/*! * Casper is a navigation utility for PhantomJS. * * Documentation: http://casperjs.org/ * Repository: http://github.com/casperjs/casperjs * * Copyright (c) 2011-2012 Nicolas Perriault * * Part of source code is Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ require = patchRequire(require); var fs = require('fs'); var events = require('events'); var utils = require('utils'); var f = utils.format; function AssertionError(msg, result) { "use strict"; Error.call(this); this.message = msg; this.name = 'AssertionError'; this.result = result; } AssertionError.prototype = new Error(); exports.AssertionError = AssertionError; function TerminationError(msg) { "use strict"; Error.call(this); this.message = msg; this.name = 'TerminationError'; } TerminationError.prototype = new Error(); exports.TerminationError = TerminationError; function TimedOutError(msg) { "use strict"; Error.call(this); this.message = msg; this.name = 'TimedOutError'; } TimedOutError.prototype = new Error(); exports.TimedOutError = TimedOutError; /** * Creates a tester instance. * * @param Casper casper A Casper instance * @param Object options Tester options * @return Tester */ exports.create = function create(casper, options) { "use strict"; return new Tester(casper, options); }; /** * Casper tester: makes assertions, stores test results and display then. * * @param Casper casper A valid Casper instance * @param Object|null options Options object */ var Tester = function Tester(casper, options) { "use strict"; /*eslint max-statements:0*/ if (!utils.isCasperObject(casper)) { throw new CasperError("Tester needs a Casper instance"); } // self reference var self = this; // casper reference this.casper = casper; // public properties this._setUp = undefined; this._tearDown = undefined; this.aborted = false; this.executed = 0; this.currentTestFile = null; this.currentTestStartTime = new Date(); this.currentSuite = undefined; this.currentSuiteNum = 0; this.lastAssertTime = 0; this.loadIncludes = { includes: [], pre: [], post: [] }; this.options = utils.mergeObjects({ concise: false, // concise output? failFast: false, // terminates a suite as soon as a test fails? failText: "FAIL", // text to use for a failed test passText: "PASS", // text to use for a succesful test skipText: "SKIP", // text to use for a skipped test save: false, // false to not save pad: 80 , // maximum number of chars for a result line warnText: "WARN" // text to use for a dubious test }, options); this.queue = []; this.running = false; this.started = false; this.suiteResults = new TestSuiteResult(); this.on('success', function onSuccess(success) { var timeElapsed = new Date() - this.currentTestStartTime; this.currentSuite.addSuccess(success, timeElapsed - this.lastAssertTime); this.lastAssertTime = timeElapsed; }); this.on('skipped', function onSkipped(skipped) { var timeElapsed = new Date() - this.currentTestStartTime; this.currentSuite.addSkip(skipped, timeElapsed - this.lastAssertTime); this.lastAssertTime = timeElapsed; }); this.on('fail', function onFail(failure) { // export var valueKeys = Object.keys(failure.values), timeElapsed = new Date() - this.currentTestStartTime; this.currentSuite.addFailure(failure, timeElapsed - this.lastAssertTime); this.lastAssertTime = timeElapsed; // special printing if (failure.type) { this.comment(' type: ' + failure.type); } if (failure.file) { this.comment(' file: ' + failure.file + (failure.line ? ':' + failure.line : '')); } if (failure.lineContents) { this.comment(' code: ' + failure.lineContents); } if (!failure.values || valueKeys.length === 0) { return; } valueKeys.forEach(function(name) { this.comment(f(' %s: %s', name, utils.formatTestValue(failure.values[name], name))); }.bind(this)); // check for fast failing if (this.options.failFast) { return this.terminate('--fail-fast: aborted all remaining tests'); } }); function errorHandler(error, backtrace) { self.casper.unwait(); if (error instanceof Error) { self.processError(error); return; } if (utils.isString(error) && /^(Assertion|Termination|TimedOut)Error/.test(error)) { return; } var line = 0; try { line = (backtrace || []).filter(function(entry) { return self.currentTestFile === entry.file; })[0].line; } catch (e) {} self.uncaughtError(error, self.currentTestFile, line, backtrace); } function errorHandlerAndDone(error, backtrace) { errorHandler(error, backtrace); self.done(); } // casper events this.casper.on('error', function onCasperError(msg, backtrace) { self.processPhantomError(msg, backtrace); }); [ 'wait.error', 'waitFor.timeout.error', 'event.error', 'complete.error' ].forEach(function(event) { self.casper.on(event, errorHandlerAndDone); }); self.casper.on('step.error', errorHandler); this.casper.on('warn', function(warning) { if (self.currentSuite) { self.currentSuite.addWarning(warning); } }); // Do not hook casper if we're not testing if (!phantom.casperTest) { return; } // specific timeout callbacks this.casper.options.onStepTimeout = function test_onStepTimeout(timeout, step) { throw new TimedOutError(f("Step timeout occured at step %s (%dms)", step, timeout)); }; this.casper.options.onTimeout = function test_onTimeout(timeout) { throw new TimedOutError(f("Timeout occured (%dms)", timeout)); }; this.casper.options.onWaitTimeout = function test_onWaitTimeout(timeout, details) { /*eslint complexity:0*/ var message = f("Wait timeout occured (%dms)", timeout); details = details || {}; if (details.selector) { message = f(details.waitWhile ? '"%s" never went away in %dms' : '"%s" still did not exist in %dms', details.selector, timeout); } else if (details.visible) { message = f(details.waitWhile ? '"%s" never disappeared in %dms' : '"%s" never appeared in %dms', details.visible, timeout); } else if (details.url || details.resource) { message = f('%s did not load in %dms', details.url || details.resource, timeout); } else if (details.popup) { message = f('%s did not pop up in %dms', details.popup, timeout); } else if (details.text) { message = f('"%s" did not appear in the page in %dms', details.text, timeout); } else if (details.selectorTextChange) { message = f('"%s" did not have a text change in %dms', details.selectorTextChange, timeout); } else if (utils.isFunction(details.testFx)) { message = f('"%s" did not evaluate to something truthy in %dms', details.testFx.toString(), timeout); } errorHandlerAndDone(new TimedOutError(message)); }; }; // Tester class is an EventEmitter utils.inherits(Tester, events.EventEmitter); exports.Tester = Tester; /** * Aborts current test suite. * * @param String message Warning message (optional) */ Tester.prototype.abort = function abort(message) { "use strict"; throw new TerminationError(message || 'test suite aborted'); }; /** * Skip `nb` tests. * * @param Integer nb Number of tests to skip * @param String message Message to display * @return Object */ Tester.prototype.skip = function skip(nb, message) { "use strict"; return this.processAssertionResult({ success: null, standard: f("%d test%s skipped", nb, nb > 1 ? "s" : ""), message: message, type: "skip", number: nb, skipped: true }); }; /** * Skip `nb` test on specific engine(s). * * A skip specifier is an object of the form: * { * name: 'casperjs' | 'phantomjs', * version: { * min: Object, * max: Object * }, * message: String * } * * Minimal and maximal versions to be skipped are determined using * utils.matchEngine. * * @param Integer nb Number of tests to skip * @param Mixed skipSpec a single skip specifier object or * an Array of skip specifier objects * @return Object */ Tester.prototype.skipIfEngine = function skipIfEngine(nb, skipSpec) { skipSpec = utils.matchEngine(skipSpec); if (skipSpec) { var message = skipSpec.name; var version = skipSpec.version; var skipMessage = skipSpec.message; if (version) { var min = version.min; var max = version.max; if (min && min === max) { message += ' ' + min; } else { if (min) { message += ' from ' + min; } if (max) { message += ' to ' + max; } } } if (skipMessage) { message += ' ' + skipMessage; } return this.skip(nb, message); } return false; }; /** * Asserts that a condition strictly resolves to true. Also returns an * "assertion object" containing useful informations about the test case * results. * * This method is also used as the base one used for all other `assert*` * family methods; supplementary informations are then passed using the * `context` argument. * * Note: an AssertionError is thrown if the assertion fails. * * @param Boolean subject The condition to test * @param String message Test description * @param Object|null context Assertion context object (Optional) * @return Object An assertion result object if test passed * @throws AssertionError in case the test failed */ Tester.prototype.assert = Tester.prototype.assertTrue = function assert(subject, message, context) { "use strict"; this.executed++; var result = utils.mergeObjects({ success: subject === true, type: "assert", standard: "Subject is strictly true", message: message, file: this.currentTestFile, doThrow: true, values: { subject: utils.getPropertyPath(context, 'values.subject') || subject } }, context || {}); if (!result.success && result.doThrow) { throw new AssertionError(message || result.standard, result); } return this.processAssertionResult(result); }; /** * Asserts that two values are strictly equals. * * @param Mixed subject The value to test * @param Mixed expected The expected value * @param String message Test description (Optional) * @return Object An assertion result object */ Tester.prototype.assertEquals = Tester.prototype.assertEqual = function assertEquals(subject, expected, message) { "use strict"; return this.assert(utils.equals(subject, expected), message, { type: "assertEquals", standard: "Subject equals the expected value", values: { subject: subject, expected: expected } }); }; /** * Asserts that two values are strictly not equals. * * @param Mixed subject The value to test * @param Mixed expected The unwanted value * @param String|null message Test description (Optional) * @return Object An assertion result object */ Tester.prototype.assertNotEquals = function assertNotEquals(subject, shouldnt, message) { "use strict"; return this.assert(!this.testEquals(subject, shouldnt), message, { type: "assertNotEquals", standard: "Subject doesn't equal what it shouldn't be", values: { subject: subject, shouldnt: shouldnt } }); }; /** * Asserts that a selector expression matches n elements. * * @param Mixed selector A selector expression * @param Number count Expected number of matching elements * @param String message Test description (Optional) * @return Object An assertion result object */ Tester.prototype.assertElementCount = function assertElementCount(selector, count, message) { "use strict"; if (!utils.isNumber(count) || count < 0) { throw new CasperError('assertElementCount() needs a positive integer count'); } var elementCount = this.casper.evaluate(function(selector) { try { return __utils__.findAll(selector).length; } catch (e) { return -1; } }, selector); return this.assert(elementCount === count, message, { type: "assertElementCount", standard: f('%d element%s matching selector "%s" found', count, count > 1 ? 's' : '', selector), values: { selector: selector, expected: count, obtained: elementCount } }); }; /** * Asserts that a code evaluation in remote DOM resolves to true. * * @param Function fn A function to be evaluated in remote DOM * @param String message Test description * @param Object params Object/Array containing the parameters to inject into * the function (optional) * @return Object An assertion result object */ Tester.prototype.assertEval = Tester.prototype.assertEvaluate = function assertEval(fn, message, params) { "use strict"; return this.assert(this.casper.evaluate(fn, params), message, { type: "assertEval", standard: "Evaluated function returns true", values: { fn: fn, params: params } }); }; /** * Asserts that the result of a code evaluation in remote DOM equals * an expected value. * * @param Function fn The function to be evaluated in remote DOM * @param Boolean expected The expected value * @param String|null message Test description * @param Object|null params Object containing the parameters to inject into the * function (optional) * @return Object An assertion result object */ Tester.prototype.assertEvalEquals = Tester.prototype.assertEvalEqual = function assertEvalEquals(fn, expected, message, params) { "use strict"; var subject = this.casper.evaluate(fn, params); return this.assert(utils.equals(subject, expected), message, { type: "assertEvalEquals", standard: "Evaluated function returns the expected value", values: { fn: fn, params: params, subject: subject, expected: expected } }); }; function baseFieldAssert(inputName, expected, actual, message) { "use strict"; return this.assert(utils.equals(actual, expected), message, { type: 'assertField', standard: f('"%s" input field has the value "%s"', inputName, expected), values: { inputName: inputName, actual: actual, expected: expected } }); } /** * Asserts that the provided assertion fails (used for internal testing). * * @param Function fn A closure calling an assertion * @param String|null message Test description * @return Object An assertion result object */ Tester.prototype.assertFail = function assertFail(fn, message) { "use strict"; var failed = false; try { fn(); } catch (e) { failed = true; } return this.assert(failed, message, { type: "assertFail", standard: "Assertion fails as expected" }); }; /** * Asserts that a given input field has the provided value. * * @param String|Object input The name attribute of the input element * or an object with the selector * @param String expected The expected value of the input element * @param String message Test description * @param Object options ClientUtils#getFieldValue options (optional) * @return Object An assertion result object */ Tester.prototype.assertField = function assertField(input, expected, message, options) { "use strict"; if (typeof input === 'object') { switch (input.type) { case 'css': return this.assertFieldCSS(input.path, expected, message); case 'xpath': return this.assertFieldXPath(input.path, expected, message); default: throw new CasperError('Invalid regexp.'); // no default } } var actual = this.casper.evaluate(function(inputName) { return __utils__.getFieldValue(__utils__.makeSelector(inputName,'name')); }, input); return baseFieldAssert.call(this, input, expected, actual, message); }; /** * Asserts that a given input field by CSS selector has the provided value. * * @param Object cssSelector The CSS selector to use for the assert field value * @param String expected The expected value of the input element * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertFieldCSS = function assertFieldCSS(cssSelector, expected, message) { "use strict"; var actual = this.casper.evaluate(function(inputName) { return __utils__.getFieldValue(__utils__.makeSelector(inputName,'css')); }, cssSelector); return baseFieldAssert.call(this, null, expected, actual, message); }; /** * Asserts that a given input field by XPath selector has the provided value. * * @param Object xPathSelector The XPath selector to use for the assert field value * @param String expected The expected value of the input element * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertFieldXPath = function assertFieldXPath(xPathSelector, expected, message) { "use strict"; var actual = this.casper.evaluate(function(inputName) { return __utils__.getFieldValue(__utils__.makeSelector(inputName,'xpath')); }, xPathSelector); return baseFieldAssert.call(this, null, expected, actual, message); }; /** * Asserts that an element matching the provided selector expression exists in * remote DOM. * * @param String selector Selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertExists = Tester.prototype.assertExist = Tester.prototype.assertSelectorExists = Tester.prototype.assertSelectorExist = function assertExists(selector, message) { "use strict"; return this.assert(this.casper.exists(selector), message, { type: "assertExists", standard: f("Find an element matching: %s", selector), values: { selector: selector } }); }; /** * Asserts that an element matching the provided selector expression does not * exist in remote DOM. * * @param String selector Selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertDoesntExist = Tester.prototype.assertNotExists = function assertDoesntExist(selector, message) { "use strict"; return this.assert(!this.casper.exists(selector), message, { type: "assertDoesntExist", standard: f("Fail to find element matching selector: %s", selector), values: { selector: selector } }); }; /** * Asserts that current HTTP status is the one passed as argument. * * @param Number status HTTP status code * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertHttpStatus = function assertHttpStatus(status, message) { "use strict"; var currentHTTPStatus = this.casper.currentHTTPStatus; return this.assert(utils.equals(this.casper.currentHTTPStatus, status), message, { type: "assertHttpStatus", standard: f("HTTP status code is: %s", status), values: { current: currentHTTPStatus, expected: status } }); }; /** * Asserts that a provided string matches a provided RegExp pattern. * * @param String subject The string to test * @param RegExp pattern A RegExp object instance * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertMatch = Tester.prototype.assertMatches = function assertMatch(subject, pattern, message) { "use strict"; if (utils.betterTypeOf(pattern) !== "regexp") { throw new CasperError('Invalid regexp.'); } return this.assert(pattern.test(subject), message, { type: "assertMatch", standard: "Subject matches the provided pattern", values: { subject: subject, pattern: pattern.toString() } }); }; /** * Asserts a condition resolves to false. * * @param Boolean condition The condition to test * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertNot = Tester.prototype.assertFalse = function assertNot(condition, message) { "use strict"; return this.assert(!condition, message, { type: "assertNot", standard: "Subject is falsy", values: { condition: condition } }); }; /** * Asserts that a selector expression is not currently visible. * * @param String expected selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertNotVisible = Tester.prototype.assertInvisible = function assertNotVisible(selector, message) { "use strict"; return this.assert(!this.casper.visible(selector), message, { type: "assertNotVisible", standard: "Selector is not visible", values: { selector: selector } }); }; /** * Asserts that the provided function called with the given parameters * will raise an exception. * * @param Function fn The function to test * @param Array args The arguments to pass to the function * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertRaises = Tester.prototype.assertRaise = Tester.prototype.assertThrows = function assertRaises(fn, args, message) { "use strict"; var error, thrown = false, context = { type: "assertRaises", standard: "Function raises an error" }; try { fn.apply(null, args); } catch (err) { thrown = true; error = err; } this.assert(thrown, message, utils.mergeObjects(context, { values: { error: error } })); }; /** * Asserts that the current page has a resource that matches the provided test * * @param Function/String test A test function that is called with every response * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertResourceExists = Tester.prototype.assertResourceExist = function assertResourceExists(test, message) { "use strict"; return this.assert(this.casper.resourceExists(test), message, { type: "assertResourceExists", standard: "Confirm page has resource", values: { test: test } }); }; /** * Asserts that given text doesn't exist in the document body. * * @param String text Text not to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTextDoesntExist = Tester.prototype.assertTextDoesntExist = function assertTextDoesntExist(text, message) { "use strict"; var textFound = (this.casper.evaluate(function _evaluate() { return document.body.textContent || document.body.innerText; }).indexOf(text) === -1); return this.assert(textFound, message, { type: "assertTextDoesntExists", standard: "Text doesn't exist within the document body", values: { text: text } }); }; /** * Asserts that given text exists in the document body. * * @param String text Text to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTextExists = Tester.prototype.assertTextExist = function assertTextExists(text, message) { "use strict"; var textFound = (this.casper.evaluate(function _evaluate() { return document.body.textContent || document.body.innerText; }).indexOf(text) !== -1); return this.assert(textFound, message, { type: "assertTextExists", standard: "Find text within the document body", values: { text: text } }); }; /** * Asserts a subject is truthy. * * @param Mixed subject Test subject * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTruthy = function assertTruthy(subject, message) { "use strict"; /*eslint eqeqeq:0*/ return this.assert(utils.isTruthy(subject), message, { type: "assertTruthy", standard: "Subject is truthy", values: { subject: subject } }); }; /** * Asserts a subject is falsy. * * @param Mixed subject Test subject * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertFalsy = function assertFalsy(subject, message) { "use strict"; /*eslint eqeqeq:0*/ return this.assert(utils.isFalsy(subject), message, { type: "assertFalsy", standard: "Subject is falsy", values: { subject: subject } }); }; /** * Asserts that given text exists in the provided selector. * * @param String selector Selector expression * @param String text Text to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertSelectorHasText = Tester.prototype.assertSelectorContains = function assertSelectorHasText(selector, text, message) { "use strict"; var got = this.casper.fetchText(selector); var textFound = got.indexOf(text) !== -1; return this.assert(textFound, message, { type: "assertSelectorHasText", standard: f('Find "%s" within the selector "%s"', text, selector), values: { selector: selector, text: text, actualContent: got } }); }; /** * Asserts that given text does not exist in the provided selector. * * @param String selector Selector expression * @param String text Text not to be found * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertSelectorDoesntHaveText = Tester.prototype.assertSelectorDoesntContain = function assertSelectorDoesntHaveText(selector, text, message) { "use strict"; var textFound = this.casper.fetchText(selector).indexOf(text) === -1; return this.assert(textFound, message, { type: "assertSelectorDoesntHaveText", standard: f('Did not find "%s" within the selector "%s"', text, selector), values: { selector: selector, text: text } }); }; /** * Asserts that title of the remote page equals to the expected one. * * @param String expected The expected title string * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTitle = function assertTitle(expected, message) { "use strict"; var currentTitle = this.casper.getTitle(); return this.assert(utils.equals(currentTitle, expected), message, { type: "assertTitle", standard: f('Page title is: "%s"', expected), values: { subject: currentTitle, expected: expected } }); }; /** * Asserts that title of the remote page matched the provided pattern. * * @param RegExp pattern The pattern to test the title against * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertTitleMatch = Tester.prototype.assertTitleMatches = function assertTitleMatch(pattern, message) { "use strict"; if (utils.betterTypeOf(pattern) !== "regexp") { throw new CasperError('Invalid regexp.'); } var currentTitle = this.casper.getTitle(); return this.assert(pattern.test(currentTitle), message, { type: "assertTitle", details: "Page title does not match the provided pattern", values: { subject: currentTitle, pattern: pattern.toString() } }); }; /** * Asserts that the provided subject is of the given type. * * @param mixed subject The value to test * @param String type The javascript type name * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertType = function assertType(subject, type, message) { "use strict"; var actual = utils.betterTypeOf(subject); return this.assert(utils.equals(actual, type), message, { type: "assertType", standard: f('Subject type is: "%s"', type), values: { subject: subject, type: type, actual: actual } }); }; /** * Asserts that the provided subject has the provided constructor in its prototype hierarchy. * * @param mixed subject The value to test * @param Function constructor The javascript type name * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertInstanceOf = function assertInstanceOf(subject, constructor, message) { "use strict"; if (utils.betterTypeOf(constructor) !== "function") { throw new CasperError('Subject is null or undefined.'); } return this.assert(utils.betterInstanceOf(subject, constructor), message, { type: "assertInstanceOf", standard: f('Subject is instance of: "%s"', constructor.name), values: { subject: subject, constructorName: constructor.name } }); }; /** * Asserts that a the current page url matches a given pattern. A pattern may be * either a RegExp object or a String. The method will test if the URL matches * the pattern or contains the String. * * @param RegExp|String pattern The test pattern * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertUrlMatch = Tester.prototype.assertUrlMatches = function assertUrlMatch(pattern, message) { "use strict"; var currentUrl = this.casper.getCurrentUrl(), patternType = utils.betterTypeOf(pattern), result; if (patternType === "regexp") { result = pattern.test(currentUrl); } else if (patternType === "string") { result = currentUrl.indexOf(pattern) !== -1; } else { throw new CasperError("assertUrlMatch() only accepts strings or regexps"); } return this.assert(result, message, { type: "assertUrlMatch", standard: "Current url matches the provided pattern", values: { currentUrl: currentUrl, pattern: pattern.toString() } }); }; /** * Asserts that a selector expression is currently visible. * * @param String expected selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertVisible = function assertVisible(selector, message) { "use strict"; return this.assert(this.casper.visible(selector), message, { type: "assertVisible", standard: "Selector is visible", values: { selector: selector } }); }; /** * Asserts that all elements matching selector expression are currently visible. * Fails if even one element is not visible. * * @param String expected selector expression * @param String message Test description * @return Object An assertion result object */ Tester.prototype.assertAllVisible = function assertAllVisible(selector, message) { "use strict"; return this.assert(this.casper.allVisible(selector), message, { type: "assertAllVisible", standard: "All elements matching selector are visible", values: { selector: selector } }); }; /** * Prints out a colored bar onto the console. * */ Tester.prototype.bar = function bar(text, style) { "use strict"; this.casper.echo(text, style, this.options.pad); }; /** * Defines a function which will be executed before every test. * * @param Function fn */ Tester.prototype.setUp = function setUp(fn) { "use strict"; this._setUp = fn; }; /** * Defines a function which will be executed after every test. * * @param Function fn */ Tester.prototype.tearDown = function tearDown(fn) { "use strict"; this._tearDown = fn; }; /** * Starts a suite. * * Can be invoked different ways: * * casper.test.begin("suite description", plannedTests, function(test){}) * casper.test.begin("suite description", function(test){}) */ Tester.prototype.begin = function begin() { "use strict"; if (this.started && this.running) return this.queue.push(arguments); function getConfig(args) { var config = { setUp: function(){}, tearDown: function(){} }; if (utils.isFunction(args[1])) { config.test = args[1]; } else if (utils.isObject(args[1])) { config = utils.mergeObjects(config, args[1]); } else if (utils.isNumber(args[1]) && utils.isFunction(args[2])) { config.planned = ~~args[1] || undefined; config.test = args[2]; } else if (utils.isNumber(args[1]) && utils.isObject(args[2])) { config.config = utils.mergeObjects(config, args[2]); config.planned = ~~args[1] || undefined; } else { throw new CasperError('Invalid call'); } if (!utils.isFunction(config.test)) throw new CasperError('begin() is missing a mandatory test function'); return config; } var description = arguments[0] || f("Untitled suite in %s", this.currentTestFile), config = getConfig([].slice.call(arguments)), next = function() { config.test(this, this.casper); }.bind(this); if (!this.options.concise) this.comment(description); this.currentSuite = new TestCaseResult({ name: description, file: this.currentTestFile, config: config, planned: config.planned || undefined }); this.executed = 0; this.running = this.started = true; try { if (config.setUp) config.setUp(this, this.casper); if (!this._setUp) return next(); if (this._setUp.length > 0) return this._setUp.call(this, next); // async this._setUp.call(this); // sync next(); } catch (err) { this.processError(err); this.done(); } }; /** * Render a colorized output. Basically a proxy method for * `Casper.Colorizer#colorize()`. * * @param String message * @param String style The style name * @return String */ Tester.prototype.colorize = function colorize(message, style) { "use strict"; return this.casper.getColorizer().colorize(message, style); }; /** * Writes a comment-style formatted message to stdout. * * @param String message */ Tester.prototype.comment = function comment(message) { "use strict"; this.casper.echo('# ' + message, 'COMMENT'); }; /** * Declares the current test suite done. * */ Tester.prototype.done = function done() { "use strict"; /*eslint max-statements:0, complexity:0*/ var planned, config = this.currentSuite && this.currentSuite.config || {}; if (arguments.length && utils.isNumber(arguments[0])) { this.casper.warn('done() `planned` arg is deprecated as of 1.1'); planned = arguments[0]; } if (config && config.tearDown && utils.isFunction(config.tearDown)) { try { config.tearDown(this, this.casper); } catch (error) { this.processError(error); } } var next = function() { if (this.currentSuite && this.currentSuite.planned && this.currentSuite.planned !== this.executed + this.currentSuite.skipped && !this.currentSuite.failed) { this.dubious(this.currentSuite.planned, this.executed, this.currentSuite.name); } else if (planned && planned !== this.executed) { // BC this.dubious(planned, this.executed); } if (this.currentSuite) { this.suiteResults.push(this.currentSuite); if (!this.options.concise) { var message = [ this.colorize('PASS', 'INFO'), this.formatMessage(this.currentSuite.name) ]; if (config.planned) { message.push([ this.colorize(f('(%d test%s)', config.planned, config.planned > 1 ? 's' : ''), 'INFO') ]); } this.casper.echo(message.join(' ')); } this.currentSuite = undefined; this.executed = 0; } this.emit('test.done'); this.casper.currentHTTPResponse = {}; this.running = this.started = false; var nextTest = this.queue.shift(); if (nextTest) { this.begin.apply(this, nextTest); } }.bind(this); if (!this._tearDown) { return next(); } try { if (this._tearDown.length > 0) { // async this._tearDown.call(this, next); } else { // sync this._tearDown.call(this); next(); } } catch (error) { this.processError(error); } }; /** * Marks a test as dubious, when the number of planned tests doesn't match the * number of actually executed one. * * @param String message */ Tester.prototype.dubious = function dubious(planned, executed, suite) { "use strict"; var message = f('%s: %d tests planned, %d tests executed', suite || 'global', planned, executed); this.casper.warn(message); if (!this.currentSuite) return; this.currentSuite.addFailure({ type: "dubious", file: this.currentTestFile, standard: message }); }; /** * Writes an error-style formatted message to stdout. * * @param String message */ Tester.prototype.error = function error(message) { "use strict"; this.casper.echo(message, 'ERROR'); }; /** * Executes a file, wraping and evaluating its code in an isolated * environment where only the current `casper` instance is passed. * * @param String file Absolute path to some js/coffee file */ Tester.prototype.exec = function exec(file) { "use strict"; file = this.filter('exec.file', file) || file; if (!fs.isFile(file) || !utils.isJsFile(file)) { var e = new CasperError(f("Cannot exec %s: can only exec() files with .js or .coffee extensions", file)); e.fileName = e.file = e.sourceURL = file; throw e; } this.currentTestFile = file; phantom.injectJs(file); }; /** * Adds a failed test entry to the stack. * * @param String message * @param Object Failure context (optional) */ Tester.prototype.fail = function fail(message, context) { "use strict"; context = context || {}; return this.assert(false, message, utils.mergeObjects({ type: "fail", standard: "explicit call to fail()" }, context)); }; /** * Recursively finds all test files contained in a given directory. * * @param String dir Path to some directory to scan */ Tester.prototype.findTestFiles = function findTestFiles(dir) { "use strict"; var self = this; if (!fs.isDirectory(dir)) { return []; } var entries = fs.list(dir).filter(function _filter(entry) { return entry !== '.' && entry !== '..'; }).map(function _map(entry) { return fs.absolute(fs.pathJoin(dir, entry)); }); entries.forEach(function _forEach(entry) { if (fs.isDirectory(entry)) { entries = entries.concat(self.findTestFiles(entry)); } }); return entries.filter(function _filter(entry) { return utils.isJsFile(entry); }).sort(); }; /** * Computes current suite identifier. * * @return String */ Tester.prototype.getCurrentSuiteId = function getCurrentSuiteId() { "use strict"; return this.casper.test.currentSuiteNum + "-" + this.casper.step; }; /** * Formats a message to highlight some parts of it. * * @param String message * @param String style */ Tester.prototype.formatMessage = function formatMessage(message, style) { "use strict"; var parts = /^([a-z0-9_\.]+\(\))(.*)/i.exec(message); if (!parts) { return message; } return this.colorize(parts[1], 'PARAMETER') + this.colorize(parts[2], style); }; /** * Writes an info-style formatted message to stdout. * * @param String message */ Tester.prototype.info = function info(message) { "use strict"; this.casper.echo(message, 'PARAMETER'); }; /** * Adds a succesful test entry to the stack. * * @param String message */ Tester.prototype.pass = function pass(message) { "use strict"; return this.assert(true, message, { type: "pass", standard: "explicit call to pass()" }); }; function getStackEntry(error, testFile) { "use strict"; if ("stackArray" in error) { // PhantomJS has changed the API of the Error object :-/ // https://github.com/ariya/phantomjs/commit/c9cf14f221f58a3daf585c47313da6fced0276bc return error.stackArray.filter(function(entry) { return testFile === entry.sourceURL; })[0]; } if (! ('stack' in error)) return null; var r = /\r?\n\s*(.*?)(at |@)([^:]*?):(\d+):?(\d*)/g; var m; while ((m = r.exec(error.stack))) { var sourceURL = m[3]; if (sourceURL.indexOf('->') !== -1) { sourceURL = sourceURL.split('->')[1].trim(); } if (sourceURL === testFile) { return { sourceURL: sourceURL, line: m[4], column: m[5]}; } } return null; } /** * Processes an assertion error. * * @param AssertionError error */ Tester.prototype.processAssertionError = function(error) { "use strict"; var result = error && error.result || {}, testFile = this.currentTestFile, stackEntry; try { stackEntry = getStackEntry(error, testFile); } catch (e) {} if (stackEntry) { result.line = stackEntry.line; try { result.lineContents = fs.read(this.currentTestFile).split('\n')[result.line - 1].trim(); } catch (e) {} } return this.processAssertionResult(result); }; /** * Processes an assertion result by emitting the appropriate event and * printing result onto the console. * * @param Object result An assertion result object * @return Object The passed assertion result Object */ Tester.prototype.processAssertionResult = function processAssertionResult(result) { "use strict"; if (!this.currentSuite) { // this is for BC when begin() didn't exist this.currentSuite = new TestCaseResult({ name: "Untitled suite in " + this.currentTestFile, file: this.currentTestFile, planned: undefined }); } var eventName = 'success', message = result.message || result.standard, style = 'INFO', status = this.options.passText; if (null === result.success) { eventName = 'skipped'; style = 'SKIP'; status = this.options.skipText; } else if (!result.success) { eventName = 'fail'; style = 'RED_BAR'; status = this.options.failText; } if (!this.options.concise) { this.casper.echo([this.colorize(status, style), this.formatMessage(message)].join(' ')); } this.emit(eventName, result); return result; }; /** * Processes an error. * * @param Error error */ Tester.prototype.processError = function processError(error) { "use strict"; if (error instanceof AssertionError) { return this.processAssertionError(error); } if (error instanceof TerminationError) { return this.terminate(error.message); } return this.uncaughtError(error, this.currentTestFile, error.line); }; /** * Processes a PhantomJS error, which is an error message and a backtrace. * * @param String message * @param Array backtrace */ Tester.prototype.processPhantomError = function processPhantomError(msg, backtrace) { "use strict"; if (/^AssertionError/.test(msg)) { this.casper.warn('looks like you did not use begin(), which is mandatory since 1.1'); } var termination = /^TerminationError:?\s?(.*)/.exec(msg); if (termination) { var message = termination[1]; if (backtrace && backtrace[0]) { message += ' at ' + backtrace[0].file + backtrace[0].line; } return this.terminate(message); } this.fail(msg, { type: "error", doThrow: false, values: { error: msg, stack: backtrace } }); this.done(); }; /** * Renders a detailed report for each failed test. * */ Tester.prototype.renderFailureDetails = function renderFailureDetails() { "use strict"; if (!this.suiteResults.isFailed()) { return; } var failures = this.suiteResults.getAllFailures(); this.casper.echo(f("\nDetails for the %d failed test%s:\n", failures.length, failures.length > 1 ? "s" : ""), "PARAMETER"); failures.forEach(function _forEach(failure) { this.casper.echo(f('In %s%s', failure.file, ~~failure.line ? ':' + ~~failure.line : '')); if (failure.suite) { this.casper.echo(f(' %s', failure.suite), "PARAMETER"); } this.casper.echo(f(' %s: %s', failure.type || "unknown", failure.message || failure.standard || "(no message was entered)"), "COMMENT"); }.bind(this)); }; /** * Render tests results, an optionally exit phantomjs. * * @param Boolean exit Exit casper after results have been rendered? * @param Number status Exit status code (default: 0) * @param String save Optional path to file where to save the results log */ Tester.prototype.renderResults = function renderResults(exit, status, save) { "use strict"; /*eslint max-statements:0*/ save = save || this.options.save; var exitStatus = 0, failed = this.suiteResults.countFailed(), total = this.suiteResults.countExecuted(), statusText, style, result; if (total === 0) { exitStatus = 1; statusText = this.options.warnText; style = 'WARN_BAR'; result = f("%s Looks like you didn't run any tests.", statusText); } else { if (this.suiteResults.isFailed()) { exitStatus = 1; statusText = this.options.failText; style = 'RED_BAR'; } else { statusText = this.options.passText; style = 'GREEN_BAR'; } result = f('%s %d test%s executed in %ss, %d passed, %d failed, %d dubious, %d skipped.', statusText, total, total > 1 ? "s" : "", utils.ms2seconds(this.suiteResults.calculateDuration()), this.suiteResults.countPassed(), failed, this.suiteResults.countDubious(), this.suiteResults.countSkipped()); } this.casper.echo(result, style, this.options.pad); this.renderFailureDetails(); if (save) { this.saveResults(save); } if (exit === true) { this.emit("exit"); this.casper.exit(status ? ~~status : exitStatus); } }; /** * Runs all suites contained in the paths passed as arguments. * */ Tester.prototype.runSuites = function runSuites() { "use strict"; var testFiles = [], self = this; if (arguments.length === 0) { throw new CasperError("runSuites() needs at least one path argument"); } this.loadIncludes.includes.forEach(function _forEachInclude(include) { phantom.injectJs(include); }); this.loadIncludes.pre.forEach(function _forEachPreTest(preTestFile) { testFiles = testFiles.concat(preTestFile); }); Array.prototype.forEach.call(arguments, function _forEachArgument(path) { if (!fs.exists(path)) { self.bar(f("Path %s doesn't exist", path), "RED_BAR"); } if (fs.isDirectory(path)) { testFiles = testFiles.concat(self.findTestFiles(path)); } else if (fs.isFile(path)) { testFiles.push(path); } }); this.loadIncludes.post.forEach(function _forEachPostTest(postTestFile) { testFiles = testFiles.concat(postTestFile); }); if (testFiles.length === 0) { this.bar(f("No test file found in %s, terminating.", Array.prototype.slice.call(arguments)), "RED_BAR"); this.casper.exit(1); } self.currentSuiteNum = 0; self.currentTestStartTime = new Date(); self.lastAssertTime = 0; var interval = setInterval(function _check(self) { if (self.running) { return; } if (self.currentSuiteNum === testFiles.length || self.aborted) { self.emit('tests.complete'); clearInterval(interval); self.aborted = false; } else { self.runTest(testFiles[self.currentSuiteNum]); self.currentSuiteNum++; } }, 20, this); }; /** * Runs a test file * */ Tester.prototype.runTest = function runTest(testFile) { "use strict"; this.bar(f('Test file: %s', testFile), 'INFO_BAR'); this.running = true; // this.running is set back to false with done() this.executed = 0; this.exec(testFile); }; /** * Terminates current suite. * */ Tester.prototype.terminate = function(message) { "use strict"; if (message) { this.casper.warn(message); } this.done(); this.aborted = true; this.emit('tests.complete'); }; /** * Saves results to file. * * @param String filename Target file path. */ Tester.prototype.saveResults = function saveResults(filepath) { "use strict"; var exporter = require('xunit').create(); exporter.setResults(this.suiteResults); try { fs.write(filepath, exporter.getSerializedXML(), 'w'); this.casper.echo(f('Result log stored in %s', filepath), 'INFO', 80); } catch (e) { this.casper.echo(f('Unable to write results to %s: %s', filepath, e), 'ERROR', 80); } }; /** * Tests equality between the two passed arguments. * * @param Mixed v1 * @param Mixed v2 * @param Boolean */ Tester.prototype.testEquals = Tester.prototype.testEqual = function testEquals(v1, v2) { "use strict"; return utils.equals(v1, v2); }; /** * Processes an error caught while running tests contained in a given test * file. * * @param Error|String error The error * @param String file Test file where the error occurred * @param Number line Line number (optional) * @param Array backtrace Error stack trace (optional) */ Tester.prototype.uncaughtError = function uncaughtError(error, file, line, backtrace) { "use strict"; // XXX: this is NOT an assertion scratch that return this.processAssertionResult({ success: false, type: "uncaughtError", file: file, line: ~~line, message: utils.isObject(error) ? error.message : error, values: { error: error, stack: backtrace } }); }; /** * Test suites array. * */ function TestSuiteResult() {} TestSuiteResult.prototype = []; exports.TestSuiteResult = TestSuiteResult; /** * Returns the number of tests. * * @return Number */ TestSuiteResult.prototype.countTotal = function countTotal() { "use strict"; return this.countPassed() + this.countFailed() + this.countDubious(); }; /** * Returns the number of dubious results. * * @return Number */ TestSuiteResult.prototype.countDubious = function countDubious() { "use strict"; return this.map(function(result) { return result.dubious; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of executed tests. * * @return Number */ TestSuiteResult.prototype.countExecuted = function countTotal() { "use strict"; return this.countTotal() - this.countDubious(); }; /** * Returns the number of errors. * * @return Number */ TestSuiteResult.prototype.countErrors = function countErrors() { "use strict"; return this.map(function(result) { return result.crashed; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of failed tests. * * @return Number */ TestSuiteResult.prototype.countFailed = function countFailed() { "use strict"; return this.map(function(result) { return result.failed - result.dubious; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of succesful tests. * * @return Number */ TestSuiteResult.prototype.countPassed = function countPassed() { "use strict"; return this.map(function(result) { return result.passed; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of skipped tests. * * @return Number */ TestSuiteResult.prototype.countSkipped = function countSkipped() { "use strict"; return this.map(function(result) { return result.skipped; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Returns the number of warnings. * * @return Number */ TestSuiteResult.prototype.countWarnings = function countWarnings() { "use strict"; return this.map(function(result) { return result.warned; }).reduce(function(a, b) { return a + b; }, 0); }; /** * Checks if the suite has failed. * * @return Number */ TestSuiteResult.prototype.isFailed = function isFailed() { "use strict"; return this.countErrors() + this.countFailed() + this.countDubious() > 0; }; /** * Checks if the suite has skipped tests. * * @return Number */ TestSuiteResult.prototype.isSkipped = function isSkipped() { "use strict"; return this.countSkipped() > 0; }; /** * Returns all failures from this suite. * * @return Array */ TestSuiteResult.prototype.getAllFailures = function getAllFailures() { "use strict"; var failures = []; this.forEach(function(result) { failures = failures.concat(result.failures); }); return failures; }; /** * Returns all succesful tests from this suite. * * @return Array */ TestSuiteResult.prototype.getAllPasses = function getAllPasses() { "use strict"; var passes = []; this.forEach(function(result) { passes = passes.concat(result.passes); }); return passes; }; /** * Returns all skipped tests from this suite. * * @return Array */ TestSuiteResult.prototype.getAllSkips = function getAllSkips() { "use strict"; var skipped = []; this.forEach(function(result) { skipped = skipped.concat(result.skipped); }); return skipped; }; /** * Returns all results from this suite. * * @return Array */ TestSuiteResult.prototype.getAllResults = function getAllResults() { "use strict"; return this.getAllPasses().concat(this.getAllFailures()); }; /** * Computes the sum of all durations of the tests which were executed in the * current suite. * * @return Number */ TestSuiteResult.prototype.calculateDuration = function calculateDuration() { "use strict"; return this.getAllResults().map(function(result) { return ~~result.time; }).reduce(function add(a, b) { return a + b; }, 0); }; /** * Test suite results object. * * @param Object options */ function TestCaseResult(options) { "use strict"; this.name = options && options.name; this.file = options && options.file; this.planned = ~~(options && options.planned) || undefined; this.errors = []; this.failures = []; this.passes = []; this.skips = []; this.warnings = []; this.config = options && options.config; this.__defineGetter__("assertions", function() { return this.passed + this.failed; }); this.__defineGetter__("crashed", function() { return this.errors.length; }); this.__defineGetter__("failed", function() { return this.failures.length; }); this.__defineGetter__("dubious", function() { return this.failures.filter(function(failure) { return failure.type === "dubious"; }).length; }); this.__defineGetter__("passed", function() { return this.passes.length; }); this.__defineGetter__("skipped", function() { return this.skips.map(function(skip) { return skip.number; }).reduce(function(a, b) { return a + b; }, 0); }); } exports.TestCaseResult = TestCaseResult; /** * Adds a failure record and its execution time. * * @param Object failure * @param Number time */ TestCaseResult.prototype.addFailure = function addFailure(failure, time) { "use strict"; failure.suite = this.name; failure.time = time; this.failures.push(failure); }; /** * Adds an error record. * * @param Object failure */ TestCaseResult.prototype.addError = function addFailure(error) { "use strict"; error.suite = this.name; this.errors.push(error); }; /** * Adds a success record and its execution time. * * @param Object success * @param Number time */ TestCaseResult.prototype.addSuccess = function addSuccess(success, time) { "use strict"; success.suite = this.name; success.time = time; this.passes.push(success); }; /** * Adds a success record and its execution time. * * @param Object success * @param Number time */ TestCaseResult.prototype.addSkip = function addSkip(skipped, time) { "use strict"; skipped.suite = this.name; skipped.time = time; this.skips.push(skipped); }; /** * Adds a warning message. * NOTE: quite contrary to addError, addSuccess, and addSkip * this adds a String value, NOT an Object * * @param String warning */ TestCaseResult.prototype.addWarning = function addWarning(warning) { "use strict"; this.warnings.push(warning); }; /** * Computes total duration for this suite. * * @return Number */ TestCaseResult.prototype.calculateDuration = function calculateDuration() { "use strict"; function add(a, b) { return a + b; } var passedTimes = this.passes.map(function(success) { return ~~success.time; }).reduce(add, 0); var failedTimes = this.failures.map(function(failure) { return ~~failure.time; }).reduce(add, 0); return passedTimes + failedTimes; };