From 0bbfd2b6d38392938781d846ad37b5a0fd964004 Mon Sep 17 00:00:00 2001 From: Hank Duan Date: Wed, 26 Nov 2014 13:20:37 -0800 Subject: [PATCH] feat(protractor/runner): allow multiple browser in test --- lib/driverProviders/README.md | 10 +- lib/driverProviders/direct.js | 39 ++---- lib/driverProviders/driverProvider.js | 55 +++++++++ lib/driverProviders/hosted.js | 39 +----- lib/driverProviders/local.js | 36 ++---- lib/driverProviders/mock.js | 35 +++--- lib/driverProviders/sauce.js | 66 +++------- lib/protractor.js | 30 ++--- lib/runner.js | 150 ++++++++++++++--------- scripts/test.js | 4 +- spec/directConnect/directconnect_spec.js | 14 +++ spec/directConnectConf.js | 19 +++ spec/interaction/interaction_spec.js | 137 +++++++++++++++++++++ spec/interactionConf.js | 15 +++ testapp/alt_root_index.html | 1 + testapp/app.css | 9 ++ testapp/app.js | 1 + testapp/index.html | 2 + testapp/interaction/interaction.html | 17 +++ testapp/interaction/interaction.js | 43 +++++++ testapp/scripts/web-server.js | 21 ++++ 21 files changed, 504 insertions(+), 239 deletions(-) create mode 100644 lib/driverProviders/driverProvider.js create mode 100644 spec/directConnect/directconnect_spec.js create mode 100644 spec/directConnectConf.js create mode 100644 spec/interaction/interaction_spec.js create mode 100644 spec/interactionConf.js create mode 100644 testapp/interaction/interaction.html create mode 100644 testapp/interaction/interaction.js diff --git a/lib/driverProviders/README.md b/lib/driverProviders/README.md index 9ed964c18..c77b369d5 100644 --- a/lib/driverProviders/README.md +++ b/lib/driverProviders/README.md @@ -14,9 +14,9 @@ Each file exports a function which takes in the configuration as a parameter and DriverProvider.prototype.setupEnv /** - * @return {webdriver.WebDriver} The setup driver instance. + * @return {webdriver.WebDriver} A new setup driver instance. */ -DriverProvider.prototype.getDriver +DriverProvider.prototype.getNewDriver /** * @return {q.promise} A promise which will resolve when the environment @@ -37,6 +37,10 @@ DriverProvider.prototype.updateJob Requirements ------------ - - `setupEnv` and `getDriver` will be called before the test framework is loaded, so any pre-work which might cause timeouts on the first test should be done there. + - `setupEnv` will be called before the test framework is loaded, so any + pre-work which might cause timeouts on the first test should be done there. + `getNewDriver` will be called once right after `setupEnv` to generate the + initial driver, and possibly during the middle of the test if users request + additional browsers. - `teardownEnv` should call the driver's `quit` method. diff --git a/lib/driverProviders/direct.js b/lib/driverProviders/direct.js index 41c15defb..a0552a9bd 100644 --- a/lib/driverProviders/direct.js +++ b/lib/driverProviders/direct.js @@ -9,12 +9,14 @@ var webdriver = require('selenium-webdriver'), firefox = require('selenium-webdriver/firefox'), q = require('q'), fs = require('fs'), - path = require('path'); + path = require('path'), + util = require('util'), + DriverProvider = require('./driverProvider'); var DirectDriverProvider = function(config) { - this.config_ = config; - this.driver_ = null; + DriverProvider.call(this, config); }; +util.inherits(DirectDriverProvider, DriverProvider); /** * Configure and launch (if applicable) the object's environment. @@ -38,30 +40,14 @@ DirectDriverProvider.prototype.setupEnv = function() { }; /** - * Teardown and destroy the environment and do any associated cleanup. - * Shuts down the driver. + * Create a new driver. * * @public - * @return {q.promise} A promise which will resolve when the environment - * is down. - */ -DirectDriverProvider.prototype.teardownEnv = function() { - var deferred = q.defer(); - this.driver_.quit().then(function() { - deferred.resolve(); - }); - return deferred.promise; -}; - -/** - * Retrieve the webdriver for the runner. - * @public + * @override * @return webdriver instance */ -DirectDriverProvider.prototype.getDriver = function() { - if (this.driver_) { - return this.driver_; - } +DirectDriverProvider.prototype.getNewDriver = function() { + var driver; switch (this.config_.capabilities.browserName) { case 'chrome': var chromeDriverFile = this.config_.chromeDriver || @@ -78,20 +64,21 @@ DirectDriverProvider.prototype.getDriver = function() { } var service = new chrome.ServiceBuilder(chromeDriverFile).build(); - this.driver_ = chrome.createDriver( + driver = chrome.createDriver( new webdriver.Capabilities(this.config_.capabilities), service); break; case 'firefox': if (this.config_.firefoxPath) { this.config_.capabilities.firefox_binary = this.config_.firefoxPath; } - this.driver_ = new firefox.Driver(this.config_.capabilities); + driver = new firefox.Driver(this.config_.capabilities); break; default: throw new Error('browserName ' + this.config_.capabilities.browserName + 'is not supported with directConnect.'); } - return this.driver_; + this.drivers_.push(driver); + return driver; }; // new instance w/ each include diff --git a/lib/driverProviders/driverProvider.js b/lib/driverProviders/driverProvider.js new file mode 100644 index 000000000..94dbd805a --- /dev/null +++ b/lib/driverProviders/driverProvider.js @@ -0,0 +1,55 @@ +/** + * This is a base driver provider class. + * It is responsible for setting up the account object, tearing + * it down, and setting up the driver correctly. + */ + +var webdriver = require('selenium-webdriver'), + q = require('q'); + +var DriverProvider = function(config) { + this.config_ = config; + this.drivers_ = []; +}; + +/** + * Teardown and destroy the environment and do any associated cleanup. + * Shuts down the drivers. + * + * @public + * @return {q.promise} A promise which will resolve when the environment + * is down. + */ +DriverProvider.prototype.teardownEnv = function() { + var deferredArray = this.drivers_.map(function(driver) { + var deferred = q.defer(); + driver.getSession().then(function(session_) { + if (session_) { + driver.quit().then(function() { + deferred.resolve(); + }); + } else { + deferred.resolve(); + } + }); + return deferred.promise; + }); + return q.all(deferredArray); +}; + +/** + * Create a new driver. + * + * @public + * @return webdriver instance + */ +DriverProvider.prototype.getNewDriver = function() { + var newDriver = new webdriver.Builder(). + usingServer(this.config_.seleniumAddress). + withCapabilities(this.config_.capabilities). + build(); + this.drivers_.push(newDriver); + return newDriver; +}; + +module.exports = DriverProvider; diff --git a/lib/driverProviders/hosted.js b/lib/driverProviders/hosted.js index b2290503c..12c490abc 100644 --- a/lib/driverProviders/hosted.js +++ b/lib/driverProviders/hosted.js @@ -5,13 +5,13 @@ */ var util = require('util'), - webdriver = require('selenium-webdriver'), - q = require('q'); + q = require('q'), + DriverProvider = require('./driverProvider'); var HostedDriverProvider = function(config) { - this.config_ = config; - this.driver_ = null; + DriverProvider.call(this, config); }; +util.inherits(HostedDriverProvider, DriverProvider); /** * Configure and launch (if applicable) the object's environment. @@ -34,37 +34,6 @@ HostedDriverProvider.prototype.setupEnv = function() { } }; -/** - * Teardown and destroy the environment and do any associated cleanup. - * Shuts down the driver. - * - * @public - * @return {q.promise} A promise which will resolve when the environment - * is down. - */ -HostedDriverProvider.prototype.teardownEnv = function() { - var deferred = q.defer(); - this.driver_.quit().then(function() { - deferred.resolve(); - }); - return deferred.promise; -}; - -/** - * Return the webdriver for the runner. - * @public - * @return webdriver instance - */ -HostedDriverProvider.prototype.getDriver = function() { - if (!this.driver_) { - this.driver_ = new webdriver.Builder(). - usingServer(this.config_.seleniumAddress). - withCapabilities(this.config_.capabilities). - build(); - } - return this.driver_; -}; - // new instance w/ each include module.exports = function(config) { return new HostedDriverProvider(config); diff --git a/lib/driverProviders/local.js b/lib/driverProviders/local.js index 437d825a5..3830c9c21 100644 --- a/lib/driverProviders/local.js +++ b/lib/driverProviders/local.js @@ -7,17 +7,17 @@ * so that we only start the local selenium once per entire launch. */ var util = require('util'), - webdriver = require('selenium-webdriver'), path = require('path'), remote = require('selenium-webdriver/remote'), fs = require('fs'), - q = require('q'); + q = require('q'), + DriverProvider = require('./driverProvider'); var LocalDriverProvider = function(config) { - this.config_ = config; - this.driver_ = null; + DriverProvider.call(this, config); this.server_ = null; }; +util.inherits(LocalDriverProvider, DriverProvider); /** @@ -88,41 +88,25 @@ LocalDriverProvider.prototype.setupEnv = function() { /** * Teardown and destroy the environment and do any associated cleanup. - * Shuts down the driver. + * Shuts down the drivers and server. * * @public + * @override * @return {q.promise} A promise which will resolve when the environment * is down. */ LocalDriverProvider.prototype.teardownEnv = function() { - var deferred = q.defer(); var self = this; - - util.puts('Shutting down selenium standalone server.'); - self.driver_.quit().then(function() { - return self.server_.stop().then(function() { + var deferred = q.defer(); + DriverProvider.prototype.teardownEnv.call(this).then(function() { + util.puts('Shutting down selenium standalone server.'); + self.server_.stop().then(function() { deferred.resolve(); }); }); - return deferred.promise; }; -/** - * Retrieve the webdriver for the runner. - * @public - * @return webdriver instance - */ -LocalDriverProvider.prototype.getDriver = function() { - if (!this.driver_) { - this.driver_ = new webdriver.Builder(). - usingServer(this.config_.seleniumAddress). - withCapabilities(this.config_.capabilities). - build(); - } - return this.driver_; -}; - // new instance w/ each include module.exports = function(config) { return new LocalDriverProvider(config); diff --git a/lib/driverProviders/mock.js b/lib/driverProviders/mock.js index e9306fa71..06054d872 100644 --- a/lib/driverProviders/mock.js +++ b/lib/driverProviders/mock.js @@ -4,12 +4,14 @@ * server. */ var webdriver = require('selenium-webdriver'), - q = require('q'); + util = require('util'), + q = require('q'), + DriverProvider = require('./driverProvider'); /** * @constructor */ var MockExecutor = function() { - this.driver_ = null; + this.drivers_ = []; }; /** @@ -25,8 +27,10 @@ MockExecutor.prototype.execute = function(command, callback) { }; var MockDriverProvider = function(config) { - this.config_ = config; + DriverProvider.call(this, config); }; +util.inherits(MockDriverProvider, DriverProvider); + /** * Configure and launch (if applicable) the object's environment. @@ -37,30 +41,19 @@ MockDriverProvider.prototype.setupEnv = function() { return q.fcall(function() {}); }; -/** - * Teardown and destroy the environment and do any associated cleanup. - * - * @public - * @return {q.promise} A promise which will resolve immediately. - */ -MockDriverProvider.prototype.teardownEnv = function() { - var deferred = q.defer(); - this.driver_.quit().then(function() { - deferred.resolve(); - }); - return deferred.promise; -}; /** - * Retrieve the webdriver for the runner. + * Create a new driver. + * * @public + * @override * @return webdriver instance */ -MockDriverProvider.prototype.getDriver = function() { +MockDriverProvider.prototype.getNewDriver = function() { var mockSession = new webdriver.Session('test_session_id', {}); - - this.driver_ = new webdriver.WebDriver(mockSession, new MockExecutor()); - return this.driver_; + var newDriver = new webdriver.WebDriver(mockSession, new MockExecutor()); + this.drivers_.push(newDriver); + return newDriver; }; // new instance w/ each include diff --git a/lib/driverProviders/sauce.js b/lib/driverProviders/sauce.js index 1dd074e5e..6c7168f2d 100644 --- a/lib/driverProviders/sauce.js +++ b/lib/driverProviders/sauce.js @@ -5,16 +5,16 @@ */ var util = require('util'), - webdriver = require('selenium-webdriver'), SauceLabs = require('saucelabs'), - q = require('q'); + q = require('q'), + DriverProvider = require('./driverProvider'); var SauceDriverProvider = function(config) { - this.config_ = config; + DriverProvider.call(this, config); this.sauceServer_ = {}; - this.driver_ = null; }; +util.inherits(SauceDriverProvider, DriverProvider); /** @@ -24,21 +24,25 @@ var SauceDriverProvider = function(config) { * @return {q.promise} A promise that will resolve when the update is complete. */ SauceDriverProvider.prototype.updateJob = function(update) { - var deferred = q.defer(); + var self = this; - this.driver_.getSession().then(function(session) { - console.log('SauceLabs results available at http://saucelabs.com/jobs/' + - session.getId()); - self.sauceServer_.updateJob(session.getId(), update, function(err) { - if (err) { - throw new Error( - 'Error updating Sauce pass/fail status: ' + util.inspect(err) - ); - } - deferred.resolve(); + var deferredArray = this.drivers_.map(function(driver) { + var deferred = q.defer(); + driver.getSession().then(function(session) { + console.log('SauceLabs results available at http://saucelabs.com/jobs/' + + session.getId()); + self.sauceServer_.updateJob(session.getId(), update, function(err) { + if (err) { + throw new Error( + 'Error updating Sauce pass/fail status: ' + util.inspect(err) + ); + } + deferred.resolve(); + }); }); + return deferred.promise; }); - return deferred.promise; + return q.all(deferredArray); }; /** @@ -74,36 +78,6 @@ SauceDriverProvider.prototype.setupEnv = function() { return deferred.promise; }; -/** - * Teardown and destroy the environment and do any associated cleanup. - * Shuts down the driver. - * - * @public - * @return {q.promise} A promise which will resolve when the environment - * is down. - */ -SauceDriverProvider.prototype.teardownEnv = function() { - var deferred = q.defer(); - this.driver_.quit().then(function() { - deferred.resolve(); - }); - return deferred.promise; -}; - -/** - * Retrieve the webdriver for the runner. - * @public - * @return webdriver instance - */ -SauceDriverProvider.prototype.getDriver = function() { - if (!this.driver_) { - this.driver_ = new webdriver.Builder(). - usingServer(this.config_.seleniumAddress). - withCapabilities(this.config_.capabilities).build(); - } - return this.driver_; -}; - // new instance w/ each include module.exports = function(config) { return new SauceDriverProvider(config); diff --git a/lib/protractor.js b/lib/protractor.js index ab2872344..7c40e4ae3 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -1085,6 +1085,15 @@ var Protractor = function(webdriverInstance, opt_baseUrl, opt_rootElement) { * @type {string} */ this.resetUrl = DEFAULT_RESET_URL; + this.driver.getCapabilities().then(function(caps) { + // Internet Explorer does not accept data URLs, which are the default + // reset URL for Protractor. + // Safari accepts data urls, but SafariDriver fails after one is used. + var browserName = caps.get('browserName'); + if (browserName === 'internet explorer' || browserName === 'safari') { + self.resetUrl = 'about:blank'; + } + }); /** * Information about mock modules that will be installed during every @@ -1530,24 +1539,3 @@ Protractor.prototype.pause = function(opt_debugPort) { exports.wrapDriver = function(webdriver, opt_baseUrl, opt_rootElement) { return new Protractor(webdriver, opt_baseUrl, opt_rootElement); }; - -/** - * @type {Protractor} - */ -var instance; - -/** - * Set a singleton instance of protractor. - * @param {Protractor} ptor - */ -exports.setInstance = function(ptor) { - instance = ptor; -}; - -/** - * Get the singleton instance. - * @return {Protractor} - */ -exports.getInstance = function() { - return instance; -}; diff --git a/lib/runner.js b/lib/runner.js index 1628c38c4..0415c888c 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -22,6 +22,7 @@ var Runner = function(config) { this.preparer_ = null; this.driverprovider_ = null; this.config_ = config; + this.drivers_ = []; if (config.v8Debug) { // Call this private function instead of sending SIGUSR1 because Windows. @@ -144,29 +145,7 @@ Runner.prototype.controlFlow = function() { * Sets up convenience globals for test specs * @private */ -Runner.prototype.setupGlobals_ = function(driver) { - var browser = protractor.wrapDriver( - driver, - this.config_.baseUrl, - this.config_.rootElement); - - browser.params = this.config_.params; - protractor.setInstance(browser); - - driver.getCapabilities().then(function(caps) { - // Internet Explorer does not accept data URLs, which are the default - // reset URL for Protractor. - // Safari accepts data urls, but SafariDriver fails after one is used. - var browserName = caps.get('browserName'); - if (browserName === 'internet explorer' || browserName === 'safari') { - browser.resetUrl = 'about:blank'; - } - }); - - if (this.config_.getPageTimeout) { - browser.getPageTimeout = this.config_.getPageTimeout; - } - +Runner.prototype.setupGlobals_ = function(browser) { // Export protractor to the global namespace to be used in tests. global.protractor = protractor; global.browser = browser; @@ -182,6 +161,75 @@ Runner.prototype.setupGlobals_ = function(driver) { global.DartObject = function(o) { this.o = o; }; }; + +/** + * Create a new driver from a driverProvider. Then set up a + * new protractor instance using this driver. + * This is used to set up the initial protractor instances and any + * future ones. + * + * @return {Protractor} a protractor instance. + * @public + */ +Runner.prototype.createBrowser = function() { + var config = this.config_; + var driver = this.driverprovider_.getNewDriver(); + this.drivers_.push(driver); + driver.manage().timeouts().setScriptTimeout(config.allScriptsTimeout); + var browser = protractor.wrapDriver(driver, + config.baseUrl, config.rootElement); + browser.params = config.params; + if (config.getPageTimeout) { + browser.getPageTimeout = config.getPageTimeout; + } + var self = this; + + /** + * Fork another instance of protractor for use in interactive tests. + * + * @param {boolean} opt_useSameUrl Whether to navigate to current url on creation + * @param {boolean} opt_copyMockModules Whether to apply same mock modules on creation + * @return {Protractor} a protractor instance. + */ + browser.forkNewDriverInstance = function(opt_useSameUrl, opt_copyMockModules) { + var newBrowser = self.createBrowser(); + if (opt_copyMockModules) { + newBrowser.mockModules_ = browser.mockModules_; + } + if (opt_useSameUrl) { + browser.driver.getCurrentUrl().then(function(url) { + newBrowser.get(url); + }); + } + return newBrowser; + }; + return browser; +}; + +/** + * Final cleanup on exiting the runner. + * + * @return {q.Promise} A promise which resolves on finish. + * @private + */ +Runner.prototype.shutdown_ = function() { + var deferredArr = this.drivers_.map(function(driver) { + var deferred = q.defer(); + driver.getSession().then(function(session_) { + if (session_) { + driver.quit().then(function() { + deferred.resolve(); + }); + } else { + deferred.resolve(); + } + }); + return deferred.promise; + }); + return q.all(deferredArr); +}; + + /** * The primary workhorse interface. Kicks off the test running process. * @@ -190,50 +238,30 @@ Runner.prototype.setupGlobals_ = function(driver) { */ Runner.prototype.run = function() { var self = this, - driver, - specs, testPassed, plugins; - specs = this.config_.specs; - plugins = new Plugins(this.config_); - - if (!specs.length) { + if (!this.config_.specs.length) { throw new Error('Spec patterns did not match any files.'); } - var gracefulShutdown = function(driver) { - if (driver) { - return driver.getSession().then(function(session_) { - if (session_) { - return driver.quit(); - } - }); - } - }; - // 1) Setup environment //noinspection JSValidateTypes return this.driverprovider_.setupEnv().then(function() { - return q.all( - [self.config_.capabilities, self.config_.multiCapabilities]). - spread(function(capabilites, multiCapabilities) { - self.config_.capabilities = capabilites; - self.config_.multiCapabilities = multiCapabilities; - }).then(function() { - driver = self.driverprovider_.getDriver(); - }); - // 2) Webdriver could schedule this out of order if not in separate 'then' - // See https://github.com/angular/protractor/issues/1385 + // Resolve capabilities first, so it can be a promise + return q(self.config_.capabilities).then(function(capabilities) { + self.config_.capabilities = capabilities; + }); + // 2) Create a browser and setup globals }).then(function() { - return driver.manage().timeouts() - .setScriptTimeout(self.config_.allScriptsTimeout); - // 3) Setup globals and plugins + var browser = self.createBrowser(); + self.setupGlobals_(browser); + // 3) Setup plugins }).then(function() { - self.setupGlobals_.bind(self)(driver); + plugins = new Plugins(self.config_); return plugins.setup(); // 4) Execute test cases - }).then(function(setupResults) { + }).then(function(pluginSetupResults) { // Do the framework setup here so that jasmine and mocha globals are // available to the onPrepare function. var frameworkPath = ''; @@ -249,14 +277,16 @@ Runner.prototype.run = function() { throw new Error('config.framework (' + self.config_.framework + ') is not a valid framework.'); } - return require(frameworkPath).run(self, specs).then(function(testResults) { - return helper.joinTestLogs(setupResults, testResults); - }); + + return require(frameworkPath).run(self, self.config_.specs). + then(function(testResults) { + return helper.joinTestLogs(pluginSetupResults, testResults); + }); // 5) Teardown plugins }).then(function(testResults) { var deferred = q.defer(); - plugins.teardown().then(function(teardownResults) { - deferred.resolve(helper.joinTestLogs(testResults, teardownResults)); + plugins.teardown().then(function(pluginTeardownResults) { + deferred.resolve(helper.joinTestLogs(testResults, pluginTeardownResults)); }, function(error) { deferred.reject(error); }); @@ -279,7 +309,7 @@ Runner.prototype.run = function() { var exitCode = testPassed ? 0 : 1; return self.exit_(exitCode); }).fin(function() { - return gracefulShutdown(driver); + return self.shutdown_(); }); }; diff --git a/scripts/test.js b/scripts/test.js index 274e739ef..901822bda 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -22,7 +22,9 @@ var passingTests = [ 'node lib/cli.js spec/suitesConf.js --suite okmany,okspec', 'node lib/cli.js spec/pluginsBasicConf.js', 'node lib/cli.js spec/pluginsFullConf.js', - 'node lib/cli.js spec/ngHintSuccessConfig.js' + 'node lib/cli.js spec/ngHintSuccessConfig.js', + 'node lib/cli.js spec/interactionConf.js', + 'node lib/cli.js spec/directConnectConf.js' ]; passingTests.push( diff --git a/spec/directConnect/directconnect_spec.js b/spec/directConnect/directconnect_spec.js new file mode 100644 index 000000000..9ea93849b --- /dev/null +++ b/spec/directConnect/directconnect_spec.js @@ -0,0 +1,14 @@ +describe('direct connect', function() { + it('should instantiate and run', function() { + var usernameInput = element(by.model('username')); + var name = element(by.binding('username')); + + browser.get('index.html#/form'); + + expect(name.getText()).toEqual('Anon'); + + usernameInput.clear(); + usernameInput.sendKeys('Jane'); + expect(name.getText()).toEqual('Jane'); + }); +}); diff --git a/spec/directConnectConf.js b/spec/directConnectConf.js new file mode 100644 index 000000000..b11aca79b --- /dev/null +++ b/spec/directConnectConf.js @@ -0,0 +1,19 @@ +var env = require('./environment.js'); + +// A configuration file running a simple direct connect spec +exports.config = { + directConnect: true, + + capabilities: { + 'browserName': 'chrome' + }, + + baseUrl: env.baseUrl, + + specs: ['directConnect/*_spec.js'], + + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000 + } +}; diff --git a/spec/interaction/interaction_spec.js b/spec/interaction/interaction_spec.js new file mode 100644 index 000000000..9b94321e7 --- /dev/null +++ b/spec/interaction/interaction_spec.js @@ -0,0 +1,137 @@ +var util = require('util'); + +describe('Browser', function() { + + var newBrowser; + + afterEach(function(done) { + // Calling quit will remove the browser. + // You can choose to not quit the browser, and protractor will quit all of + // them for you when it exits (i.e. if you need a static number of browsers + // throughout all of your tests). However, I'm forking browsers in my tests + // and don't want to pile up my browser count. + if (newBrowser) { + newBrowser.quit().then(function() { + done(); + }); + } else { + done(); + } + }); + + it('should be able to fork', function() { + browser.get('index.html'); + newBrowser = browser.forkNewDriverInstance(); + expect(newBrowser).not.toEqual(browser); + expect(newBrowser.driver).not.toEqual(browser.driver); + expect(newBrowser.driver.getCurrentUrl()).toEqual('data:,'); + }); + + it('should be able to navigate to same url on fork', function() { + browser.get('index.html'); + newBrowser = browser.forkNewDriverInstance(true); + expect(newBrowser.driver.getCurrentUrl()). + toMatch('index.html#/form'); + }); + + it('should be able to copy mock modules on fork', function() { + var mockModule = function() { + var newModule = angular.module('mockModule', []); + newModule.value('version', '2'); + }; + + browser.addMockModule('mockModule', mockModule); + browser.get('index.html'); + + newBrowser = browser.forkNewDriverInstance(true, true); + expect(newBrowser.element(by.css('[app-version]')).getText()).toEqual('2'); + }); + + + describe('Multiple browsers', function() { + + var Person = function(name, browser) { + var $ = browser.$; + var $$ = browser.$$; + var element = browser.element; + + this.openApp = function() { + browser.get('index.html#/interaction'); + }; + + this.login = function() { + element(by.model('userInput')).sendKeys(name); + $('#sendUser').click(); + }; + + this.clearMessages = function() { + $('#clearMessages').click(); + }; + + this.sendMessage = function(msg) { + element(by.model('message')).sendKeys(msg); + $('#sendMessage').click(); + }; + + this.getMessages = function() { + return element.all(by.repeater("msg in messages track by $index")); + }; + }; + + var p0, p1; + + beforeEach(function() { + // default browser. + p0 = new Person('p0', browser); + p0.openApp(); + p0.login(); + p0.clearMessages(); + + // Any additional browsers can be instantiated via browser.forkNewDriverInstance(). + newBrowser = browser.forkNewDriverInstance(true); + p1 = new Person('p1', newBrowser); + p1.openApp(); + p1.login(); + }); + + it('should be able to interact', function() { + expect(p0.getMessages().count()).toEqual(0); + + p0.sendMessage('p0'); + browser.sleep(100); // The app polls every 100ms for updates. + expect(p0.getMessages().count()).toEqual(1); + expect(p1.getMessages().count()).toEqual(1); + + p1.sendMessage('p1'); + browser.sleep(100); // The app polls every 100ms for updates. + expect(p0.getMessages().count()).toEqual(2); + expect(p1.getMessages().count()).toEqual(2); + }); + + it('should perform actions in sync', function() { + var ACTIONS = 10; + expect(p0.getMessages().count()).toEqual(0); + + var expectedMessages = []; + var i; + for (i = 0; i < ACTIONS; ++i) { + p0.sendMessage(i); + expectedMessages.push('p0: ' + i); + } + for (i = 0; i < ACTIONS; ++i) { + p1.sendMessage(i); + expectedMessages.push('p1: ' + i); + } + for (i = 0; i < ACTIONS; ++i) { + p0.sendMessage(i); + p1.sendMessage(i); + expectedMessages.push('p0: ' + i); + expectedMessages.push('p1: ' + i); + } + + browser.sleep(100); // The app polls every 100ms for updates. + expect(p0.getMessages().getText()).toEqual(expectedMessages); + expect(p1.getMessages().getText()).toEqual(expectedMessages); + }); + }); +}); diff --git a/spec/interactionConf.js b/spec/interactionConf.js new file mode 100644 index 000000000..2ffe9cb04 --- /dev/null +++ b/spec/interactionConf.js @@ -0,0 +1,15 @@ +var env = require('./environment.js'); + +// Test having two browsers interacting with each other. +exports.config = { + seleniumAddress: env.seleniumAddress, + + // Spec patterns are relative to this directory. + specs: [ + 'interaction/*_spec.js' + ], + + capabilities: env.capabilities, + + baseUrl: env.baseUrl +}; diff --git a/testapp/alt_root_index.html b/testapp/alt_root_index.html index 79a796a41..38c4ece8c 100644 --- a/testapp/alt_root_index.html +++ b/testapp/alt_root_index.html @@ -38,6 +38,7 @@ + diff --git a/testapp/app.css b/testapp/app.css index 93ef2c72f..1f4a15414 100644 --- a/testapp/app.css +++ b/testapp/app.css @@ -36,3 +36,12 @@ .ng-binding { border: 1px solid rgba(50, 200, 50, .8); } + +#chat-box { + width: 300px; + height: 200px; + padding: 25px; + border: 2px solid; + margin: 25px; + overflow: scroll; +} diff --git a/testapp/app.js b/testapp/app.js index 8b76c0f68..91084379c 100644 --- a/testapp/app.js +++ b/testapp/app.js @@ -11,6 +11,7 @@ angular.module('myApp', ['ngAnimate', 'ngRoute', 'myApp.appVersion']). $routeProvider.when('/conflict', {templateUrl: 'conflict/conflict.html', controller: ConflictCtrl}); $routeProvider.when('/polling', {templateUrl: 'polling/polling.html', controller: PollingCtrl}); $routeProvider.when('/animation', {templateUrl: 'animation/animation.html', controller: AnimationCtrl}); + $routeProvider.when('/interaction', {templateUrl: 'interaction/interaction.html', controller: InteractionCtrl}); $routeProvider.when('/slowloader', { templateUrl: 'polling/polling.html', controller: PollingCtrl, diff --git a/testapp/index.html b/testapp/index.html index 77deaa9ad..2359d6fa9 100644 --- a/testapp/index.html +++ b/testapp/index.html @@ -15,6 +15,7 @@
  • conflict
  • polling
  • animation
  • +
  • interaction
  • @@ -33,6 +34,7 @@ + diff --git a/testapp/interaction/interaction.html b/testapp/interaction/interaction.html new file mode 100644 index 000000000..537ec6ac5 --- /dev/null +++ b/testapp/interaction/interaction.html @@ -0,0 +1,17 @@ +
    +

    A simple chat system

    + +
    + + +
    + +
    +
    {{msg}}
    +
    +
    + + + +
    +
    diff --git a/testapp/interaction/interaction.js b/testapp/interaction/interaction.js new file mode 100644 index 000000000..d663af610 --- /dev/null +++ b/testapp/interaction/interaction.js @@ -0,0 +1,43 @@ +function InteractionCtrl($scope, $interval, $http) { + + $scope.messages = []; + $scope.message = ""; + $scope.user = ""; + $scope.userInput = ""; + + $scope.sendUser = function() { + $scope.user = $scope.userInput; + } + + var loadMessages = function() { + $http.get('/storage?q=chatMessages'). + success(function(data) { + $scope.messages = data ? data : []; + }). + error(function(err) { + $scope.messages = ['server request failed with: ' + err]; + }); + } + var saveMessages = function() { + var data = { + key: 'chatMessages', + value: $scope.messages + } + $http.post('/storage', data); + }; + + $scope.sendMessage = function() { + $scope.messages.push($scope.user + ': ' + $scope.message); + $scope.message = ""; + saveMessages(); + }; + $scope.clearMessages = function() { + $scope.messages = []; + saveMessages(); + }; + + $interval(function() { + loadMessages(); + }, 100); +} +InteractionCtrl.$inject = ['$scope', '$interval', '$http']; diff --git a/testapp/scripts/web-server.js b/testapp/scripts/web-server.js index 4c9925452..a55b82d5f 100755 --- a/testapp/scripts/web-server.js +++ b/testapp/scripts/web-server.js @@ -24,6 +24,7 @@ var main = function() { join(" ")); }; +var storage = {}; var testMiddleware = function(req, res, next) { if (req.path == '/fastcall') { res.send(200, 'done'); @@ -37,6 +38,25 @@ var testMiddleware = function(req, res, next) { setTimeout(function() { res.send(200, 'slow template contents'); }, 5000); + } else if (req.path == '/storage') { + if (req.method === 'GET') { + var value; + if (req.query.q) { + value = storage[req.query.q]; + res.send(200, value); + } else { + res.send(400, 'must specify query'); + } + } else if (req.method === 'POST') { + if (req.body.key && req.body.value) { + storage[req.body.key] = req.body.value; + res.send(200); + } else { + res.send(400, 'must specify key/value pair'); + } + } else { + res.send(400, 'only accepts GET/POST'); + } } else { return next(); } @@ -45,6 +65,7 @@ var testMiddleware = function(req, res, next) { testApp.configure(function() { testApp.use('/lib/angular', express.static(angularDir)); testApp.use(express.static(testAppDir)); + testApp.use(express.json()); testApp.use(testMiddleware); });