diff --git a/.versions b/.versions index 2b26955..6419c2e 100644 --- a/.versions +++ b/.versions @@ -1,4 +1,4 @@ -anti:gagarin@0.4.1 +anti:gagarin@0.4.2 base64@1.0.2 callback-hook@1.0.2 check@1.0.4 diff --git a/README.md b/README.md index a62770e..9ac517a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ var server = meteor(); var client = browser(server); // before 0.4.0 you would use server.location here ``` -# gagarin [![Circle CI](https://circleci.com/gh/anticoders/gagarin/tree/develop.svg?style=svg)](https://circleci.com/gh/anticoders/gagarin/tree/devel) +# gagarin [![Circle CI](https://circleci.com/gh/anticoders/gagarin/tree/develop.svg?style=svg)](https://circleci.com/gh/anticoders/gagarin/tree/develop) Gagarin is a tool you can use in your tests to run Meteor apps in a sandboxed environment. It's useful when you need more refined control over the meteor processes and test fancy things, e.g. the behavior of your app on server restarts or when you have multiple app instances writing to the same database. This is currently not achievable with the official Meteor testing framework. diff --git a/bin/gagarin b/bin/gagarin index bcd40da..ee6e26f 100755 --- a/bin/gagarin +++ b/bin/gagarin @@ -35,13 +35,14 @@ program .option('-r, --remote-server ', 'run tests on a remote server') .option('-m, --mute-build', 'do not show build logs', false) .option('-f, --flavor ', 'default flavor of api (promise, fiber)', 'promise') + .option('-T, --startup-timeout ' ,'server startup timeout [5000]', intParse, 5000) + .option('-U, --build-timeout ' ,'meteor building timeout [120000]', intParse, 120000); program.name = 'gagarin'; program.parse(process.argv); var gagarin = new Gagarin(program); - var pathToTests = program.args[0] || path.join(program.pathToApp, 'tests', 'gagarin'); if (!fs.existsSync(pathToTests)) { @@ -79,4 +80,3 @@ gagarin.run(function (failedCount) { function intParse(v) { return parseInt(v); }; - diff --git a/lib/browser/helpers.js b/lib/browser/helpers.js index c74268c..bbc67aa 100644 --- a/lib/browser/helpers.js +++ b/lib/browser/helpers.js @@ -1,11 +1,32 @@ var expect = require('chai').expect; var either = require('../tools').either; var Promise = require('es6-promise').Promise; - var DEFAULT_TIMEOUT = 5000; +// these two guys are required for screenshots +var fs = require('fs'); +var path = require('path'); + module.exports = { + addScript: function (url, verify, args) { + var self = this; + return self.promise(function (resolve, reject, url) { + var script = window.document.createElement('script'); + script.src = url; + script.addEventListener('load', resolve); + window.document.head.appendChild(script); + }, [ url ]).then(function () { + if (verify) { + return self.noWait().execute(verify, args).then(function (success) { + if (!success) { + throw new Error('Script ' + url + ' has loaded but it seems that it does not contain the expected content.'); + } + }); + } + }); + }, + waitForDOM: function (selector, timeout) { return this.wait(timeout || DEFAULT_TIMEOUT, 'until element ' + selector + ' is present', function (selector) { return !!document.querySelector(selector); @@ -21,7 +42,11 @@ module.exports = { waitUntilNotVisible: function (selector, timeout) { return this.wait(timeout || DEFAULT_TIMEOUT, 'until element ' + selector + ' is hidden', function (selector) { var element = document.querySelector(selector); - return !element || window.getComputedStyle(element).display === 'none'; + if(!!element){ + return element.offsetWidth <= 0 && element.offsetHeight <= 0; + }else{ + return false; + } }, [ selector ]); }, @@ -167,7 +192,7 @@ module.exports = { disconnect: function () { return this.execute(function () { Meteor.disconnect(); - }).wait(1000, 'unitl Meteor disconnects', function () { + }).wait(1000, 'until Meteor disconnects', function () { return !Meteor.status().connected; }); }, @@ -175,7 +200,7 @@ module.exports = { reconnect: function () { return this.execute(function () { Meteor.reconnect(); - }).wait(1000, 'unitl Meteor reconnects', function () { + }).wait(1000, 'until Meteor reconnects', function () { return Meteor.status().connected; }); }, @@ -205,7 +230,11 @@ module.exports = { checkIfVisible: function (selector) { return this.execute(function (selector) { var element = document.querySelector(selector); - return !!element && window.getComputedStyle(element).display !== 'none'; + if(!!element){ + return element.offsetWidth > 0 && element.offsetHeight > 0; + }else{ + return false; + } }, [ selector ]); }, diff --git a/lib/browser/index.js b/lib/browser/index.js index 8cd9fbe..a244080 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -40,7 +40,8 @@ module.exports = function createBrowser (options) { 'setCookie', 'deleteAllCookies', 'deleteCookie', - 'getLocation', + // seems like it's not working well + // 'getLocation', ]; // TODO: early detect colisions @@ -71,7 +72,7 @@ module.exports = function createBrowser (options) { function getClientLogs (action) { return function (operand, done) { - return action(operand, function (err) { + return action.call(this, operand, function (err) { var args = Array.prototype.slice.call(arguments, 0); if (err) { done(err); diff --git a/lib/browser/methods.js b/lib/browser/methods.js index 1b8601f..3948e23 100644 --- a/lib/browser/methods.js +++ b/lib/browser/methods.js @@ -1,4 +1,6 @@ var cleanError = require('../tools').cleanError; +var either = require('../tools').either; +var fs = require('fs'); module.exports = {}; @@ -7,7 +9,7 @@ module.exports.execute = function (code, args) { var self = this; - if (arguments.length < 2) { + if (args === undefined) { args = []; } @@ -20,12 +22,25 @@ module.exports.execute = function (code, args) { } return self.__custom__(function (operand, done) { - var closure = operand.closure ? operand.closure() : {}; - code = wrapSourceCode(codeToString(code), args, closure); + if (this.lastError) { + if (this.retryCount <= 1 && /chai not found/.test(this.lastError.message)) { + operand.browser.execute(fs.readFileSync('./node_modules/chai/chai.js', "utf8"), either(done).or(doAction)); + } else { + done(this.lastError); + } + } else { + doAction(); + } + + function doAction() { + var closure = operand.closure ? operand.closure() : {}; + + operand.browser.execute("return (" + wrapSourceCode(codeToString(code), args, closure) + ").apply(null, arguments)", + values(closure), feedbackProcessor(operand.closure.bind(operand), done)); + } - operand.browser.execute("return (" + code + ").apply(null, arguments)", - values(closure), feedbackProcessor(operand.closure.bind(operand), done)); + return true; // retry on first attempt }); }; @@ -39,7 +54,7 @@ module.exports.promise = function (code, args) { var self = this; - if (arguments.length < 2) { + if (args === undefined) { args = []; } @@ -113,7 +128,7 @@ module.exports.promise = function (code, args) { module.exports.wait = function (timeout, message, code, args) { "use strict"; - if (arguments.length < 4) { + if (args === undefined) { args = []; } @@ -252,7 +267,11 @@ function wrapSourceCode(code, args, closure) { //addSyncChunks(chunks, closure, accessor); chunks.push( + " 'use strict';", +// " var expect;", " try {", +// " if (!window.chai) { throw new Error('chai not found'); }", +// " expect = window.chai.expect;", " return (function ($) {", " return {", " closure: {" diff --git a/lib/meteor/build.js b/lib/meteor/build.js index 262f3a5..6209e83 100644 --- a/lib/meteor/build.js +++ b/lib/meteor/build.js @@ -12,11 +12,10 @@ var myBuildPromises = {}; module.exports = function BuildAsPromise (options) { "use strict"; - options = options || {}; var pathToApp = options.pathToApp || path.resolve('.'); - var timeout = options.timeout || 60000; + var timeout = options.timeout || 120000; var verbose = options.verbose !== undefined ? !!options.verbose : false; var pathToSmartJson = path.join(pathToApp, 'smart.json'); @@ -33,25 +32,19 @@ module.exports = function BuildAsPromise (options) { if (myBuildPromises[pathToApp]) return myBuildPromises[pathToApp]; - if (fs.existsSync(pathToSmartJson)) { - myBuildPromises[pathToApp] = tools.smartPackagesAsPromise(pathToApp).then(function () { - return MongoServerAsPromise({ pathToApp: pathToApp }).then(function (mongoUrl) { - return BuildPromise({ - pathToApp : pathToApp, - mongoUrl : mongoUrl, - verbose : verbose, - }); - }); - }); - } else { - myBuildPromises[pathToApp] = MongoServerAsPromise({ pathToApp: pathToApp }).then(function (mongoUrl) { + var smartPackagesOrNothing = fs.existsSync(pathToSmartJson) ? + tools.smartPackagesAsPromise(pathToApp) : Promise.resolve(); + + myBuildPromises[pathToApp] = smartPackagesOrNothing.then(function () { + return MongoServerAsPromise({ pathToApp: pathToApp }).then(function (mongoUrl) { return BuildPromise({ pathToApp : pathToApp, mongoUrl : mongoUrl, verbose : verbose, + timeout : timeout }); }); - } + }); return myBuildPromises[pathToApp]; }; @@ -106,12 +99,6 @@ function BuildPromise(options) { reject(new Error('File ' + pathToMain + ' does not exist.')); return; } - try { - checkIfVersionsMatches(pathToApp); - } catch (err) { - reject(err); - return; - } resolve(pathToMain); }); meteor.kill('SIGINT'); @@ -181,7 +168,7 @@ function BuildPromise(options) { setTimeout(function () { meteor.once('exit', function () { - reject(new Error('Timeout while wating for meteor to start.')); + reject(new Error('Timeout while waiting for meteor to start.')); }); meteor.kill('SIGINT') }, timeout); @@ -202,28 +189,9 @@ function BuildPromise(options) { */ function isLocked(pathToApp) { var pathToMongoLock = path.join(pathToApp, '.meteor', 'local', 'db', 'mongod.lock'); - return fs.existsSync(pathToMongoLock) && fs.readFileSync(pathToMongoLock).toString('utf8'); + return fs.existsSync(pathToMongoLock) && fs.readFileSync(pathToMongoLock).toString('utf8'); } /** * Verify if Gagarin is instaled and if the version is compatible. */ -function checkIfVersionsMatches(pathToApp) { - - var pathToVersions = path.join(pathToApp, '.meteor', 'versions'); - var versions = fs.readFileSync(pathToVersions, 'utf-8'); - var versionMatch = versions.match(/anti:gagarin@(.*)/); - - if (!versionMatch) { // looks like gagarin is not even instaled - throw new Error('Please add anti:gagarin to your app before running tests.'); - } else if (versionMatch[1] !== version) { // versions of gagarin are not compatible - throw new Error( - 'Versions of node package (' + version + - ') and meteor package (' + versionMatch[1] + - ') are not compatible; please update.' - ); - } - -} - - diff --git a/lib/meteor/index.js b/lib/meteor/index.js index 191270f..4ba6101 100644 --- a/lib/meteor/index.js +++ b/lib/meteor/index.js @@ -9,16 +9,16 @@ var Closure = require('../tools/closure'); var generic = require('../tools/generic'); var tools = require('../tools'); var url = require('url'); +var path = require('path'); +var fs = require('fs'); +var version = require('../../package.json').version; module.exports = function createMeteor (options) { "use strict"; - options = options || {}; - if (typeof options === 'string') { options = { pathToApp: options }; } - var pathToApp = options.pathToApp || path.resolve('.'); var skipBuild = !!options.skipBuild; var verbose = !!options.verbose; @@ -91,7 +91,7 @@ module.exports = function createMeteor (options) { // TODO: do not start if we haven't done it yet return this.__custom__(function (operand, done) { - + operand.ddpClient.close(); if (!operand.process) { // e.g. if using remote server @@ -169,6 +169,11 @@ module.exports = function createMeteor (options) { } function getMeteor () { + try { + checkIfVersionsMatch(options.pathToApp) + } catch (err) { + return Promise.reject(err); + } if (remoteServer) { return Promise.resolve(null); @@ -179,7 +184,8 @@ module.exports = function createMeteor (options) { getPathToMain(), getMongoUrl() ]).then(function (results) { - + + return getMeteorProcess({ pathToNode : tools.getNodePath(pathToApp), pathToMain : results[0], @@ -198,16 +204,16 @@ module.exports = function createMeteor (options) { getDDPSetup(), getMeteor(), getConfig() ]).then(function (results) { - + return getDDPClient(results[0]).then(function (ddpClient) { return { ddpClient: ddpClient, process: results[1], closure: closure }; }); - + }); } function getDDPSetup () { - + if (remoteServer) { return Promise.resolve({ host: remoteServer.hostname, @@ -227,3 +233,20 @@ module.exports = function createMeteor (options) { return meteor; } + +function checkIfVersionsMatch(pathToApp) { + + var pathToVersions = path.join(pathToApp, '.meteor', 'versions'); + var versions = fs.readFileSync(pathToVersions, 'utf-8'); + var versionMatch = versions.match(/anti:gagarin@(.*)/); + if (!versionMatch) { // looks like gagarin is not even instaled + throw new Error('Please add anti:gagarin to your app before running tests.'); + } else if (versionMatch[1] !== version) { // versions of gagarin are not compatible + throw new Error( + 'Versions of node package (' + version + + ') and meteor package (' + versionMatch[1] + + ') are not compatible; please update.' + ); + } + +} diff --git a/lib/meteor/meteorProcess.js b/lib/meteor/meteorProcess.js index 262b9f2..defe654 100644 --- a/lib/meteor/meteorProcess.js +++ b/lib/meteor/meteorProcess.js @@ -3,60 +3,80 @@ var spawn = require('child_process').spawn; var processCounter = 0; /** - * Creates a smart meteor instance, suitable for usage with the testing framework. - * - * @param {string} path to node executable - * @param {string} path to applciation main file - * @param {object} environment variables for the new process - * @param {object} prototype for the returned object - * @param {object} options; may contain safetyTimeout value - * - * The prototype may implement the following methods - * - onStart () - * - onExit () - * - onData (data, { isError: true/false }) - */ +* Creates a smart meteor instance, suitable for usage with the testing framework. +* +* @param {string} path to node executable +* @param {string} path to applciation main file +* @param {object} environment variables for the new process +* @param {object} prototype for the returned object +* @param {object} options; may contain startupTimeout value +* +* The prototype may implement the following methods +* - onStart () +* - onExit () +* - onData (data, { isError: true/false }) +*/ module.exports = function createMeteorProcess (node, main, env, myPrototype, options) { "use strict"; - options = options || {}; - - var instance = Object.create(myPrototype); - var meteor = null; - var lastError = ""; - var lastErrorAt = "nowhere"; - var safetyTimeout = options.safetyTimeout !== undefined ? options.safetyTimeout : 5 * 1000; - var safetyHandle = null; + var instance = Object.create(myPrototype); + var meteor = null; + var lastError = ""; + var lastErrorAt = "nowhere"; + var startupTimeout = options.startupTimeout; + var startupHandle = null; + var hasFirstOutput = false; instance.env = env; instance.pid = processCounter++; // the only requirement is that it stays unique "locally" - instance.kill = kill; setTimeout(function () { + try { meteor = spawn(node, [ main ], { env: env }); } catch (err) { return instance.onStart && instance.onStart(err); } - safetyHandle = setTimeout(function () { + startupHandle = setTimeout(function () { kill(function (err) { if (instance.onStart) { if (err) { instance.onStart(err); } else { - instance.onStart(new Error('Gagarin is not there.' + - ' Please make sure you have added it with: meteor add anti:gagarin.')); + instance.onStart(new Error(startupTimeout +' ms startup timeout exceeded when waiting for the first server output;' + + ' please try increasing it with -T option')); } } }); - }, safetyTimeout); + }, startupTimeout); + //just for internal test + if (options.startupTimeout2 !== undefined) { + startupTimeout = options.startupTimeout2; + } + meteor.stdout.on('data', function (data) { + if (!hasFirstOutput) { + clearTimeout(startupHandle); + hasFirstOutput = true; + startupHandle = setTimeout(function () { + kill(function (err) { + if (instance.onStart) { + if (err) { + instance.onStart(err); + } else { + instance.onStart(new Error(startupTimeout +' ms startup timeout exceeded when waiting for server startup;' + + ' please try to increasing it using -T option')); + } + } + }); + }, startupTimeout); + } if (/Поехали!/.test(data.toString())) { - clearTimeout(safetyHandle); + clearTimeout(startupHandle); //------------------------------------- instance.onStart && instance.onStart(); } @@ -74,21 +94,21 @@ module.exports = function createMeteorProcess (node, main, env, myPrototype, opt data.toString().split('\n').forEach(function (line) { var hasMatch = [ - { - regExp: /Error\:\s*(.*)/, - action: function (match) { - lastError = match[1]; - lastErrorAt = ''; - }, + { + regExp: /Error\:\s*(.*)/, + action: function (match) { + lastError = match[1]; + lastErrorAt = ''; }, - { - regExp: /at\s.*/, - action: function (match) { - if (!lastErrorAt) { - lastErrorAt = match[0]; - } - }, + }, + { + regExp: /at\s.*/, + action: function (match) { + if (!lastErrorAt) { + lastErrorAt = match[0]; + } }, + }, ].some(function (options) { var match = options.regExp.exec(line); if (match) { @@ -103,7 +123,7 @@ module.exports = function createMeteorProcess (node, main, env, myPrototype, opt }); meteor.on('exit', function (code) { - clearTimeout(safetyHandle); + clearTimeout(startupHandle); //------------------------- if (instance.onExit) { instance.onExit(code, lastError, lastErrorAt); @@ -114,10 +134,10 @@ module.exports = function createMeteorProcess (node, main, env, myPrototype, opt }); /** - * Kill the meteor process and cleanup. - * - * @param {Function} cb - */ + * Kill the meteor process and cleanup. + * + * @param {Function} cb + */ function kill (cb) { if (!meteor) { return cb(); @@ -143,4 +163,3 @@ module.exports = function createMeteorProcess (node, main, env, myPrototype, opt return instance; } - diff --git a/lib/meteor/meteorProcessManager.js b/lib/meteor/meteorProcessManager.js index 79276e0..360e623 100644 --- a/lib/meteor/meteorProcessManager.js +++ b/lib/meteor/meteorProcessManager.js @@ -7,9 +7,7 @@ var tools = require('../tools'); module.exports = function createMeteorProcessManager (options) { "use strict"; - options = options || {}; - var meteorProcessPrototype = {}; var meteorProcess = null; var meteorPromise = null; @@ -18,6 +16,7 @@ module.exports = function createMeteorProcessManager (options) { var pathToNode = ""; var mongoUrl = ""; + var meteorHasCrashed = false; var meteorSettings = tools.getSettings(options.settings); @@ -51,11 +50,9 @@ module.exports = function createMeteorProcessManager (options) { }).then(function () { done() }).catch(done); } - - function getMeteorProcess (setup) { - - // TODO: make sure the setup is fine + function getMeteorProcess (setup) { + // TODO: make sure the set up is fine if (pathToMain !== setup.pathToMain || pathToNode !== setup.pathToNode || mongoUrl !== setup.mongoUrl) { restartRequired = true; } @@ -95,8 +92,7 @@ module.exports = function createMeteorProcessManager (options) { env.GAGARIN_SETTINGS = "{}"; // only used if METEOR_SETTINGS does not contain gagarin field setTimeout(function () { - // TODO: we may also specify the "safetyTimeout" here - meteorProcess = new createMeteorProcess(pathToNode, pathToMain, env, meteorProcessPrototype, {}); + meteorProcess = new createMeteorProcess(pathToNode, pathToMain, env, meteorProcessPrototype, options); }, meteorRestartDelay); }).catch(reject); @@ -161,4 +157,3 @@ module.exports = function createMeteorProcessManager (options) { return getMeteorProcess; } - diff --git a/lib/mocha/gagarin.js b/lib/mocha/gagarin.js index acdf609..15d459e 100644 --- a/lib/mocha/gagarin.js +++ b/lib/mocha/gagarin.js @@ -51,6 +51,7 @@ Gagarin.prototype.run = function (callback) { var buildOnly = !!this.options.buildOnly; var muteBuild = !!this.options.muteBuild; var verbose = buildOnly || (this.options.verbose !== undefined ? !!this.options.verbose : false); + var buildTimeout = this.options.buildTimeout; var self = this; process.stdout.write('\n'); @@ -75,6 +76,7 @@ Gagarin.prototype.run = function (callback) { pathToApp : pathToApp, skipBuild : skipBuild, verbose : !muteBuild, + timeout : buildTimeout }).then(function () { diff --git a/lib/mocha/interface.js b/lib/mocha/interface.js index 931545c..d8d39e6 100644 --- a/lib/mocha/interface.js +++ b/lib/mocha/interface.js @@ -68,14 +68,16 @@ Mocha.interfaces['gagarin'] = module.exports = function (suite) { } tools.mergeHelpers(myHelpers, options.helpers); - var meteor = new Meteor({ - pathToApp : options.pathToApp || gagarinOptions.pathToApp, - helpers : myHelpers, - settings : tools.getSettings(options.settings) || gagarinSettings, - verbose : options.verbose !== undefined ? options.verbose : gagarinOptions.verbose, - remoteServer : options.remoteServer || gagarinOptions.remoteServer, - skipBuild : options.skipBuild !== undefined ? options.skipBuild : gagarinOptions.skipBuild, + + pathToApp : options.pathToApp || gagarinOptions.pathToApp, + helpers : myHelpers, + settings : tools.getSettings(options.settings) || gagarinSettings, + verbose : options.verbose !== undefined ? options.verbose : gagarinOptions.verbose, + remoteServer : options.remoteServer || gagarinOptions.remoteServer, + skipBuild : options.skipBuild !== undefined ? options.skipBuild : gagarinOptions.skipBuild, + startupTimeout : options.startupTimeout !== undefined ? options.startupTimeout : gagarinOptions.startupTimeout, + startupTimeout2 : options.startupTimeout2, // this one is only used for internal tests }); meteor.useClosure(function () { @@ -110,7 +112,7 @@ Mocha.interfaces['gagarin'] = module.exports = function (suite) { context.browser = function (options, initialize) { var createBrowser = require('../browser'); - + var myHelpers = {}; options = options || {}; @@ -222,12 +224,10 @@ Mocha.interfaces['gagarin'] = module.exports = function (suite) { context.closure = function (listOfKeys, runInContext) { var accessor = runInContext.length >= 2 ? runInContext : function (key, value) { - return runInContext(key + (arguments.length > 1 ? '=' + JSON.stringify(value) : '')); + return runInContext(key + (arguments.length > 1 ? '=' + stringify(value) : '')); } before(function () { - stack.push( - new Closure(stack[stack.length-1], listOfKeys, accessor) - ); + stack.push(new Closure(stack[stack.length-1], listOfKeys, accessor)); }); after(function () { stack.pop(); @@ -238,6 +238,13 @@ Mocha.interfaces['gagarin'] = module.exports = function (suite) { }); } +function stringify(value) { + if (typeof value === 'function') { + throw new Error('cannot use function as a closure variable'); + } + return value !== undefined ? JSON.stringify(value) : "undefined"; +} + function wrapPromisesForFiber(obj, methodList) { var proxy = {}; @@ -299,4 +306,4 @@ function runInsideFiber (originalFunction) { }; return fiberizeFunction; -} \ No newline at end of file +} diff --git a/lib/mongo/mongoDBProcess.js b/lib/mongo/mongoDBProcess.js index 583f7e8..13ba972 100644 --- a/lib/mongo/mongoDBProcess.js +++ b/lib/mongo/mongoDBProcess.js @@ -1,4 +1,5 @@ var MongoClient = require('mongodb').MongoClient; +var findAvailablePort = require('../tools').findAvailablePort; var debounce = require('debounce'); var Promise = require('es6-promise').Promise; var mkdirp = require('mkdirp'); @@ -23,74 +24,76 @@ module.exports = function MongoDBProcess (options) { } var pathToGitIgnore = tools.getPathToGitIgnore(pathToApp); - var mongoPort = options.dbPort || 27018 + Math.floor(Math.random() * 1000); + var mongoPort = options.dbPort; var mongoPath = tools.getMongoPath(pathToApp); var pathToDB = options.dbPath || tools.getPathToDB(pathToApp); - var mongoUrl = 'mongodb://127.0.0.1:' + mongoPort; - + var mongoUrl; + if (!fs.existsSync(mongoPath)) { return Promise.reject(new Error('file ' + mongoPath + ' does not exists')); } //------------------------------------------------------------------------- myMongoServerPromises[pathToApp] = new Promise(function (resolve, reject) { - var mongoArgs = [ '--port', mongoPort, '--smallfiles', '--nojournal', '--noprealloc' ]; - - try { - if (!fs.existsSync(pathToGitIgnore)) { - // make sure the directory exists - mkdirp.sync(path.dirname(pathToGitIgnore)); - // make sure "local" directory is ignored by git - fs.writeFileSync(pathToGitIgnore, 'local'); - } - mkdirp.sync(pathToDB); - } catch (err) { + (mongoPort ? Promise.resolve(mongoPort) : findAvailablePort()).then(function(port){ + mongoPort = port; + mongoUrl = 'mongodb://127.0.0.1:' + mongoPort; + var mongoArgs = [ '--port', mongoPort, '--smallfiles', '--nojournal', '--noprealloc' ]; + + try { + if (!fs.existsSync(pathToGitIgnore)) { + // make sure the directory exists + mkdirp.sync(path.dirname(pathToGitIgnore)); + // make sure "local" directory is ignored by git + fs.writeFileSync(pathToGitIgnore, 'local'); + } + mkdirp.sync(pathToDB); + } catch (err) { - return reject(err); - } + return reject(err); + } - pathToDB && mongoArgs.push('--dbpath', path.resolve(pathToDB)); + pathToDB && mongoArgs.push('--dbpath', path.resolve(pathToDB)); - var mongoProcess = spawn(mongoPath || 'mongod', mongoArgs); + var mongoProcess = spawn(mongoPath || 'mongod', mongoArgs); - // use debounce to give the process some time in case it exits prematurely + // use debounce to give the process some time in case it exits prematurely - var numberOfRetries = 10; + var numberOfRetries = 10; - function connectWithRetry () { - if (numberOfRetries < 0) { - return reject(new Error('cannot connect to ' + mongoUrl + '... giving up')); - } - MongoClient.connect(mongoUrl + '/test', function (err, db) { - if (err) { - setTimeout(connectWithRetry, 100); - } else { - db.close(function () { - resolve(mongoUrl); - }); + function connectWithRetry () { + if (numberOfRetries < 0) { + return reject(new Error('cannot connect to ' + mongoUrl + '... giving up')); } + MongoClient.connect(mongoUrl + '/test', function (err, db) { + if (err) { + setTimeout(connectWithRetry, 100); + } else { + db.close(function () { + resolve(mongoUrl); + }); + } + }); + }; + + mongoProcess.stdout.on('data', debounce(tools.once(connectWithRetry), 1000)); + + // TODO: get rid of this log before the next release + mongoProcess.stderr.on('data', function (data) { + console.log(data.toString()); }); - }; - - mongoProcess.stdout.on('data', debounce(tools.once(connectWithRetry), 1000)); - // TODO: get rid of this log before the next release - mongoProcess.stderr.on('data', function (data) { - console.log(data.toString()); - }); - - // on premature exit, reject the promise - mongoProcess.on('exit', function (code) { - code && reject(new Error("mongo exited with code: " + code)); - }); + // on premature exit, reject the promise + mongoProcess.on('exit', function (code) { + code && reject(new Error("mongo exited with code: " + code)); + }); - // make sure mongoProcess is killed if the parent process exits - process.on('exit', function () { - mongoProcess.kill(); + // make sure mongoProcess is killed if the parent process exits + process.on('exit', function () { + mongoProcess.kill(); + }); }); - }); return myMongoServerPromises[pathToApp]; }; - diff --git a/lib/tools/closure.js b/lib/tools/closure.js index 88fb32a..f4ede21 100644 --- a/lib/tools/closure.js +++ b/lib/tools/closure.js @@ -15,11 +15,8 @@ function Closure (parent, listOfKeys, accessor) { listOfKeys = listOfKeys || []; accessor = accessor || function () {}; - parent = parent || {}; - - Object.keys(parent).forEach(function (key) { - closure[key] = parent[key]; - }); + + parent && parent.__mixin__ && parent.__mixin__(closure); listOfKeys.forEach(function (key) { closure[key] = accessor.bind(null, key); @@ -29,6 +26,12 @@ function Closure (parent, listOfKeys, accessor) { var values = {}; Object.keys(closure).forEach(function (key) { values[key] = closure[key](); + if (values[key] === undefined) { + values[key] = null; + } + if (typeof values[key] === 'function') { + throw new Error('a closure variable must be serializable, so you cannot use a function'); + } }); return values; } @@ -39,6 +42,12 @@ function Closure (parent, listOfKeys, accessor) { }); } + this.__mixin__ = function (object) { + Object.keys(closure).forEach(function (key) { + object[key] = closure[key]; + }); + } + } /** @@ -57,7 +66,7 @@ Closure.mixin = function (object) { object.useClosure = function (objectOrGetter) { if (typeof objectOrGetter !== 'function' && typeof objectOrGetter !== 'object') { - throw new Error('closure must be either function or object'); + throw new Error('closure must be either a function or an object'); } accessor = function (values) { var closure = (typeof objectOrGetter === 'function') ? objectOrGetter() : objectOrGetter; diff --git a/lib/tools/genericPromiseChain.js b/lib/tools/genericPromiseChain.js index a8f9146..611b9a9 100644 --- a/lib/tools/genericPromiseChain.js +++ b/lib/tools/genericPromiseChain.js @@ -122,17 +122,26 @@ module.exports = function genericPromiseChain(methods, myPrototype, options) { if (!operand || typeof operand !== 'object') { reject(new Error('GenericPromiseChain: invalid operand')); } + if (transform) { action = transform(action); } - action(operand, either(cleanError(reject)).or(resolve)); + + (function doAction(err, retryCount) { + var retryOnError = action.call({ lastError: err, retryCount: retryCount }, operand, either(function (err) { + if (retryOnError) { + doAction(err, retryCount + 1); + } else { + cleanError(reject)(err); + } + }).or(resolve)) + })(null, 0); + }); }); return self; }; - - methods.forEach(function (name) { "use strict"; diff --git a/lib/tools/index.js b/lib/tools/index.js index 67a616a..fc98ac9 100644 --- a/lib/tools/index.js +++ b/lib/tools/index.js @@ -107,20 +107,24 @@ module.exports = { return new Promise(function (resolve, reject) { var meteorite; - - try { - meteorite = spawn('mrt', [ 'install' ], { cwd: pathToApp }); - } catch (err) { - - return reject(err); - } - - meteorite.on('exit', module.exports.either(function (code) { - reject(new Error('Bad luck, meteorite exited with code: ' + code)); - }).or(resolve)); + var exec = require('child_process').exec; + exec('mrt --version', function (err) { + if (err) { + reject('meteorite not found! please install it with: npm install -g meteorite,' + + ' or make sure your project does not contain smart.json file'); + return; + } + try { + meteorite = spawn('mrt', [ 'install' ], { cwd: pathToApp }); + } catch (err) { + return reject(err); + } + meteorite.on('exit', module.exports.either(function (code) { + reject(new Error('Bad luck, meteorite exited with code: ' + code)); + }).or(resolve)); + }); // TODO: timeout - }); }, @@ -156,7 +160,7 @@ module.exports = { "use strict"; helpers = helpers || {}; - + if (moreHelpers) { if (!Array.isArray(moreHelpers)) { moreHelpers = [ moreHelpers ]; @@ -257,7 +261,7 @@ function parseRelease(release) { function initial(array) { "use strict"; - + return array.slice(0, array.length - 1); } @@ -267,5 +271,3 @@ function logError(context, error) { console.warn(chalk.inverse('[gagarin] ' + context)); console.warn(chalk.yellow(err.stack)); } - - diff --git a/package.js b/package.js index b0fbf79..2df5c1a 100644 --- a/package.js +++ b/package.js @@ -2,7 +2,7 @@ Package.describe({ summary: "Gagarin, a Meteor testing framework", name: "anti:gagarin", - version: "0.4.1", + version: "0.4.2", git: "https://github.com/anticoders/gagarin.git", }); diff --git a/package.json b/package.json index 6b991ae..964b751 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gagarin", - "version": "0.4.1", + "version": "0.4.2", "description": "another testing framework for your meteor apps", "main": "gagarin.js", "repository": { diff --git a/test.js b/test.js index 4ae3aa5..5462c2c 100755 --- a/test.js +++ b/test.js @@ -24,6 +24,8 @@ var gagarin = new Gagarin({ skipBuild : program.skipBuild, buildOnly : program.buildOnly, muteBuild : !program.verbose, + + startupTimeout : 5000, //verbose : program.verbose, verbose : true, }); diff --git a/tests/build_error/.meteor/versions b/tests/build_error/.meteor/versions index 21cb25d..cd1d120 100644 --- a/tests/build_error/.meteor/versions +++ b/tests/build_error/.meteor/versions @@ -1,4 +1,4 @@ -anti:gagarin@0.4.1 +anti:gagarin@0.4.2 application-configuration@1.0.4 autopublish@1.0.2 autoupdate@1.1.5 diff --git a/tests/example/.meteor/packages b/tests/example/.meteor/packages index 7985a64..68eb637 100644 --- a/tests/example/.meteor/packages +++ b/tests/example/.meteor/packages @@ -10,3 +10,4 @@ mystor:device-detection accounts-password accounts-base +iron:router diff --git a/tests/example/.meteor/versions b/tests/example/.meteor/versions index 550cf22..80f5361 100644 --- a/tests/example/.meteor/versions +++ b/tests/example/.meteor/versions @@ -1,6 +1,6 @@ accounts-base@1.1.3 accounts-password@1.0.6 -anti:gagarin@0.4.1 +anti:gagarin@0.4.2 application-configuration@1.0.4 autoupdate@1.1.5 base64@1.0.2 @@ -22,6 +22,14 @@ htmljs@1.0.3 http@1.0.10 id-map@1.0.2 insecure@1.0.2 +iron:controller@1.0.7 +iron:core@1.0.7 +iron:dynamic-template@1.0.7 +iron:layout@1.0.7 +iron:location@1.0.7 +iron:middleware-stack@1.0.7 +iron:router@1.0.7 +iron:url@1.0.7 jquery@1.11.3 json@1.0.2 launch-screen@1.0.1 diff --git a/tests/example/example.html b/tests/example/example.html index 98860cf..ef6e142 100644 --- a/tests/example/example.html +++ b/tests/example/example.html @@ -3,7 +3,6 @@ - {{> hello}} + + \ No newline at end of file diff --git a/tests/example/example.js b/tests/example/example.js index 6ae718c..73276b0 100644 --- a/tests/example/example.js +++ b/tests/example/example.js @@ -2,19 +2,25 @@ Fiber = null; Items = new Meteor.Collection('items'); reset = 0; +Router.route('/', function () { + this.render('hello'); +}); + +Router.route('/test', function () { + this.render('testRouteTemplate'); +}); + if (Meteor.isClient) { Session.set('counter', 0); Meteor.subscribe('items'); + Template.hello.helpers({ greeting: function () { return "Welcome to example."; - } - }); - - Template.hello.helpers({ + }, counter: function () { return Session.get('counter'); } @@ -23,6 +29,31 @@ if (Meteor.isClient) { Template.hello.events({ 'click input': function () { Session.set('counter', Session.get('counter') + 1); + }, + 'click #waitForDOM' : function () { + var waitForTestDiv = document.createElement('div'); + waitForTestDiv.id = 'waitForTestDiv'; + waitForTestDiv.innerHTML = 'I have been added.'; + document.body.appendChild(waitForTestDiv); + }, + 'click #waitUntilGone' : function () { + var parent = document.getElementById('waitUntilGone'); + var child = document.getElementById('removeChildTestDiv'); + parent.removeChild(child); + }, + 'click #waitUntilNotVisible' : function () { + var div = document.getElementById('waitUntilNotVisible'); + div.style.display = "none"; + }, + 'click #hideParentElement' : function () { + var div = document.getElementById('waitUntilNotVisibleParent'); + div.style.display = "none"; + }, + 'focus #focus' : function () { + document.getElementById('focus').value = 'Focused.'; + }, + 'blur #blur' : function() { + document.getElementById('blur').value = 'Blurred.'; } }); diff --git a/tests/example/public/test.js b/tests/example/public/test.js new file mode 100644 index 0000000..25ea6b3 --- /dev/null +++ b/tests/example/public/test.js @@ -0,0 +1 @@ +window.thisPropertyIsAddedByTestJS = {}; diff --git a/tests/specs/addScript.js b/tests/specs/addScript.js new file mode 100644 index 0000000..8c844f5 --- /dev/null +++ b/tests/specs/addScript.js @@ -0,0 +1,18 @@ +describe('Add script helper', function () { + + var server = meteor(); + var client = browser(server); + + it('should be able to use addScript helper', function () { + return client + .execute(function () { + return document.location.origin; + }) + .then(function (location) { + return client.addScript(location + '/test.js', function () { + return !!window.thisPropertyIsAddedByTestJS; + }); + }); + }); + +}); diff --git a/tests/specs/benchmark.js b/tests/specs/benchmark.js index 9fc541f..0b63a67 100644 --- a/tests/specs/benchmark.js +++ b/tests/specs/benchmark.js @@ -1,4 +1,6 @@ +var Promise = require('es6-promise').Promise; + describe('Benchmark test suite', function () { var server = meteor(); @@ -40,4 +42,21 @@ describe('Benchmark test suite', function () { }); }); + describe("Browser benchmark", function () { + var client = browser(server); + + this.timeout(15000); + + it('500x execute', function () { + var myPromise = client; + var i; + for (var i=0; i<500; i++) { + myPromise = myPromise.execute(function () { + return Meteor.relese; + }); + } + return myPromise; + }); + }); + }); diff --git a/tests/specs/closures.js b/tests/specs/closures.js index 9f38684..2280f33 100644 --- a/tests/specs/closures.js +++ b/tests/specs/closures.js @@ -7,9 +7,12 @@ describe('Closures', function () { var c = Math.random(); var d = Math.random(); - var zero = 0; + var zero = 0; + + var v_undefined = undefined; + var v_null = null; - closure(['a', 'b', 'c', 'd', 'zero'], function (expr) { return eval(expr); }); + closure(['a', 'b', 'c', 'd', 'zero', 'v_undefined', 'v_null'], function (expr) { return eval(expr); }); describe('Closure variables in server scripts', function () { @@ -23,6 +26,22 @@ describe('Closures', function () { }); }); + it('null variable should equal null', function () { + return server.execute(function () { + return v_null; + }).then(function (value) { + expect(value).to.equal(null); + }); + }); + + it('undefined variable should be translated to null variable', function () { + return server.execute(function () { + return v_undefined; + }).then(function (value) { + expect(value).to.equal(null); + }); + }); + }); describe('When using server.execute', function () { @@ -260,6 +279,22 @@ describe('Closures', function () { }); }); + it('null variable should equal null', function () { + return server.execute(function () { + return v_null; + }).then(function (value) { + expect(value).to.equal(null); + }); + }); + + it('undefined variable should be translated to null variable', function () { + return server.execute(function () { + return v_undefined; + }).then(function (value) { + expect(value).to.equal(null); + }); + }); + }); describe('When using client.execute', function () { @@ -477,4 +512,44 @@ describe('Closures', function () { }); + describe('Closure hierarchy', function () { + + var a2 = 1; + var b2 = 2; + var c2 = 3; + + closure(['a2', 'b2', 'c2'], function (expr) { return eval(expr); }); + + it('should be able to use the new variables', function () { + return server.execute(function () { + return a2 + b2 + c2; + }).then(function (value) { + expect(value).to.equal(6); + }); + }); + + it('should be able to access the parent variables as well', function () { + return server.execute(function () { + return a + b + c; + }).then(function (value) { + expect(value).to.equal(a + b + c); + }); + }); + + }); + + describe('Invalid closure variables', function () { + var f = function () {}; + + closure(['f'], function (expr) { return eval(expr); }); + + it('should reject an invalid value', function () { + return server.execute(function () { + return f; + }).expectError(function (err) { + expect(err.message).to.contain('cannot use a function'); + }); + }); + }); + }); diff --git a/tests/specs/exceptions.js b/tests/specs/exceptions.js index a1df12b..1e9f8e2 100644 --- a/tests/specs/exceptions.js +++ b/tests/specs/exceptions.js @@ -155,6 +155,46 @@ describe('Reporting Exceptions', function () { }); + describe('Given timeout for the first server output is exceeded', function(){ + var server = meteor({ + noAutoStart : true, + startupTimeout : 1, + }); + + it('should throw an error', function () { + return server + .init() + .expectError(function (err) { + message = err.message; + }); + }); + + it('the error should contain useful information', function () { + expect(message).to.contain("server output"); + }); + + }); + + describe('Given timeout for server startup is exceeded', function(){ + var server = meteor({ + noAutoStart : true, + startupTimeout2 : 1, + }); + + it('should throw an error', function () { + return server + .init() + .expectError(function (err) { + message = err.message; + }); + }); + + it('the error should contain useful information', function () { + expect(message).to.contain("server startup"); + }); + + }); + describe('Given the app is properly built,', function () { // SERVER SIDE ERRORS @@ -345,6 +385,34 @@ describe('Reporting Exceptions', function () { var message = ""; var client = browser(server2); + describe('Use strict', function () { + + it('should not allow introducing new global variables in client.execute', function () { + return client.execute(function () { + someNewVariable = true; + }).expectError(function (err) { + expect(err.message).to.include('someNewVariable'); + }); + }); + + it('should not allow introducing new global variables in client.promise', function () { + return client.promise(function (resolve) { + resolve(someNewVariable = true); + }).expectError(function (err) { + expect(err.message).to.include('someNewVariable'); + }); + }); + + it('should not allow introducing new global variables in client.wait', function () { + return client.wait(1000, '', function () { + return someNewVariable = true; + }).expectError(function (err) { + expect(err.message).to.include('someNewVariable'); + }); + }); + + }); + describe('If there is a syntax error in client-side injected script', function () { it('should be properly reported', function () { @@ -361,6 +429,24 @@ describe('Reporting Exceptions', function () { }); + describe.skip('If chai assertion fails in client-side injected script', function () { + + it('should be properly reported', function () { + return client + .execute(function () { + expect(true).to.be.false; + }) + .expectError(function (err) { + message = err.message; + }); + }); + + it('the error message should contain useful information', function () { + expect(message).to.contain('expected true to be false'); + }); + + }); + describe('If there is a syntax error in client-side promise', function () { it('should be properly reported', function () { @@ -431,7 +517,7 @@ describe('Reporting Exceptions', function () { }); describe('If the client-side wait fails due to some error', function () { - + it('should be properly reported', function () { return client .wait(1000, 'until error is thrown', function () { @@ -449,7 +535,7 @@ describe('Reporting Exceptions', function () { }); describe('If the client-side wait fails due to timeout', function () { - + it('should be properly reported', function () { return client .wait(100, 'until error is thrown', function () { diff --git a/tests/specs/flavor.js b/tests/specs/flavor.js index 0fd331a..e09fcea 100644 --- a/tests/specs/flavor.js +++ b/tests/specs/flavor.js @@ -90,7 +90,7 @@ describe('Flavor', function () { beforeEach(function() { beforeY = fiberServer.execute(function(beforeY) { return beforeY + 10; - }, beforeY); + }, [ beforeY ]); }); it('should get the value incremented by beforeEach', function() { @@ -107,7 +107,7 @@ describe('Flavor', function () { afterEach(function() { beforeA = fiberServer.execute(function(beforeA) { return beforeA + 10; - }, beforeA); + }, [ beforeA ]); }); it('should not get the value set by afterEach', function() { @@ -128,7 +128,7 @@ describe('Flavor', function () { after(function() { beforeB = fiberServer.execute(function(beforeB) { return beforeB + 10; - }, beforeB); + }, [ beforeB ]); }); it('should not get the value set by after', function() { diff --git a/tests/specs/helpers.js b/tests/specs/helpers.js index 085d41d..dd78a3e 100644 --- a/tests/specs/helpers.js +++ b/tests/specs/helpers.js @@ -2,7 +2,7 @@ var Promise = require('es6-promise').Promise; describe('Helpers', function () { - describe('Built in helpers', function () { + describe('Built in DOM helpers', function () { var server = meteor(); var client = browser(server); @@ -19,6 +19,352 @@ describe('Helpers', function () { .expectTextToContain('p', '1'); }); + it('waitForDOM should return true when dom object has been added', function () { + return client + .click('#waitForDOM') + .waitForDOM('#waitForTestDiv') + .then(function(res) { + expect(res).to.be.true; + }); + }); + + it('waitUntilGone should return true when dom object has been removed', function () { + return client + .click('#waitUntilGone') + .waitUntilGone('#removeChildTestDiv') + .then(function(res) { + expect(res).to.be.true; + }); + }); + + it('waitUntilNotVisible should return true when dom object is no longer visible', function () { + return client + .click('#waitUntilNotVisible') + .waitUntilNotVisible('#waitUntilNotVisible') + .then(function(res) { + expect(res).to.be.true; + }); + }); + + it('waitUntilNotVisible should return true when dom object ancestor no longer visible', function () { + return client + .click('#hideParentElement') + .waitUntilNotVisible('#visibleChild2') + .then(function(res) { + expect(res).to.be.true; + }); + }); + + it('getText should return innerHTML for a given selector', function () { + return client + .getText('#getText') + .then(function(res) { + expect(res).to.be.eql('

