/*! * Module dependencies. */ var path = require('path'); var util = require('util'); var events = require('events'); var HttpRequest = require('./http/request.js'); var CommandQueue = require('./core/queue.js'); var Assertion = require('./core/assertion.js'); var Logger = require('./util/logger.js'); var Api = require('./core/api.js'); var Utils = require('./util/utils.js'); function Nightwatch(options) { events.EventEmitter.call(this); this.locateStrategy = 'css selector'; this.api = { capabilities : {}, globals : options && options.persist_globals && options.globals || {}, sessionId : null }; this.setMaxListeners(0); this.sessionId = null; this.context = null; this.terminated = false; this.setOptions(options) .setCapabilities() .loadKeyCodes(); this.errors = []; this.results = { passed:0, failed:0, errors:0, skipped:0, tests:[] }; Assertion.init(this); Api.init(this).load(); this.queue = CommandQueue; this.queue.empty(); this.queue.reset(); } Nightwatch.DEFAULT_CAPABILITIES = { browserName: 'firefox', javascriptEnabled: true, acceptSslCerts: true, platform: 'ANY' }; util.inherits(Nightwatch, events.EventEmitter); Nightwatch.prototype.assertion = Assertion.assert; Nightwatch.prototype.setOptions = function(options) { this.options = {}; this.api.options = {}; if (options && typeof options == 'object') { for (var propName in options) { this.options[propName] = options[propName]; } } this.api.launchUrl = this.options.launchUrl || this.options.launch_url || null; // backwords compatibility this.api.launch_url = this.api.launchUrl; if (this.options.globals && typeof this.options.globals == 'object' && !this.options.persist_globals) { for (var globalKey in this.options.globals) { this.api.globals[globalKey] = this.options.globals[globalKey]; } } var screenshots = this.options.screenshots; var screenshotsEnabled = screenshots && screenshots.enabled || false; this.api.options.screenshots = screenshotsEnabled; if (screenshotsEnabled) { if (typeof screenshots.path == 'undefined') { throw new Error('Please specify the screenshots.path in nightwatch.json.'); } this.options.screenshots.on_error = this.options.screenshots.on_error || (typeof this.options.screenshots.on_error == 'undefined'); this.api.screenshotsPath = this.api.options.screenshotsPath = screenshots.path; } else { this.options.screenshots = { enabled : false, path : '' }; } if (this.options.use_xpath) { this.locateStrategy = 'xpath'; } if (this.options.silent) { Logger.disable(); } else { Logger.enable(); } this.options.start_session = this.options.start_session || (typeof this.options.start_session == 'undefined'); this.api.options.skip_testcases_on_fail = this.options.skip_testcases_on_fail || (typeof this.options.skip_testcases_on_fail == 'undefined' && this.options.start_session); // off by default for unit tests this.api.options.log_screenshot_data = this.options.log_screenshot_data || (typeof this.options.log_screenshot_data == 'undefined'); var seleniumPort = this.options.seleniumPort || this.options.selenium_port; var seleniumHost = this.options.seleniumHost || this.options.selenium_host; var useSSL = this.options.useSsl || this.options.use_ssl; var proxy = this.options.proxy; if (seleniumPort) { HttpRequest.setSeleniumPort(seleniumPort); } if (seleniumHost) { HttpRequest.setSeleniumHost(seleniumHost); } if (useSSL) { HttpRequest.useSSL(true); } if (proxy) { HttpRequest.setProxy(proxy); } if (typeof this.options.default_path_prefix == 'string') { HttpRequest.setDefaultPathPrefix(this.options.default_path_prefix); } var username = this.options.username; var key = this.options.accesKey || this.options.access_key || this.options.password; if (username && key) { this.api.options.username = username; this.api.options.accessKey = key; HttpRequest.setCredentials({ username : username, key : key }); } this.endSessionOnFail(typeof this.options.end_session_on_fail == 'undefined' || this.options.end_session_on_fail); return this; }; Nightwatch.prototype.endSessionOnFail = function(value) { if (typeof value == 'undefined') { return this.options.end_session_on_fail; } this.options.end_session_on_fail = value; return this; }; Nightwatch.prototype.setCapabilities = function() { this.desiredCapabilities = {}; for (var capability in Nightwatch.DEFAULT_CAPABILITIES) { this.desiredCapabilities[capability] = Nightwatch.DEFAULT_CAPABILITIES[capability]; } if (this.options.desiredCapabilities) { for (var prop in this.options.desiredCapabilities) { if (this.options.desiredCapabilities.hasOwnProperty(prop)) { this.desiredCapabilities[prop] = this.options.desiredCapabilities[prop]; } } } this.api.options.desiredCapabilities = this.desiredCapabilities; return this; }; Nightwatch.prototype.loadKeyCodes = function() { this.api.Keys = require('./util/keys.json'); return this; }; Nightwatch.prototype.start = function() { if (!this.sessionId && this.options.start_session) { this .once('selenium:session_create', this.start) .startSession(); return this; } var self = this; this.queue.reset(); this.queue.run(function(error) { if (error) { var stackTrace = ''; if (error.stack) { stackTrace = error.stack.split('\n').slice(1).join('\n'); } self.results.errors++; self.errors.push(error.name + ': ' + error.message + '\n' + stackTrace); if (self.options.output) { Utils.showStackTraceWithHeadline(error.name + ': ' + error.message, stackTrace, true); } if (self.options.start_session) { self.terminate(); } return; } self.finished(); }); return this; }; Nightwatch.prototype.terminate = function(deferred) { // in case this was a synchronous command (e.g. assert.ok()) we need to wait for other possible // commands which might have been added afterwards while client is terminated if (deferred) { this.queue.instance().once('queue:started', this.terminateSession.bind(this)); } else { this.terminateSession(); } this.terminated = true; return this; }; Nightwatch.prototype.resetTerminated = function() { this.terminated = false; return this; }; Nightwatch.prototype.terminateSession = function() { this.queue.reset(); this.queue.empty(); if (this.options.end_session_on_fail && this.options.start_session) { this.api.end(function() { this.finished(); }.bind(this)); // FIXME: sometimes the queue is incorrectly restarted when another .end() is // scheduled from globalBeforeEach and results into a session command being sent with // null as the sessionId this.queue.run(); } else { this.finished(); } return this; }; Nightwatch.prototype.complete = function() { return this.emit('complete'); }; Nightwatch.prototype.finished = function() { Logger.info('FINISHED'); this.emit('nightwatch:finished', this.results, this.errors); return this; }; Nightwatch.prototype.getFailureMessage = function() { var errors = ''; var failure_msg = []; if (this.results.failed > 0) { failure_msg.push(Logger.colors.red(this.results.failed) + ' assertions failed'); } if (this.results.errors > 0) { failure_msg.push(Logger.colors.red(this.results.errors) + ' errors'); } if (this.results.passed > 0) { failure_msg.push(Logger.colors.green(this.results.passed) + ' passed'); } if (this.results.skipped > 0) { failure_msg.push(Logger.colors.blue(this.results.skipped) + ' skipped'); } return failure_msg.join(', ').replace(/,([^,]*)$/g, function($0, $1) { return ' and' + $1; }); }; Nightwatch.prototype.printResult = function(elapsedTime) { if (this.options.output && this.options.start_session) { var ok = false; if (this.results.failed === 0 && this.results.errors === 0) { ok = true; } if (ok && this.results.passed > 0) { console.log('\n' + Logger.colors.green('OK.'), Logger.colors.green(this.results.passed) + ' assertions passed. (' + Utils.formatElapsedTime(elapsedTime, true) + ')'); } else if (ok && this.results.passed === 0) { if (this.options.start_session) { console.log(Logger.colors.green('No assertions ran.')); } } else { var failure_msg = this.getFailureMessage(); console.log('\n' + Logger.colors.red('FAILED: '), failure_msg, '(' + Utils.formatElapsedTime(elapsedTime, true) + ')'); } } }; Nightwatch.prototype.clearResult = function() { this.errors.length = 0; this.results.passed = 0; this.results.failed = 0; this.results.errors = 0; this.results.skipped = 0; this.results.tests.length = 0; }; Nightwatch.prototype.handleException = function(err) { var stack = err.stack.split('\n'); var failMessage = stack.shift(); var firstLine = ' ' + String.fromCharCode(10006) + ' ' + failMessage; if (typeof err.actual != 'undefined' && typeof err.expected != 'undefined') { firstLine += '\033[0;90m - expected ' + Logger.colors.green('"' + err.expected + '"') + ' \033[0;90mbut got: ' + Logger.colors.red('"' + err.actual + '"'); } if (this.options.output) { Utils.showStackTraceWithHeadline(firstLine, stack); } if (err.name == 'AssertionError') { this.results.failed++; stack.unshift(failMessage + ' - expected "' + err.expected + '" but got: "' + err.actual + '"'); this.results.stackTrace = stack.join('\n'); } else { this.addError('\n ' + err.stack, firstLine); this.terminate(); } }; Nightwatch.prototype.runProtocolAction = function(requestOptions, callback) { var self = this; var request = new HttpRequest(requestOptions) .on('result', function(result) { if (typeof callback != 'function') { var error = new Error('Callback parameter is not a function - ' + typeof(callback) + ' passed: "' + callback + '"'); self.errors.push(error.stack); self.results.errors++; } else { callback.call(self.api, result); } if (result.lastScreenshotFile && self.results.tests.length > 0) { var lastTest = self.results.tests[self.results.tests.length-1]; lastTest.screenshots = lastTest.screenshots || []; lastTest.screenshots.push(result.lastScreenshotFile); delete result.lastScreenshotFile; } request.emit('complete'); }) .on('success', function(result, response) { if (result.status && result.status !== 0) { result = self.handleTestError(result); } request.emit('result', result, response); }) .on('error', function(result, response, screenshotContent) { result = self.handleTestError(result); if (screenshotContent && self.options.screenshots.on_error) { var fileNamePath = Utils.getScreenshotFileName(self.api.currentTest, true, self.options.screenshots.path); self.saveScreenshotToFile(fileNamePath, screenshotContent); result.lastScreenshotFile = fileNamePath; } request.emit('result', result, response); }); return request; }; Nightwatch.prototype.addError = function(message, logMessage) { var currentTest; if (this.api.currentTest) { currentTest = '[' + Utils.getTestSuiteName(this.api.currentTest.module) + ' / ' + this.api.currentTest.name + ']'; } else { currentTest = 'tests'; } this.errors.push(' Error while running '+ currentTest + ':\n' + message); this.results.errors++; if (this.options.output) { Logger.warn(' ' + (logMessage || message)); } }; Nightwatch.prototype.saveScreenshotToFile = function(fileName, content, cb) { var mkpath = require('mkpath'); var fs = require('fs'); var self = this; cb = cb || function() {}; var dir = path.resolve(fileName, '..'); var fail = function(err) { if (self.options.output) { console.log(Logger.colors.yellow('Couldn\'t save screenshot to '), fileName); } Logger.warn(err); cb(err); }; mkpath(dir, function(err) { if (err) { fail(err); } else { fs.writeFile(fileName, content, 'base64', function(err) { if (err) { fail(err); } else { cb(null, fileName); } }); } }); }; Nightwatch.prototype.handleTestError = function(result) { var errorMessage = ''; if (result && result.status) { var errorCodes = require('./api/errors.json'); errorMessage = errorCodes[result.status] && errorCodes[result.status].message || ''; } return { status: -1, value : result && result.value || null, errorStatus: result && result.status || '', error : errorMessage }; }; Nightwatch.prototype.startSession = function () { if (this.terminated) { return this; } var self = this; var options = { path : '/session', data : { desiredCapabilities : this.desiredCapabilities } }; var request = new HttpRequest(options); request.on('success', function(data, response, isRedirect) { if (data && data.sessionId) { self.sessionId = self.api.sessionId = data.sessionId; if (data.value) { self.api.capabilities = data.value; } Logger.info('Got sessionId from selenium', self.sessionId); self.emit('selenium:session_create', self.sessionId, request, response); } else if (isRedirect) { self.followRedirect(request, response); } else { request.emit('error', data, null); } }) .on('error', function(data, err) { if (self.options.output) { console.error('\n' + Logger.colors.light_red('Error retrieving a new session from the selenium server')); } if (typeof data == 'object' && Object.keys(data).length === 0) { data = ''; } if (!data && err) { data = err; } self.emit('error', data); }) .send(); return this; }; Nightwatch.prototype.followRedirect = function (request, response) { if (!response.headers || !response.headers.location) { this.emit('error', null, null); return this; } var url = require('url'); var urlParts = url.parse(response.headers.location); request.setOptions({ path : urlParts.pathname, host : urlParts.hostname, port : urlParts.port, method : 'GET' }).send(); return this; }; exports = module.exports = {}; exports.client = function(options) { return new Nightwatch(options); }; exports.cli = function(runTests) { var cli = require('./runner/cli/cli.js'); cli.setup(); var argv = cli.init(); if (argv.help) { cli.showHelp(); } else if (argv.version) { var packageConfig = require(__dirname + '/../package.json'); console.log(packageConfig.name + ' v' + packageConfig.version); } else { if (typeof runTests != 'function') { throw new Error('Supplied argument needs to be a function!'); } runTests(argv); } }; exports.runner = function(argv, done, settings) { var runner = exports.CliRunner(argv); return runner.setup(settings, done).runTests(done); }; exports.initGrunt = function(grunt) { grunt.registerMultiTask('nightwatch', 'run nightwatch.', function() { var done = this.async(); var options = this.options(); var settings = this.data && this.data.settings; var argv = this.data && this.data.argv; exports.cli(function(a) { Object.keys(argv).forEach(function(key) { if (key === 'env' && a['parallel-mode'] === true) { return; } a[key] = argv[key]; }); if (a.test) { a.test = path.resolve(a.test); } if (options.cwd) { process.chdir(options.cwd); } exports.runner(a, done, settings); }); }); }; exports.CliRunner = function(argv) { var CliRunner = require('./runner/cli/clirunner.js'); return new CliRunner(argv); }; exports.initClient = function(opts) { var Manager = require('./runner/clientmanager.js'); var instance = new Manager(); return instance.init(opts); };