Get text.

'); + }); + }); + + it('getValue should return the value for a given selector', function () { + return client + .getValue('#getValue') + .then(function(res) { + expect(res).to.be.eql('Get value.'); + }); + }); + + it('getClass should return the classname for a given selector', function () { + return client + .getClass('#getClass') + .then(function(res) { + expect(res).to.be.eql('myClass'); + }); + }); + + it('getClass should return empty string if selector has no class', function () { + return client + .getClass('#noClass') + .then(function(res) { + expect(res).to.be.eql(''); + }); + }); + + it('setValue should set the value of an input', function () { + return client + .setValue('#setValue','test value') + .getValue('#setValue') + .then(function(res) { + expect(res).to.be.eql('test value'); + }); + }); + + it('focus should set focus on the given selector', function () { + return client + .focus('#focus') + .wait(1000,'until focused',function(){ + var element = document.querySelector('#focus'); + return element.value==='Focused.'; + }) + .then(function(res) { + expect(res).to.be.true; + }); + }); + + it('blur should set blur on the given selector', function () { + return client + .focus('#blur') + .blur('#blur') + .wait(1000,'until blurred',function(){ + var element = document.querySelector('#blur'); + return element.value==='Blurred.'; + }) + .then(function(res) { + expect(res).to.be.true; + }); + }); + + }); + + describe('waitForRoute', function () { + + var server = meteor(); + var client = browser(server); + + it('should change the route and load correct template', function () { + return client + .waitForRoute('/test') + .waitForDOM('#testRouteDiv') + .getText('#testRouteDiv') + .then(function(res) { + expect(res).to.contain('Testing Iron Router'); + }); + }); + + it('should be ok if routing to the current route', function () { + return client + .waitForRoute('/test') + .getText('#testRouteDiv') // we shouldn't need to waitForDOM here + .then(function(res) { + expect(res).to.contain('Testing Iron Router'); + }); + }); + + it('should be ok returning to previous route', function () { + return client + .waitForRoute('/') + .waitForDOM('#getText') + }); + + it('should work with a specified timeout', function () { + return client + .waitForRoute('/test',3000) + .waitForDOM('#testRouteDiv') + }); + + }); + + describe('Built in Accounts helpers', function () { + + var server = meteor(); + var client = browser(server); + + before(function () { + return server.execute(function () { + Accounts.createUser({email: 'existingUser@example.com',password: 'password'}); + }) + }); + + it('signUp should create a new user with email option', function () { + return client + .signUp({email: 'test@example.com',password: 'password'}) + .execute(function () { + return Meteor.users.findOne({'emails.address': 'test@example.com'}); + }) + .then(function(res) { + var email = res.emails[0].address; + expect(email).to.eql('test@example.com'); + }); + }); + + it('signUp should create a new user with username option', function () { + return client + .signUp({username: 'testName',password: 'password'}) + .execute(function () { + return Meteor.users.findOne({username: 'testName'}); + }) + .then(function(res) { + expect(res.username).to.eql('testName'); + }); + }); + + it('login should login existing user', function () { + return client + .login('existingUser@example.com','password') + .execute(function () { + return Meteor.user(); + }) + .then(function(res) { + var email = res.emails[0].address; + expect(email).to.eql('existingUser@example.com'); + }); + }); + + it('logout should logout logged in user', function () { + return client + .logout() + .execute(function () { + return Meteor.user(); + }) + .then(function(res) { + expect(res).to.be.null; + }); + }); + + + }); + + // TODO test afterFlush + describe.skip('Built in Tracker helpers', function () { + var server = meteor(); + var client = browser(server); + it('afterFlush should schedule function for next flush', function () { + return client + .afterFlush() + }); + }); + + describe('Built in server connections helpers', function () { + var server = meteor(); + var client = browser(server); + + it('start in connected state.', function () { + return client + .wait(3000,'until connected',function(){ + return Meteor.status().connected===true; + }) + }); + + it('disconnect client from the server.', function () { + return client + .disconnect() + .execute(function () { + return Meteor.status(); + }) + .then(function(res) { + expect(res.connected).to.be.false; + }) + }); + + it('reconnect client to the server.', function () { + return client + .reconnect() + .execute(function () { + return Meteor.status(); + }) + .then(function(res) { + expect(res.status).to.eql('connected'); + expect(res.connected).to.be.true; + }) + }); + + }); + + //TODO where to put screenshot file for this test ? + describe.skip('screenshot', function () { + var server = meteor(); + var client = browser(server); + + it('should save a screenshot to file.', function () { + return client + .screenshot() + .then(function(res) { + //assert file exists and is named by today's date and has some bytes + }) + }); + }); + + describe('helper assertions', function () { + var server = meteor(); + var client = browser(server); + + it('assertion helpers should work', function () { + return client + .expectExist('#visibleChild') + .expectNotExist('#noExist') + .expectVisible('#visibleChild') + .expectNotVisible('#hiddenChild') + .expectValueToEqual('#getValue','Get value.') + .expectTextToEqual('#getText','

Get text.

') + .expectTextToContain('#getText','Get tex') + .expectToHaveClass('#getClass','myClass') + }); + + describe('checkIfExist', function () { + it('should return true if element exists', function () { + return client + .checkIfExist('#visibleElement') + .then(function(res) { + expect(res).to.be.true; + }) + }); + + it('should return false if element does not exist', function () { + return client + .checkIfExist('#noExist') + .then(function(res) { + expect(res).to.be.false; + }) + }); + }); + + describe('checkIfVisible ', function () { + it('should return true if element is visible', function () { + return client + .checkIfVisible('#visibleElement') + .then(function(res) { + expect(res).to.be.true; + }) + }); + + it('should return true if element and ancestors are visible', function () { + return client + .checkIfVisible('#visibleChild') + .then(function(res) { + expect(res).to.be.true; + }) + }); + + it('should return false if element does not exist', function () { + return client + .checkIfVisible('#noExist') + .then(function(res) { + expect(res).to.be.false; + }) + }); + + it('should return false if element is not visible', function () { + return client + .checkIfVisible('#hiddenElement') + .then(function(res) { + expect(res).to.be.false; + }) + }); + + it('should return false if any ancestors are not visible', function () { + return client + .checkIfVisible('#hiddenChild') + .then(function(res) { + expect(res).to.be.false; + }) + }); + + it('should work for fixed and absolute position elements', function () { + return client + .expectVisible('#fixedPositionDiv') + .expectVisible('#absolutePositionDiv') + .expectNotVisible('#fixedPositionDivHidden') + .expectNotVisible('#absolutePositionDivHidden') + }); + + }); }); describe('Custom user-defined helpers', function () {