├── fixtures └── auth-e2e │ ├── .gitignore │ ├── .meteor │ ├── .gitignore │ ├── cordova-plugins │ ├── release │ ├── platforms │ ├── .finished-upgraders │ ├── .id │ ├── packages │ └── versions │ ├── both │ └── collections.js │ ├── client │ ├── email.js │ ├── layout.html │ ├── actions.html │ ├── email.html │ ├── client.css │ ├── lib │ │ └── sniff-browser.js │ └── actions.js │ ├── README.md │ ├── server │ ├── password.js │ ├── email.js │ ├── methods.js │ └── oauth.js │ └── deploy.sh ├── .gitignore ├── specs ├── test │ └── test.js └── auth │ ├── oauth_redirect.js │ ├── oauth.js │ ├── password.js │ ├── oauth_providers.js │ └── email.js ├── run.js ├── .jshintrc ├── package.json ├── lib ├── oauth_reporter.js ├── failure_banner.js ├── fiber_utils.js ├── wd_with_sync.js ├── test_runner.js ├── reporter.js ├── master.js ├── test_interface.js └── test_env.js ├── config.js └── README.md /fixtures/auth-e2e/.gitignore: -------------------------------------------------------------------------------- 1 | secrets.json 2 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.1 2 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/both/collections.js: -------------------------------------------------------------------------------- 1 | EmailFlowLogs = new Mongo.Collection('email-flow-logs'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | \#*# 4 | secrets.json 5 | screenshots/*.png 6 | run.sh -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/email.js: -------------------------------------------------------------------------------- 1 | Template.emailLogs.helpers({ 2 | logs: function () { 3 | return EmailFlowLogs.find({ 4 | to: Session.get('browser') + '@qa.com' 5 | }, { sort: { timestamp: -1 }}); 6 | } 7 | }); -------------------------------------------------------------------------------- /specs/test/test.js: -------------------------------------------------------------------------------- 1 | describe('Google', function () { 2 | 3 | it('should have the correct title', function () { 4 | browser.get('http://www.google.com'); 5 | expect(browser.title()).to.contain('Google'); 6 | }); 7 | 8 | }); -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | var minimist = require('minimist'); 2 | var options = minimist(process.argv.slice(2)); 3 | 4 | if (options.help) { 5 | console.log(require('fs').readFileSync('./usage', 'utf-8')); 6 | process.exit(0); 7 | } 8 | 9 | options.files = options._; 10 | require('./lib/master').run(options); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "evil": true, 4 | "strict": false, 5 | "quotmark": false, 6 | "globals": { 7 | "it": true, 8 | "describe": true, 9 | "before": true, 10 | "after": true, 11 | "beforeEach": true, 12 | "afterEach": true, 13 | "expect": true, 14 | "browser": true, 15 | "$": true 16 | } 17 | } -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1iv76aq79bm8lr8gcuc 8 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/layout.html: -------------------------------------------------------------------------------- 1 | 2 | Meteor Auth QA 3 | 4 | 5 | 6 | 7 |

This is the Meteor Auth QA page.

8 |

Browser-specific test account: {{browserId}}@qa.com

9 | {{> actions}} 10 | {{> loginButtons}} 11 | {{> emailLogs}} 12 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/README.md: -------------------------------------------------------------------------------- 1 | # Auth QA app 2 | 3 | Run `./deploy.sh` and this app will be deployed at http://rainforest-auth-qa.meteor.com. The app files here are only for local development; the actual deployed test app is created fresh on every run. 4 | 5 | Then, login to RainforestQA to start the "Auth QA" test. 6 | 7 | ### Test accounts for all logins: 8 | 9 | - Username: meteorauthqa@gmail.com 10 | - Password: Authqa15hard -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/actions.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | meteor-platform 7 | autopublish 8 | insecure 9 | accounts-ui 10 | accounts-facebook 11 | accounts-google 12 | accounts-twitter 13 | accounts-github 14 | accounts-weibo 15 | accounts-meetup 16 | accounts-meteor-developer 17 | accounts-password 18 | service-configuration 19 | email 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-e2e", 3 | "version": "1.0.0", 4 | "description": "e2e tests for Meteor", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/meteor/e2e.git" 9 | }, 10 | "dependencies": { 11 | "byline": "^4.1.1", 12 | "chai": "^1.10.0", 13 | "chalk": "^0.5.1", 14 | "fibers": "^1.0.2", 15 | "minimist": "^1.1.0", 16 | "mktemp": "^0.3.5", 17 | "mocha": "^2.0.1", 18 | "sprintf-js": "^1.0.2", 19 | "underscore": "^1.7.0", 20 | "wd": "^0.3.10" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/email.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/server/password.js: -------------------------------------------------------------------------------- 1 | // test validate new user creation 2 | Meteor.startup(function () { 3 | 4 | Accounts.validateNewUser(function (user) { 5 | if (user.emails && user.emails[0].address === 'invalid@qa.com') { 6 | // test custom message 7 | throw new Meteor.Error(403, "Invalid email address"); 8 | } else if (user.emails && user.emails[0].address === 'foo@bar.com') { 9 | // this is to prevent rainforest testers accidentally creating the account 10 | // which would break the manual test flow. 11 | throw new Meteor.Error(403, 12 | "You shouldn't be actually creating foo@bar.com in this test."); 13 | } else { 14 | return true; 15 | } 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/client.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | } 5 | 6 | #email-logs-container { 7 | position: absolute; 8 | right: 0; 9 | top: 0; 10 | height: 100%; 11 | min-width: 40%; 12 | max-width: 50%; 13 | box-sizing: border-box; 14 | overflow-y: scroll; 15 | padding: 0 30px; 16 | background-color: #eee; 17 | } 18 | 19 | #actions { 20 | margin-bottom: 20px; 21 | } 22 | 23 | #actions a { 24 | cursor: pointer; 25 | text-decoration: underline; 26 | } 27 | 28 | #email-logs { 29 | list-style-type: none; 30 | padding: 0; 31 | font-size: 13px; 32 | } 33 | 34 | #email-logs li { 35 | margin: 20px 0; 36 | } 37 | 38 | #email-logs p { 39 | margin: 0; 40 | } -------------------------------------------------------------------------------- /fixtures/auth-e2e/server/email.js: -------------------------------------------------------------------------------- 1 | // Monkey-patching Email.send to log all emails sent. 2 | // We are displaying these in the client so we can assert the results with 3 | // selenium tests. 4 | Email.send = function (options) { 5 | options.time = new Date().toString(); 6 | options.timestamp = Date.now(); 7 | EmailFlowLogs.insert(options); 8 | }; 9 | 10 | Meteor.startup(function () { 11 | 12 | // clear ALL email logs 13 | EmailFlowLogs.remove({}); 14 | 15 | // create a common test account used in email flows 16 | // (the account to login with in the second window) 17 | try { 18 | Accounts.createUser({ 19 | email: 'email@qa.com', 20 | password: '123456' 21 | }); 22 | } catch (e) {} 23 | 24 | // send verification email 25 | Accounts.config({ 26 | sendVerificationEmail: true 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/lib/sniff-browser.js: -------------------------------------------------------------------------------- 1 | var sniffBrowserId = function () { 2 | var UA = navigator.userAgent; 3 | if (UA.match(/Android/)) return 'android'; 4 | if (UA.match(/Chrome/)) return 'chrome'; 5 | if (UA.match(/Firefox/)) return 'firefox'; 6 | if (UA.match(/Safari/)) return 'safari'; 7 | var ie = UA.match(/MSIE\s(\d+)/) || UA.match(/Trident.*rv:(\d+)/); 8 | if (ie) { 9 | return 'ie' + ie[1]; 10 | } 11 | return 'unknown'; 12 | }; 13 | 14 | var browser = sniffBrowserId(); 15 | 16 | // store the browser test account in session, so we can use it to 17 | // filter email flow logs. 18 | Session.set('browser', browser); 19 | 20 | // Generate a browserId so that testers using different browsers 21 | // create different user accounts (avoid clashing) 22 | Template.body.helpers({ 23 | browserId: function () { 24 | return browser; 25 | } 26 | }); -------------------------------------------------------------------------------- /fixtures/auth-e2e/client/actions.js: -------------------------------------------------------------------------------- 1 | Template.actions.helpers({ 2 | actionSuccess: function () { 3 | return Session.get('actionSuccess'); 4 | } 5 | }); 6 | 7 | Session.set('actionSuccess', false); 8 | 9 | // return a function that calls the given method 10 | // and set the success flag 11 | function makeAction (method) { 12 | return function () { 13 | Session.set('actionSuccess', false); 14 | Meteor.call(method, Session.get('browser'), function () { 15 | Session.set('actionSuccess', true); 16 | }); 17 | } 18 | } 19 | 20 | Template.actions.events({ 21 | 'click #clear-email-logs' : makeAction('clearEmailLogs'), 22 | 'click #create-test-account' : makeAction('createTestAccount'), 23 | 'click #remove-test-account' : makeAction('removeTestAccount'), 24 | 'click #test-send-enrollment-email' : makeAction('sendNewVerificationEmail') 25 | }); -------------------------------------------------------------------------------- /lib/oauth_reporter.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var providers = [ 3 | 'github', 4 | 'google', 5 | 'facebook', 6 | 'twitter', 7 | 'weibo', 8 | 'meteor-developer', 9 | 'meetup' 10 | ]; 11 | var providerRegex = new RegExp(providers.join('|')); 12 | var failedPairs = {}; 13 | 14 | exports.fail = function (browser, message) { 15 | var providerMatch = message.match(providerRegex); 16 | if (providerMatch) { 17 | var provider = providerMatch[0]; 18 | if (!failedPairs[browser]) { 19 | failedPairs[browser] = {}; 20 | } 21 | failedPairs[browser][provider] = true; 22 | } 23 | }; 24 | 25 | exports.reportFailedPairs = function () { 26 | if (!Object.keys(failedPairs).length) { 27 | return; 28 | } 29 | console.log(chalk.red('Failed browser/OAuth provider pairs:\n')); 30 | var browser, provider; 31 | for (browser in failedPairs) { 32 | for (provider in failedPairs[browser]) { 33 | console.log(chalk.red('- ' + browser + '/' + provider)); 34 | } 35 | } 36 | console.log(); 37 | }; -------------------------------------------------------------------------------- /fixtures/auth-e2e/server/methods.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 3 | // remove a browser-specific test account. 4 | // this will be called from the client for every new session. 5 | removeTestAccount: function (browser) { 6 | Meteor.users.remove({ 7 | 'emails.0.address': browser + '@qa.com' 8 | }); 9 | }, 10 | 11 | // convenience for additional testing 12 | createTestAccount: function (browser) { 13 | Meteor.call('removeTestAccount', browser); 14 | Accounts.createUser({ 15 | email: browser + '@qa.com', 16 | password: '123456' 17 | }); 18 | }, 19 | 20 | clearEmailLogs: function (browser) { 21 | EmailFlowLogs.remove({ 22 | to: browser + '@qa.com' 23 | }); 24 | }, 25 | 26 | sendNewVerificationEmail: function (browser) { 27 | Meteor.call('removeTestAccount', browser); 28 | var userId = Accounts.createUser({ email: browser + '@qa.com' }); 29 | Accounts.sendEnrollmentEmail(userId); 30 | }, 31 | 32 | configVerificationEmail: function (state) { 33 | Accounts.config({ 34 | sendVerificationEmail: !! state 35 | }); 36 | } 37 | 38 | }); -------------------------------------------------------------------------------- /lib/failure_banner.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var chalk = require('chalk'); 3 | 4 | // Maps browserName -> array of lines, to be displayed in a very 5 | // visible banner at the bottom of the run in case of failure. 6 | var messages = {}; 7 | 8 | exports.record = function (browser, msg) { 9 | if (!messages[browser]) 10 | messages[browser] = []; 11 | messages[browser].push(msg); 12 | }; 13 | 14 | // print a red error line 15 | var line = function (msg) { 16 | console.log(chalk.red(msg)); 17 | }; 18 | 19 | exports.print = function () { 20 | if (_.isEmpty(messages)) 21 | return; 22 | 23 | line("****************************************************************************"); 24 | line("*** FAILURE SUMMARY "); 25 | line("*** "); 26 | line("*** Here is a summary of the failures, with some links to help you diagnose."); 27 | line("*** "); 28 | 29 | for (browser in messages) { 30 | line("*** (" + browser + ")"); 31 | _.each(messages[browser], function (msg) { 32 | line("*** => " + msg); 33 | }); 34 | line("***"); 35 | }; 36 | 37 | line("****************************************************************************"); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/fiber_utils.js: -------------------------------------------------------------------------------- 1 | var Fiber = require('fibers'); 2 | var Future = require('fibers/future'); 3 | var _ = require('underscore'); 4 | 5 | // Adapted from METEOR/packages/meteor/helpers.js:Meteor.wrapAsync 6 | exports.wrapAsync = function (fn, context) { 7 | return function (/* arguments */) { 8 | var self = context || this; 9 | var newArgs = _.toArray(arguments); 10 | var callback; 11 | var fut; 12 | 13 | for (var i = newArgs.length - 1; i >= 0; --i) { 14 | var arg = newArgs[i]; 15 | var type = typeof arg; 16 | if (type !== 'undefined') { 17 | if (type === 'function') { 18 | callback = arg; 19 | } 20 | break; 21 | } 22 | } 23 | 24 | if (! callback) { 25 | fut = new Future(); 26 | callback = fut.resolver(); 27 | ++i; // Insert the callback just after arg. 28 | } 29 | 30 | newArgs[i] = function () { 31 | var args = arguments; 32 | Fiber(function () { 33 | callback.apply(null, args); 34 | }).run(); 35 | }; 36 | var result = fn.apply(self, newArgs); 37 | return (fut ? fut.wait() : result) || self; 38 | }; 39 | }; 40 | 41 | // Given a list of method names, wrap each of them 42 | // with `wrapAsync`, letting you call them synchronously 43 | // or asynchronously. 44 | // 45 | // XXX there's probably a better way to do this. 46 | // but an experiment wrapping all methods on `obj` 47 | // let to a failing test. 48 | exports.wrapAsyncObject = function (obj, methods) { 49 | _.each(methods, function (method) { 50 | obj[method] = exports.wrapAsync(obj[method], obj); 51 | }); 52 | return obj; 53 | }; -------------------------------------------------------------------------------- /fixtures/auth-e2e/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.1.2 2 | accounts-facebook@1.0.2 3 | accounts-github@1.0.2 4 | accounts-google@1.0.2 5 | accounts-meetup@1.0.2 6 | accounts-meteor-developer@1.0.2 7 | accounts-oauth@1.1.2 8 | accounts-password@1.0.4 9 | accounts-twitter@1.0.2 10 | accounts-ui-unstyled@1.1.4 11 | accounts-ui@1.1.3 12 | accounts-weibo@1.0.2 13 | application-configuration@1.0.3 14 | autopublish@1.0.1 15 | autoupdate@1.1.3 16 | base64@1.0.1 17 | binary-heap@1.0.1 18 | blaze-tools@1.0.1 19 | blaze@2.0.3 20 | boilerplate-generator@1.0.1 21 | callback-hook@1.0.1 22 | check@1.0.2 23 | ctl-helper@1.0.4 24 | ctl@1.0.2 25 | ddp@1.0.12 26 | deps@1.0.5 27 | ejson@1.0.4 28 | email@1.0.4 29 | facebook@1.1.2 30 | fastclick@1.0.1 31 | follower-livedata@1.0.2 32 | geojson-utils@1.0.1 33 | github@1.1.1 34 | google@1.1.2 35 | html-tools@1.0.2 36 | htmljs@1.0.2 37 | http@1.0.8 38 | id-map@1.0.1 39 | insecure@1.0.1 40 | jquery@1.0.1 41 | json@1.0.1 42 | launch-screen@1.0.0 43 | less@1.0.11 44 | livedata@1.0.11 45 | localstorage@1.0.1 46 | logging@1.0.5 47 | meetup@1.1.1 48 | meteor-developer@1.1.1 49 | meteor-platform@1.2.0 50 | meteor@1.1.3 51 | minifiers@1.1.2 52 | minimongo@1.0.5 53 | mobile-status-bar@1.0.1 54 | mongo@1.0.9 55 | npm-bcrypt@0.7.7 56 | oauth1@1.1.2 57 | oauth2@1.1.1 58 | oauth@1.1.2 59 | observe-sequence@1.0.3 60 | ordered-dict@1.0.1 61 | random@1.0.1 62 | reactive-dict@1.0.4 63 | reactive-var@1.0.3 64 | reload@1.1.1 65 | retry@1.0.1 66 | routepolicy@1.0.2 67 | service-configuration@1.0.2 68 | session@1.0.4 69 | sha@1.0.1 70 | spacebars-compiler@1.0.3 71 | spacebars@1.0.3 72 | srp@1.0.1 73 | templating@1.0.9 74 | tracker@1.0.3 75 | twitter@1.1.2 76 | ui@1.0.4 77 | underscore@1.0.1 78 | url@1.0.2 79 | webapp-hashing@1.0.1 80 | webapp@1.1.4 81 | weibo@1.1.1 82 | -------------------------------------------------------------------------------- /lib/wd_with_sync.js: -------------------------------------------------------------------------------- 1 | // A wrapped wd with synchronous API using fibers. 2 | 3 | var wd = require('wd'); 4 | var _ = require('underscore'); 5 | var fiberUtils = require('./fiber_utils'); 6 | 7 | var wdWithSync = _.clone(wd); 8 | 9 | var methodsToWrap = Object.keys(require('wd/lib/commands')); 10 | 11 | wdWithSync.remote = function () { 12 | var remote = wd.remote.apply(wd, arguments); 13 | 14 | // A fancy way to extract all of the wd commands, which map to methods 15 | // on `wd.remote()` that should be wrapped so that they can also be 16 | // called synchronously if a callback is omitted. 17 | fiberUtils.wrapAsyncObject(remote, Object.keys(require('wd/lib/commands'))); 18 | 19 | // Then, wrap methods on the result of the various 'elementByXXX' 20 | // methods as to allow to be called synchronously if callbacks are omitted 21 | _.each(methodsToWrap, function (methodName) { 22 | var origMethod = remote[methodName]; 23 | if (methodName.match(/elementBy.*/)) { 24 | remote[methodName] = function () { 25 | var element = origMethod.apply(this, arguments); 26 | // A fancy way to tell which methods on "element" objects 27 | // should be wrapped 28 | return fiberUtils.wrapAsyncObject( 29 | element, Object.keys(require('wd/lib/element-commands'))); 30 | }; 31 | } 32 | // wrap every element in the Array returned from elementsByXXX methods 33 | if (methodName.match(/elementsBy.*/)) { 34 | remote[methodName] = function () { 35 | var elements = origMethod.apply(this, arguments); 36 | return elements.map(function (element) { 37 | return fiberUtils.wrapAsyncObject( 38 | element, Object.keys(require('wd/lib/element-commands'))); 39 | }); 40 | }; 41 | } 42 | }); 43 | return remote; 44 | }; 45 | 46 | module.exports = wdWithSync; 47 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // SauceLabs config 2 | exports.remote = function () { 3 | if (!process.env.SAUCE_LABS_ACCESS_KEY) { 4 | console.error("Need to set SAUCE_LABS_ACCESS_KEY environment variable"); 5 | process.exit(1); 6 | } 7 | 8 | return { 9 | host: 'ondemand.saucelabs.com', 10 | port: 80, 11 | username: 'honeycomb', 12 | accessKey: process.env.SAUCE_LABS_ACCESS_KEY 13 | }; 14 | }; 15 | 16 | // WebDriver Browser Descriptors 17 | // https://saucelabs.com/platforms/webdriver 18 | var browserDescriptors = exports.browserDescriptors = { 19 | chrome: { 20 | browserName: 'chrome', 21 | version: 38, 22 | platform: 'OS X 10.10' 23 | }, 24 | firefox: { 25 | browserName: 'firefox', 26 | version: 33 27 | }, 28 | safari: { 29 | browserName: 'safari', 30 | version: 8, 31 | platform: 'OS X 10.10' 32 | }, 33 | ie8: { 34 | browserName: 'internet explorer', 35 | version: 8, 36 | platform: 'Windows XP', 37 | prerun: 'http://s3.amazonaws.com/meteor-saucelabs/disable_ie8_slow_javascript_warning.bat' 38 | }, 39 | ie9: { 40 | browserName: 'internet explorer', 41 | version: 9, 42 | platform: 'Windows 7' 43 | }, 44 | ie10: { 45 | browserName: 'internet explorer', 46 | version: 10, 47 | platform: 'Windows 8' 48 | }, 49 | ie11: { 50 | browserName: 'internet explorer', 51 | version: 11, 52 | platform: 'Windows 8.1' 53 | } 54 | }; 55 | 56 | // Browser group configurations. 57 | // These are used by the run script. 58 | // e.g. `node run --browsers=all` 59 | var browserLists = exports.browserLists = {}; 60 | 61 | // all browsers 62 | browserLists.all = Object.keys(browserDescriptors); 63 | 64 | // add individual browsers to the list 65 | // so we can do `node run --browsers=chrome` 66 | browserLists.all.forEach(function (name) { 67 | browserLists[name] = [name]; 68 | }); 69 | 70 | // XXX add your custom lists below. 71 | 72 | // `node run --browsers=mylist` 73 | // browserLists.mylist = ['chrome', 'firefox']; 74 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/server/oauth.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | // configure services 3 | ServiceConfiguration.configurations.remove({ 4 | service: { 5 | $in: [ 6 | 'facebook', 7 | 'google', 8 | 'twitter', 9 | 'github', 10 | 'meetup', 11 | 'meteor-developer', 12 | 'weibo' 13 | ] 14 | } 15 | }); 16 | 17 | var secrets = Meteor.settings; 18 | var secret = function (provider) { 19 | if (!secrets[provider]) { 20 | console.error("Need to set `Meteor.settings[\"" + provider + "\"]`"); 21 | process.exit(1); 22 | }; 23 | return secrets[provider]; 24 | }; 25 | 26 | var loginStyle = secrets.loginStyle || 'popup'; 27 | 28 | // intentionally calling the configureLoginService method 29 | // here instead of direct insert, since this is the method 30 | // being called on the server when an actual user configures it. 31 | Meteor.call('configureLoginService', { 32 | service: "facebook", 33 | appId: "461508060653790", 34 | secret: secret("facebook"), 35 | loginStyle: loginStyle 36 | }); 37 | Meteor.call('configureLoginService', { 38 | service: "google", 39 | clientId: "262689297883-d14f2erj5dlfhk6nlvhcldhq0624op7q.apps.googleusercontent.com", 40 | secret: secret("google"), 41 | loginStyle: loginStyle 42 | }); 43 | Meteor.call('configureLoginService', { 44 | service: "twitter", 45 | consumerKey: "0eRD2Z28NjAMjQGeqFXM8MMY5", 46 | secret: secret("twitter"), 47 | loginStyle: loginStyle 48 | }); 49 | Meteor.call('configureLoginService', { 50 | service: "github", 51 | clientId: "d4b54e5ba3611bc14c06", 52 | secret: secret("github"), 53 | loginStyle: loginStyle 54 | }); 55 | Meteor.call('configureLoginService', { 56 | service: "weibo", 57 | clientId: "819563028", 58 | secret: secret("weibo"), 59 | loginStyle: loginStyle 60 | }); 61 | Meteor.call('configureLoginService', { 62 | service: "meetup", 63 | clientId: "mvnukfi6bdoacs03tkirj4394n", 64 | secret: secret("meetup"), 65 | loginStyle: loginStyle 66 | }); 67 | Meteor.call('configureLoginService', { 68 | service: "meteor-developer", 69 | clientId: "u6q4sghbCdyCtkTpr", 70 | secret: secret("meteor-developer"), 71 | loginStyle: loginStyle 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /lib/test_runner.js: -------------------------------------------------------------------------------- 1 | // This script sets up a Mocha test runner instance using 2 | // our custom fiber-compatible bdd interface and runs 3 | // specified tests in a **single** browser. 4 | 5 | // We will run multiple processes of this script in parallel 6 | // to test different browsers. 7 | 8 | // dependencies 9 | var minimist = require('minimist'); 10 | var Mocha = require('mocha'); 11 | var path = require('path'); 12 | var testInterface = require('./test_interface'); 13 | var testEnvironment = require('./test_env'); 14 | 15 | // parse command line argumetnts 16 | var options = minimist(process.argv.slice(2)); 17 | 18 | // instantiate the runner instance 19 | var mocha = new Mocha(); 20 | // use our custom interface 21 | Mocha.interfaces['meteor-bdd'] = testInterface; 22 | mocha.ui('meteor-bdd'); 23 | // global threshold for fail timeout 24 | mocha.suite.timeout(50000); 25 | // use json-stream reporter since it's more flexible 26 | // for further processing. The stream emits events in the 27 | // format of ["event", data] and writes them to stdout. 28 | // the parent process will be notified of the event and 29 | // report them. 30 | mocha.reporter('json-stream'); 31 | 32 | // load all test files from the given directories 33 | // by default, run all files inside `spec/` that ends with "spec.js". 34 | mocha.files = []; 35 | var directories = options._.length ? options._ : ['']; 36 | directories.forEach(function (directory) { 37 | var found = Mocha.utils.lookupFiles('specs/' + directory, ['js'], true); 38 | mocha.files = mocha.files.concat(found); 39 | }); 40 | 41 | /** 42 | * We can also emit extra events to the parent process by 43 | * writing to stdout in the same format the json stream 44 | * reporter does. 45 | * 46 | * @param {String} event 47 | * @param {Object} data - optional. 48 | */ 49 | var emitEvent = function (event, data) { 50 | event = [event]; 51 | if (data) event.push(data); 52 | console.log(JSON.stringify(event)); 53 | }; 54 | 55 | // signal master script about initialization 56 | emitEvent('init'); 57 | 58 | // initialize the test environment, then run the tests 59 | testEnvironment.init(options, function startMocha () { 60 | // actually run mocha tests, and teardown test environment 61 | // after the tests complete 62 | var runner = mocha.run(testEnvironment.teardown); 63 | process.on('SIGINT', function () { 64 | runner.aboort(); 65 | }); 66 | // mocha's json stream reporter doesn't include error 67 | // stack trace, so we have to emit that ourselves... 68 | runner.on('fail', function (test, err) { 69 | emitEvent('stack', { stack: err.stack }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /fixtures/auth-e2e/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ -z "$METEOR" ]; then 5 | echo "This script is to be used in advance of running automated Auth QA on Rainforest" 6 | echo 7 | echo "Usage: METEOR=/path/to/meteor ./deploy.sh" 8 | exit 1 9 | fi 10 | 11 | cd `dirname "$0"` 12 | TEMPLATE_DIR=`pwd` 13 | TEMP_DIR=`mktemp -d /tmp/deploy-auth-e2e.XXXXXX` 14 | LOG="$TEMP_DIR/auth-e2e-deploy.log" 15 | 16 | # This is where we create a bunch of apps to deploy them. We also store 17 | # a log file and the ~/.meteorsession file to restore here. 18 | pushd "$TEMP_DIR" > /dev/null 19 | 20 | if [ -a ~/.meteorsession ]; then 21 | # Store the original contents in ~/.meteorsession, which contain the 22 | # credentials for the currently logged-in user. Restore that file if 23 | # this script exits. 24 | METEORSESSION_RESTORE="$TEMP_DIR/.meteorsession-restore" 25 | cp ~/.meteorsession "$METEORSESSION_RESTORE" 26 | function cleanup { 27 | tail "$LOG" 28 | cp "$METEORSESSION_RESTORE" ~/.meteorsession 29 | } 30 | else 31 | function cleanup { 32 | tail "$LOG" 33 | } 34 | fi 35 | trap cleanup EXIT 36 | 37 | # Now, login as rainforestqa. This way, anyone can access apps 38 | # deployed by this script. 39 | echo 40 | echo -n "* Logging in with the test account..." 41 | echo 42 | "$METEOR" help > /dev/null # Download dev bundle 43 | (echo rainforestqa; sleep 2s; echo rainforestqa;) | "$METEOR" login 44 | 45 | # We are creating the app from scratch to ensure fresh installation 46 | # and configuration of the account packages 47 | "$METEOR" create auth-e2e >> $LOG 2>&1 48 | pushd auth-e2e > /dev/null 49 | 50 | # Add all the packages and copy over template app files 51 | PACKAGES=( 52 | accounts-ui 53 | accounts-facebook 54 | accounts-google 55 | accounts-twitter 56 | accounts-github 57 | accounts-weibo 58 | accounts-meetup 59 | accounts-meteor-developer 60 | accounts-password 61 | service-configuration 62 | email 63 | ) 64 | "$METEOR" add ${PACKAGES[@]} 65 | 66 | # delete default files 67 | rm auth-e2e.js auth-e2e.html auth-e2e.css 68 | # copy over actual app files 69 | cp -R "$TEMPLATE_DIR/both" ./both 70 | cp -R "$TEMPLATE_DIR/client" ./client 71 | cp -R "$TEMPLATE_DIR/server" ./server 72 | 73 | # The Auth QA app is deployed at auth-e2e.meteor.com 74 | SITE=rainforest-auth-qa 75 | echo 76 | echo -n "* Deleting already deployed test app..." 77 | echo 78 | # `|| true` so that the script doesn't fail if the the app doesn't exist 79 | "$METEOR" deploy -D $SITE >> $LOG 2>&1 || true 80 | 81 | if [ -z "$OAUTH_PROVIDER_SECRETS" ]; then 82 | echo "Need to set \$OAUTH_PROVIDER_SECRETS" 83 | exit 1 84 | fi 85 | echo 86 | echo -n "* Deploying the test app to $SITE..." 87 | echo 88 | echo $OAUTH_PROVIDER_SECRETS > secrets.json 89 | "$METEOR" deploy --settings secrets.json $SITE >> $LOG 2>&1 90 | rm secrets.json 91 | 92 | echo 93 | echo DONE 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor E2E Tests 2 | 3 | ## Running Tests 4 | 5 | ``` 6 | Usage: 7 | 8 | export SAUCE_LABS_ACCESS_KEY=... # Find values in 1Password under "e2e" 9 | export OAUTH_PROVIDERS_PASSWORD=... 10 | export OAUTH_PROVIDER_SECRETS=... 11 | 12 | node run [files ...] [--local] [--browsers=all] [--concurrency=5] 13 | 14 | Options: 15 | 16 | files Files to run in `specs/`. If it's a directory, will 17 | search all files recursively. Defaults to 'specs/'. 18 | 19 | --local Run the tests against a local selenium server. 20 | 21 | --browsers List of browsers to launch. Defaults to all browsers listed 22 | in `config.js`. You can also list individual browsers like 23 | this: `--browsers=chrome,firefox`, or you can add your 24 | custom list to `exports.browserLists` in `config.js`. 25 | 26 | --concurrency Maximum number of browsers to launch at the same time. The 27 | default is what we have on our SauceLabs account. 28 | ``` 29 | 30 | ## Test Authoring 31 | 32 | All test files should be located in `specs/`. Don't place test fixtures and 33 | helpers in there - put them in `fixtures/`. The tests are run with Mocha using a 34 | custom interface, so each spec (the `it()` block) is run inside a fiber. We have 35 | also wrapped methods on the wd (SauceLabs' official Node.js selenium webdriver 36 | bridge) browser instance to let them run synchronously if you don't pass a 37 | callback as the last argument (without blocking the event loop). 38 | 39 | When tests are run, a wd browser instance will already be instantiated for you 40 | and available globally as `browser`. For assertions we are using 41 | [Chai](http://chaijs.com/api/bdd/) with `chai.expect` available globally. 42 | 43 | The result is that your tests could look like this (isn't it nice?): 44 | 45 | ``` js 46 | // specs/test/test_spec.js 47 | describe('Google', function () { 48 | 49 | it('should have the correct title', function () { 50 | browser.get('http://www.google.com'); 51 | expect(browser.title()).to.contain('Google'); 52 | }); 53 | 54 | }); 55 | ``` 56 | 57 | ## Working on the Test Runner 58 | 59 | There are several parts: 60 | 61 | - `lib/master.js`: the master script that launches child runner processes for 62 | each browser we want the tests to be run against. 63 | - `lib/test_runner.js`: a child runner process that loads Mocha and runs tests 64 | against a single browser. It communicates to the master script via stdout. 65 | - `lib/test_interface.js`: a custom mocha interface that runs all tests inside 66 | fibers. 67 | - `lib/test_env.js`: sets up the environment in which tests are run (globals, 68 | custom helpers, etc.) 69 | - `lib/reporter.js`: where we react to test progress events emitted from child 70 | processes (e.g. print to console, send to server, etc.) 71 | - `config.js`: SauceLabs credentials, browser lists, etc. 72 | 73 | -------------------------------------------------------------------------------- /lib/reporter.js: -------------------------------------------------------------------------------- 1 | var sprintf = require('sprintf-js').sprintf; 2 | var chalk = require('chalk'); 3 | var _ = require('underscore'); 4 | var oauthReporter = require('./oauth_reporter'); 5 | var failureBanner = require('./failure_banner'); 6 | var path = require('path'); 7 | var gray = chalk.gray; 8 | var red = chalk.red; 9 | var green = chalk.green; 10 | var yellow = chalk.yellow; 11 | var blue = chalk.blue; 12 | var stats = {}; 13 | 14 | exports.noError = false; 15 | 16 | /** 17 | * Helper that adds a browser prefix to a log message. 18 | * 19 | * @param {String} browser 20 | * @param {String} msg 21 | */ 22 | var log = function (browser, msg) { 23 | console.log(green(sprintf('%-12s', '[' + browser + ']')) + msg); 24 | }; 25 | 26 | /** 27 | * Report an event from the runner json stream. 28 | * 29 | * @param {String} browser 30 | * @param {Array} event - ["pass", {...}] 31 | */ 32 | exports.reportEvent = function (browser, event) { 33 | var type = event[0]; 34 | var data = event[1]; 35 | switch (type) { 36 | case 'init': 37 | log(browser, gray('Initializing browser...')); 38 | break; 39 | case 'start': 40 | log(browser, gray('Test started. (' + data.total + ' total)')); 41 | break; 42 | case 'pass': 43 | var suiteTitle = data.fullTitle.replace(data.title, ''); 44 | log(browser, green('✓ ') + yellow(suiteTitle) + data.title); 45 | break; 46 | case 'fail': 47 | log(browser, red('✗ ') + data.fullTitle); 48 | oauthReporter.fail(browser, data.fullTitle); 49 | break; 50 | case 'stack': 51 | data.stack.split('\n').forEach(function (line) { 52 | // shorten paths in stack trace 53 | var baseDir = process.cwd(); 54 | line = line.replace(baseDir, path.basename(baseDir)); 55 | 56 | log(browser, red(' ' + line)); 57 | }); 58 | break; 59 | case 'end': 60 | stats[browser] = data; 61 | break; 62 | case 'error': 63 | log(browser, red(data)); 64 | break; 65 | case 'log': 66 | log(browser, blue('LOG: ' + data)); 67 | break; 68 | case 'failureBanner': 69 | failureBanner.record(browser, data); 70 | break; 71 | } 72 | // TODO: send the event to a meteor app to display this info 73 | }; 74 | 75 | /** 76 | * Report runner process error output line-by-line. 77 | * 78 | * @param {String} browser 79 | * @param {Object} event 80 | */ 81 | exports.reportError = function (browser, line) { 82 | log(browser, red(line)); 83 | }; 84 | 85 | /** 86 | * Report all browsers stats at the end. 87 | */ 88 | exports.reportAll = function () { 89 | console.log(); 90 | for (var browser in stats) { 91 | var data = stats[browser]; 92 | log( 93 | browser, 94 | data.tests + ' total' + 95 | (data.passes > 0 ? green(' / ' + data.passes + ' passed') : '') + 96 | (data.failures > 0 ? red(' / ' + data.failures + ' failed') : '') + 97 | gray(' (' + data.duration + 'ms)') 98 | ); 99 | } 100 | console.log(); 101 | oauthReporter.reportFailedPairs(); 102 | failureBanner.print(); 103 | }; 104 | 105 | /** 106 | * Check if all tests passed 107 | * 108 | * @returns {Boolean} 109 | */ 110 | exports.allTestsPassed = function () { 111 | return _.all(stats, function (data) { 112 | return data.failures === 0; 113 | }); 114 | }; 115 | -------------------------------------------------------------------------------- /lib/master.js: -------------------------------------------------------------------------------- 1 | var Fiber = require('fibers'); 2 | var fiberUtils = require('./fiber_utils'); 3 | var reporter = require('./reporter'); 4 | var spawn = require('child_process').spawn; 5 | var byline = require('byline'); 6 | var config = require('../config'); 7 | 8 | /** 9 | * Spawn a runner process for a single browser. 10 | * 11 | * @param {String} browser 12 | * @param {Array} args 13 | * @param {Function} done 14 | */ 15 | var spawnRunner = function (browser, args, done) { 16 | args = args.concat(['--browser', browser]); 17 | var runner = spawn(process.execPath, args); 18 | byline(runner.stdout).on('data', function (line) { 19 | var payload; 20 | try { 21 | payload = JSON.parse(line.toString()); 22 | } catch (e) { 23 | // If the output from the runner process is not valid 24 | // JSON, treat it as plain text log. 25 | reporter.reportEvent(browser, ['log', line.toString()]); 26 | return; 27 | } 28 | reporter.reportEvent(browser, payload); 29 | }); 30 | byline(runner.stderr).on('data', function (line) { 31 | reporter.reportError(browser, line.toString()); 32 | }); 33 | runner.on('exit', done); 34 | }; 35 | 36 | /** 37 | * Spawn a batch of runner processes in parallel. 38 | * This function is fiber-wrapped. 39 | * 40 | * @param {Array} batch 41 | * @param {Array} args 42 | * @param {Function} done 43 | */ 44 | var spawnBatch = fiberUtils.wrapAsync(function (batch, args, done) { 45 | var pending = batch.length; 46 | batch.forEach(function (browser) { 47 | spawnRunner(browser, args, function () { 48 | pending--; 49 | if (! pending) done(); 50 | }); 51 | }); 52 | }); 53 | 54 | // exposed API 55 | // the options here are the parsed command line arguments 56 | // provided to run.js 57 | exports.run = function (options) { 58 | 59 | // check max concurrency 60 | var maxConcurrency = options.concurrency || 4; 61 | 62 | // check the browsers to launch. 63 | // the argument could be a comma-separated list, where each 64 | // item is a listName in `config.browserLists`. 65 | var browsers = (options.browsers || 'all') 66 | .split(',') 67 | .reduce(function (result, listName) { 68 | var list = config.browserLists[listName]; 69 | if (! list) { 70 | console.error('Invalid browser list: ' + listName); 71 | return result; 72 | } 73 | return result.concat(list); 74 | }, []); 75 | 76 | // build up the argument list for runner processes 77 | var runnerArgs = [__dirname + '/test_runner.js']; 78 | // pass down files list to child 79 | runnerArgs = runnerArgs.concat(options.files); 80 | // pass down local flag to child 81 | if (options.local) { 82 | runnerArgs = runnerArgs.concat(['--local']); 83 | } 84 | // XXX Oauth providers 85 | // using env variable because we also use it for 86 | // Jenkins build matrix 87 | if (options.providers) { 88 | process.env.TEST_OAUTH_PROVIDERS = options.providers; 89 | } 90 | 91 | // launch the runner processes! 92 | Fiber(function () { 93 | var alreadyRun = 0; 94 | while (alreadyRun < browsers.length) { 95 | var batch = browsers 96 | .slice(alreadyRun, alreadyRun + maxConcurrency); 97 | spawnBatch(batch, runnerArgs); 98 | alreadyRun += maxConcurrency; 99 | } 100 | reporter.reportAll(); 101 | 102 | if (!reporter.allTestsPassed()) 103 | process.exit(1); 104 | }).run(); 105 | }; 106 | -------------------------------------------------------------------------------- /specs/auth/oauth_redirect.js: -------------------------------------------------------------------------------- 1 | var excludedPairs = [ 2 | ['safari', 'github'], 3 | ['safari', 'meteor-developer'], 4 | ['ie8', 'meteor-developer'], 5 | ['ie9', 'meteor-developer'] 6 | ]; 7 | 8 | var providersToRun = function () { 9 | var _ = require('underscore'); 10 | var allProviders = require('./oauth_providers').filter(function (provider) { 11 | return ! excludedPairs.some(function (pair) { 12 | return pair[0] === browser.name && pair[1] === provider.name; 13 | }); 14 | }); 15 | 16 | if (process.env.TEST_OAUTH_PROVIDERS) { 17 | var providerList = process.env.TEST_OAUTH_PROVIDERS.split(','); 18 | return allProviders.filter(function (provider) { 19 | return _.contains(providerList, provider.name); 20 | }); 21 | } else { 22 | return allProviders; 23 | } 24 | }; 25 | 26 | describe('A small app with accounts', function () { 27 | 28 | var openDropdown = function () { 29 | browser.find("#login-sign-in-link, #login-name-link").click(); 30 | }; 31 | var closeDropdown = function () { 32 | browser.find("a.login-close-text").click(); 33 | }; 34 | 35 | var startSignIn = function (providerName) { 36 | browser.find('#login-buttons-' + providerName).click(); 37 | }; 38 | 39 | var expectSignedIn = function (userDisplayName) { 40 | expect(browser.find('#login-name-link', 30000).text()).to.contain(userDisplayName); 41 | }; 42 | 43 | var expectSignedOut = function () { 44 | expect(browser.find("#login-sign-in-link", 30000).text()).to.contain("Sign in ▾"); 45 | }; 46 | 47 | var signOut = function () { 48 | browser.find('#login-buttons-logout').click(); 49 | expectSignedOut(); 50 | }; 51 | 52 | before(function () { 53 | browser.get('http://rainforest-auth-qa.meteor.com'); 54 | }); 55 | 56 | providersToRun().forEach(function (provider) { 57 | describe("- " + provider.name + ' login', function () { 58 | // these steps are sequential and stateful in nature, so stop 59 | // after first failures. 60 | this.bail(true); 61 | 62 | before(function () { 63 | browser.refresh(); 64 | }); 65 | 66 | it('redirect to sign in page', function () { 67 | 68 | browser.wait('#login-sign-in-link', 30000); 69 | 70 | openDropdown(); 71 | expect(browser.find("#login-buttons-" + provider.name).text()).to.contain("Sign in with"); 72 | startSignIn(provider.name); 73 | 74 | }); 75 | 76 | it('sign in page loads', function () { 77 | provider.waitForRedirectPage(); 78 | }); 79 | 80 | if (provider.cancelSignIn) { 81 | it('cancel sign in', function () { 82 | provider.cancelSignIn(); 83 | expectSignedOut(); 84 | startSignIn(provider.name); 85 | provider.waitForRedirectPage(); 86 | }); 87 | } 88 | 89 | it('perform sign in', function () { 90 | provider.signInInRedirectPage(); 91 | }); 92 | 93 | it('signs in in app', function () { 94 | expectSignedIn(provider.userDisplayName); 95 | }); 96 | 97 | it('signs out', function () { 98 | openDropdown(); 99 | signOut(); 100 | }); 101 | 102 | it('open login popup after having previously logged in', function () { 103 | openDropdown(); 104 | startSignIn(provider.name); 105 | }); 106 | 107 | if (provider.signInInSecondRedirect) { 108 | it('sign in redirect page after having previously logged in', function () { 109 | provider.signInInSecondRedirect(); 110 | }); 111 | } 112 | 113 | it('signs in in app again', function () { 114 | expectSignedIn(provider.userDisplayName); 115 | }); 116 | 117 | it('signs out a second time', function () { 118 | openDropdown(); 119 | signOut(provider.name); 120 | }); 121 | 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /lib/test_interface.js: -------------------------------------------------------------------------------- 1 | // This is a custom Mocha interface based on Mocha's own 2 | // BDD interface (node_modules/mocha/lib/interfaces/bdd.js). 3 | // Mocha's original implementation is copied here and all 4 | // we're doing is wrapping a few methods like `it`, `before` 5 | // and `after` so their callbacks run inside fibers. 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var Fiber = require('fibers'); 12 | var Suite = require('mocha/lib/suite'); 13 | var Test = require('mocha/lib/test'); 14 | var escapeRe = require('mocha/node_modules/escape-string-regexp'); 15 | var path = require('path'); 16 | var fs = require('fs'); 17 | var bdd = require('mocha/lib/interfaces/bdd.js'); 18 | var _ = require('underscore'); 19 | 20 | // Takes a screenshot of the current state of the test run 21 | // and saves it to a file. 22 | // XXX really shouldn't be in this file. 23 | var screenshot = function () { 24 | var filename = _.random(100000, 999999) + ".png"; 25 | if (process.env.SCREENSHOT_FILENAME_PREFIX) 26 | filename = process.env.SCREENSHOT_FILENAME_PREFIX + filename; 27 | 28 | var dir = "screenshots"; 29 | var relPath = path.join(dir, filename); 30 | var tmpFile = path.resolve(process.cwd(), relPath); 31 | 32 | if (!fs.existsSync("screenshots")) { 33 | fs.mkdirSync("screenshots"); 34 | } else { 35 | if (!fs.statSync("screenshots").isDirectory()) { 36 | throw new Error("Unexpected: screenshot/ exists but is not a directory"); 37 | } 38 | } 39 | 40 | fs.writeFileSync( 41 | tmpFile, 42 | new Buffer(browser.takeScreenshot(), 'base64')); 43 | 44 | var screenshotLocator; 45 | if (process.env.JENKINS_URL) { 46 | // if running in Jenkins, which publishes to S3 47 | screenshotLocator = "http://s3.amazonaws.com/com.meteor.jenkins/e2e-screenshots/" + filename; 48 | } else { 49 | screenshotLocator = tmpFile; 50 | } 51 | 52 | return screenshotLocator; 53 | }; 54 | 55 | // Report a message to the reported running in the main process that 56 | // will be printed at the bottom of the run in the failure banner 57 | var reportInFailureBanner = function (msg) { 58 | console.log(JSON.stringify(["failureBanner", msg])); 59 | }; 60 | 61 | /** 62 | * Wrap a raw BDD interface method so that its callback 63 | * runs inside a fiber. 64 | * 65 | * @param {Function} original 66 | */ 67 | var wrap = function (original) { 68 | return function (/* arguments */) { 69 | var args = [].slice.call(arguments); 70 | var fn = args[args.length - 1]; 71 | var asyncRE = /^function.*\(\s*done\s*\)\s*\{/; 72 | var isVanillaAsync = asyncRE.test(fn.toString()); 73 | // If `fn` is a vanilla async function that takes a `done` 74 | // callback, let the user call `done` instead of here. 75 | args[args.length - 1] = function (done) { 76 | Fiber(function () { 77 | try { 78 | fn(done); 79 | } catch (e) { 80 | reportInFailureBanner( 81 | "Error: " + e.message); 82 | reportInFailureBanner( 83 | "Screenshot at time of failure: " + screenshot()); 84 | reportInFailureBanner( 85 | "Screencast and Selenium Logs: " + browser.sauceLabsUrl); 86 | throw e; 87 | } 88 | 89 | if (! isVanillaAsync) 90 | done(); 91 | }).run(); 92 | }; 93 | original.apply(null, args); 94 | }; 95 | }; 96 | 97 | // The interface function exported here gets called by 98 | // mocha when loading its interface internally. 99 | module.exports = function(suite){ 100 | bdd(suite); 101 | 102 | // wrap all functions used to define test steps 103 | suite.on('pre-require', function (context, file, mocha) { 104 | [ 105 | 'it', 106 | 'before', 107 | 'after', 108 | 'beforeEach', 109 | 'afterEach' 110 | ].forEach(function (key) { 111 | context[key] = wrap(context[key]); 112 | }); 113 | }); 114 | }; 115 | -------------------------------------------------------------------------------- /specs/auth/oauth.js: -------------------------------------------------------------------------------- 1 | var excludedPairs = [ 2 | ['safari', 'github'], 3 | ['safari', 'meteor-developer'], 4 | ['ie8', 'meteor-developer'], 5 | ['ie9', 'meteor-developer'] 6 | ]; 7 | 8 | var providersToRun = function () { 9 | var _ = require('underscore'); 10 | var allProviders = require('./oauth_providers').filter(function (provider) { 11 | return ! excludedPairs.some(function (pair) { 12 | return pair[0] === browser.name && pair[1] === provider.name; 13 | }); 14 | }); 15 | 16 | if (process.env.TEST_OAUTH_PROVIDERS) { 17 | var providerList = process.env.TEST_OAUTH_PROVIDERS.split(','); 18 | return allProviders.filter(function (provider) { 19 | return _.contains(providerList, provider.name); 20 | }); 21 | } else { 22 | return allProviders; 23 | } 24 | }; 25 | 26 | describe('A small app with accounts', function () { 27 | 28 | var openDropdown = function () { 29 | browser.find("#login-sign-in-link, #login-name-link").click(); 30 | }; 31 | var closeDropdown = function () { 32 | browser.find("a.login-close-text").click(); 33 | }; 34 | 35 | var startSignIn = function (providerName) { 36 | browser.find('#login-buttons-' + providerName).click(); 37 | }; 38 | 39 | var expectSignedIn = function (userDisplayName) { 40 | expect(browser.find('#login-name-link', 30000).text()).to.contain(userDisplayName); 41 | }; 42 | 43 | var signOut = function () { 44 | browser.find('#login-buttons-logout').click(); 45 | expect(browser.find("#login-sign-in-link", 30000).text()).to.contain("Sign in ▾"); 46 | }; 47 | 48 | before(function () { 49 | browser.get('http://rainforest-auth-qa.meteor.com'); 50 | }); 51 | 52 | providersToRun().forEach(function (provider) { 53 | describe("- " + provider.name + ' login', function () { 54 | // these steps are sequential and stateful in nature, so stop 55 | // after first failures. 56 | this.bail(true); 57 | 58 | before(function () { 59 | browser.focusMainWindow(); 60 | browser.refresh(); 61 | }); 62 | 63 | it('open login popup', function () { 64 | 65 | browser.wait('#login-sign-in-link', 30000); 66 | 67 | openDropdown(); 68 | expect(browser.find("#login-buttons-" + provider.name).text()).to.contain("Sign in with"); 69 | startSignIn(provider.name); 70 | 71 | }); 72 | 73 | it('popup loads', function () { 74 | // Should show a popup. Test that when we close the pop-up we 75 | // don't lose the ability to then log in again afterwards. 76 | browser.focusSecondWindow(); 77 | provider.waitForPopupContents(); 78 | browser.close(); 79 | browser.focusMainWindow(); 80 | }); 81 | 82 | it('open login popup again', function () { 83 | startSignIn(provider.name); 84 | browser.focusSecondWindow(); 85 | provider.waitForPopupContents(); 86 | }); 87 | 88 | it('sign in popup', function () { 89 | provider.signInInPopup(); 90 | browser.focusMainWindow(); 91 | }); 92 | 93 | it('signs in in app', function () { 94 | expectSignedIn(provider.userDisplayName); 95 | }); 96 | 97 | it('signs out', function () { 98 | openDropdown(); 99 | signOut(); 100 | }); 101 | 102 | it('open login popup after having previously logged in', function () { 103 | openDropdown(); 104 | startSignIn(provider.name); 105 | }); 106 | 107 | if (provider.signInInSecondPopup) { 108 | it('sign in popup after having previously logged in', function () { 109 | browser.focusSecondWindow(); 110 | provider.signInInSecondPopup(); 111 | browser.focusMainWindow(); 112 | }); 113 | }; 114 | 115 | it('signs in in app', function () { 116 | expectSignedIn(provider.userDisplayName); 117 | }); 118 | 119 | it('signs out a second time', function () { 120 | openDropdown(); 121 | signOut(provider.name); 122 | }); 123 | 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /specs/auth/password.js: -------------------------------------------------------------------------------- 1 | describe('Password based login', function () { 2 | 3 | // these steps are sequential and stateful in nature, so stop 4 | // after first failures. 5 | this.bail(true); 6 | 7 | // use a browser-specific email here to ensure each browser creates 8 | // a different test account. this is retrieved during the test and 9 | // reused in multiple specs. 10 | var browserTestAccount; 11 | 12 | before(function () { 13 | browser.get('http://rainforest-auth-qa.meteor.com'); 14 | browser.wait('#login-sign-in-link', 30000); 15 | // cache browser test account 16 | browserTestAccount = browser.find('#browser-email').text(); 17 | // delete the browser-specific test account, if it exists 18 | browser.find('#remove-test-account').click(); 19 | browser.wait('#server-action-ok', 30000); 20 | }); 21 | 22 | it('should display correct UI elements', function () { 23 | 24 | browser.find('#login-sign-in-link', 30000).click(); 25 | browser.wait([ 26 | '#login-email-label', 27 | '#login-email', 28 | '#login-password-label', 29 | '#login-password', 30 | '#login-buttons-password', 31 | '#signup-link', 32 | '#forgot-password-link' 33 | ]); 34 | }); 35 | 36 | it('should show correct error message for invalid email', function () { 37 | browser.find('#login-buttons-password').click(); 38 | expect(browser.find('.message.error-message').text()).to.contain('Invalid email'); 39 | }); 40 | 41 | it('should show correct error message when user is not found', function () { 42 | browser.find('#login-email').type('foo@bar.com'); 43 | browser.find('#login-password').type('12345'); 44 | browser.find('#login-buttons-password').click(); 45 | expect(browser.find('.message.error-message', 30000).text()).to.contain('User not found'); 46 | }); 47 | 48 | it('should require at least 6 characters for password', function () { 49 | browser.find('#signup-link').click(); 50 | browser.find('#login-email').clear().type(browserTestAccount); 51 | browser.find('#login-buttons-password').click(); 52 | expect(browser.find('.message.error-message').text()) 53 | .to.contain('Password must be at least 6 characters long'); 54 | }); 55 | 56 | it('should sign in after successfully creating a new account', function () { 57 | browser.find('#login-password').clear().type('123456'); 58 | browser.find('#login-buttons-password').click(); 59 | expect(browser.find('#login-name-link', 30000).text()).to.contain(browserTestAccount); 60 | }); 61 | 62 | it('should be able to sign out', function () { 63 | browser.find('#login-name-link').click(); 64 | browser.find('#login-buttons-logout').click(); 65 | expect(browser.find('#login-sign-in-link', 30000).text()).to.contain('Sign in ▾'); 66 | }); 67 | 68 | it('should show correct error message for incorrect password', function () { 69 | browser.find('#login-sign-in-link').click(); 70 | browser.find('#login-email').type(browserTestAccount); 71 | browser.find('#login-password').type('654321'); 72 | browser.find('#login-buttons-password').click(); 73 | expect(browser.find('.message.error-message', 30000).text()).to.contain('Incorrect password'); 74 | }); 75 | 76 | it('should be able to sign in after signing out', function () { 77 | browser.find('#login-password').clear().type('123456'); 78 | browser.find('#login-buttons-password').click(); 79 | expect(browser.find('#login-name-link', 30000).text()).to.contain(browserTestAccount); 80 | }); 81 | 82 | it('should be able to change password', function () { 83 | browser.find('#login-name-link').click(); 84 | browser.find('#login-buttons-open-change-password').click(); 85 | browser.find('#login-old-password').type('123456'); 86 | browser.find('#login-password').type('654321'); 87 | browser.find('#login-buttons-do-change-password').click(); 88 | expect(browser.find('.message.info-message', 30000).text()).to.contain('Password changed'); 89 | }); 90 | 91 | it('should be able to sign with changed password', function () { 92 | //sign out 93 | browser.find('.login-close-text').click(); 94 | browser.find('#login-name-link').click(); 95 | browser.find('#login-buttons-logout').click(); 96 | expect(browser.find('#login-sign-in-link', 30000).text()).to.contain('Sign in ▾'); 97 | // sign in again 98 | browser.find('#login-sign-in-link').click(); 99 | browser.find('#login-email').type(browserTestAccount); 100 | browser.find('#login-password').type('654321'); 101 | browser.find('#login-buttons-password').click(); 102 | expect(browser.find('#login-name-link', 30000).text()).to.contain(browserTestAccount); 103 | }); 104 | 105 | it('should show correct error message when creating an account that already exists', function () { 106 | //sign out 107 | browser.find('#login-name-link').click(); 108 | browser.find('#login-buttons-logout').click(); 109 | expect(browser.find('#login-sign-in-link', 30000).text()).to.contain('Sign in ▾'); 110 | // try to create existing account 111 | browser.find('#login-sign-in-link').click(); 112 | browser.find('#signup-link').click(); 113 | browser.find('#login-email').type(browserTestAccount); 114 | browser.find('#login-password').type('123456'); 115 | browser.find('#login-buttons-password').click(); 116 | expect(browser.find('.message.error-message', 30000).text()) 117 | .to.contain('Email already exists'); 118 | }); 119 | 120 | it('should show correct custom error message thrown in validateNewUser()', function () { 121 | browser.find('#login-email').clear().type('invalid@qa.com'); 122 | browser.find('#login-buttons-password').click(); 123 | expect(browser.find('.message.error-message', 30000).text()) 124 | .to.contain('Invalid email address'); 125 | }); 126 | 127 | }); 128 | -------------------------------------------------------------------------------- /specs/auth/oauth_providers.js: -------------------------------------------------------------------------------- 1 | // all accounts have the same email and password 2 | var email = "meteorauthqa@gmail.com"; 3 | 4 | if (!process.env.OAUTH_PROVIDERS_PASSWORD) { 5 | console.error("Need to set OAUTH_PROVIDERS_PASSWORD environment variable"); 6 | process.exit(1); 7 | } 8 | var password = process.env.OAUTH_PROVIDERS_PASSWORD; 9 | 10 | module.exports = [ 11 | { 12 | name: 'github', 13 | userDisplayName: 'Meteor AuthQA', 14 | waitForPopupContents: function () { 15 | expect(browser.find('#login', 30000).text()).to.contain("Sign in"); 16 | }, 17 | signInInPopup: function () { 18 | browser.find('#login_field').type(email); 19 | browser.find('#password').type(password); 20 | browser.find('input[name=commit]').click(); 21 | } 22 | }, 23 | { 24 | name: 'google', 25 | userDisplayName: 'Auth Meteor', 26 | waitForPopupContents: function () { 27 | expect(browser.find('h2', 30000).text()).to.contain("Sign in with your Google Account"); 28 | }, 29 | signInInPopup: function () { 30 | // Update Sep. 2015: 31 | // Google now uses a different login UX when the browser supports CSS 32 | // transitions, but keeps the old UX in older browsers (IE8,IE9) 33 | // we need to execute different test steps here 34 | var usingNewUX = true; 35 | try { 36 | browser.find('#next'); 37 | } catch (e) { 38 | usingNewUX = false; 39 | } 40 | if (usingNewUX) { 41 | browser.find('#Email').type(email); 42 | browser.find('#next').click(); 43 | browser.find('#Passwd', 3000).type(password); 44 | browser.find('#signIn').click(); 45 | } else { 46 | browser.find('#Email').type(email); 47 | browser.find('#Passwd').type(password); 48 | browser.find('#signIn').click(); 49 | } 50 | } 51 | }, 52 | { 53 | name: 'facebook', 54 | userDisplayName: 'Auth Meteor', 55 | waitForPopupContents: function () { 56 | expect(browser.find('#login_form', 30000).text()).to.contain("Meteor Auth QA"); 57 | }, 58 | signInInPopup: function () { 59 | browser.find('#email').type(email); 60 | browser.find('#pass').type(password); 61 | browser.find('input[name=login]').click(); 62 | }, 63 | cancelSignIn: function () { 64 | browser.find('input[name=cancel]').click(); 65 | } 66 | }, 67 | { 68 | name: 'twitter', 69 | userDisplayName: 'Auth Meteor', 70 | waitForPopupContents: function () { 71 | if (browser.name === "safari") { 72 | // For some reason, on Twitter login under Safari-on-Selenium, 73 | // trying to run any action immediately after focusing the 74 | // popup window leads to Selenium hanging. All webdriver 75 | // operations seem to exhibit the same behavior. So 76 | // unfortunately, we use a long timeout to wait for the popup 77 | // to "stabilize" enough for Selenium operations to not hang. 78 | browser.sleep(10000); 79 | } 80 | expect(browser.find('div.auth h2', 30000).text()).to.contain("Authorize Meteor Auth QA"); 81 | }, 82 | signInInPopup: function () { 83 | browser.find('#username_or_email').type(email); 84 | browser.find('#password').type(password); 85 | browser.find('#allow').click(); 86 | // Mysteriously, on some browsers, Twitter requires also 87 | // clicking on "Authorize App" on every sign in. 88 | // 89 | // Hack: poll every sec to see if the popup is still open. 90 | // if it's closed, it means we've successfully signed in; otherwise, 91 | // check the #allow button's value (it's an tag, for whatever 92 | // reason!) - if it's "Authorize app" then we click it again, otherwise 93 | // keep polling until popup closes. 94 | while (true) { 95 | browser.sleep(1000); 96 | var windowCount = browser.windowHandles().length; 97 | if (windowCount > 1) { 98 | // popup still open 99 | var button; 100 | var buttonText; 101 | try { 102 | button = browser.find('#allow'); 103 | buttonText = button.getValue(); 104 | } catch (e) { 105 | // if we get an error here, it's a stale element 106 | // reference, so the popup was closed when we were getting 107 | // the button text; we're done. 108 | break; 109 | } 110 | // second click is required. 111 | if (buttonText === 'Authorize app') { 112 | button.click(); 113 | break; 114 | } 115 | } else { 116 | break; 117 | } 118 | } 119 | } 120 | }, 121 | { 122 | name: 'meteor-developer', 123 | userDisplayName: 'meteorauthqa', 124 | waitForPopupContents: function () { 125 | expect(browser.find('div.header', 30000).text()).to.contain("Meteor Auth QA"); 126 | }, 127 | signInInPopup: function () { 128 | browser.find('input[name=usernameOrEmail]').type(email); 129 | browser.find('input[name=password]').type(password); 130 | browser.find('input[type=submit]').click(); 131 | }, 132 | signInInSecondPopup: function () { 133 | browser.find('a.login-with-account').click(); // "Use this account" 134 | } 135 | }, 136 | { 137 | name: 'meetup', 138 | userDisplayName: 'Auth Meteor', 139 | waitForPopupContents: function () { 140 | if (browser.name === "safari") { 141 | // For some reason, on Meetup login under Safari-on-Selenium, 142 | // trying to run any action immediately after focusing the 143 | // popup window leads to Selenium hanging. All webdriver 144 | // operations seem to exhibit the same behavior. So 145 | // unfortunately, we use a long timeout to wait for the popup 146 | // to "stabilize" enough for Selenium operations to not hang. 147 | browser.sleep(10000); 148 | } 149 | expect(browser.find('#paneLogin', 30000).text()).to.contain("Meteor Auth QA"); 150 | }, 151 | signInInPopup: function () { 152 | browser.find('#email').type(email); 153 | browser.find('#password').type(password); 154 | browser.find('input[type=submit]').click(); 155 | } 156 | } 157 | 158 | // Weibo is excluded from the tests because it requires Captcha in an 159 | // unpredictable fashion. 160 | 161 | // , { 162 | // name: 'weibo', 163 | // userDisplayName: 'AuthMeteor', 164 | // waitForPopupContents: function () { 165 | // expect(browser.find('p.oauth_main_info', 30000).text()).to.contain("meteor_auth_qa"); 166 | // }, 167 | // signInInPopup: function () { 168 | // browser.find('#userId').type(email); 169 | // browser.find('#passwd').type(password); 170 | // browser.find('a[action-type=submit]').click(); 171 | // } 172 | // } 173 | ]; 174 | 175 | // by default, we can reuse the same sign-in behavior in 176 | // both popup and redirect flows, unless explicitly 177 | // overwritten. 178 | module.exports.forEach(function (provider) { 179 | if (!provider.waitForRedirectPage) { 180 | provider.waitForRedirectPage = provider.waitForPopupContents; 181 | } 182 | if (!provider.signInInRedirectPage) { 183 | provider.signInInRedirectPage = provider.signInInPopup; 184 | } 185 | if (provider.signInInSecondPopup && !provider.signInInSecondRedirect) { 186 | provider.signInInSecondRedirect = provider.signInInSecondPopup; 187 | } 188 | }); 189 | -------------------------------------------------------------------------------- /lib/test_env.js: -------------------------------------------------------------------------------- 1 | // Initialize the test environment in which the actual test 2 | // files will be run. 3 | // 4 | // - attach global utlities, e.g. browser, expect, find 5 | // - attach custom convenience methods to the browser, e.g. 6 | // methods that focus on the main window or popup. 7 | 8 | var wd = require('./wd_with_sync'); 9 | var chai = require('chai'); 10 | var config = require('../config'); 11 | var _ = require('underscore'); 12 | var Fiber = require('fibers'); 13 | var Future = require('fibers/future'); 14 | var fiberUtils = require('./fiber_utils'); 15 | 16 | /** 17 | * Wrap a function into a wd waitForElement asserter. The wrapped function 18 | * receives a fiber-wrapped wd Element instance, and should return a Boolean 19 | * value. wd will poll the asserter until it returns true. This is useful when 20 | * we expect an existing element to change but don't know how long we should 21 | * wait. 22 | * 23 | * For example: 24 | * browser.wait('#login-name-link', 30000, function (el) { 25 | * return el.text().indexOf('changed text') > -1; 26 | * }); 27 | */ 28 | var elementMethods = Object.keys(require('wd/lib/element-commands')); 29 | var wrapAsserter = function (fn) { 30 | if (fn) { 31 | return new wd.asserters.Asserter(function (el, cb) { 32 | el = fiberUtils.wrapAsyncObject(el, elementMethods); 33 | Fiber(function () { 34 | var res; 35 | try { 36 | res = fn(el); 37 | } catch (e) { 38 | // due to network connection latency, when we execute element 39 | // methods in the asserter, the element could have been redrawn 40 | // by Blaze, thus causing a stale element reference error in 41 | // Selenium. In that case we simply return false and let wd poll again 42 | // to get the reference to the new element. 43 | // 44 | // http://docs.seleniumhq.org/exceptions/stale_element_reference.jsp 45 | if (e.toString().indexOf('StaleElementReference') > -1) { 46 | return cb(null, false); 47 | } else { 48 | return cb(e); 49 | } 50 | } 51 | cb(null, res); 52 | }).run(); 53 | }); 54 | } else { 55 | return new wd.asserters.Asserter(function (el, cb) { 56 | cb(null, true); 57 | }); 58 | } 59 | }; 60 | 61 | exports.init = function (options, callback) { 62 | 63 | // attach chai.expect to global 64 | global.expect = chai.expect; 65 | 66 | // Create wd browser instance. 67 | // Are we running remote on saucelabs? Defaults to remote. 68 | var wdOptions = options.local ? undefined : config.remote(); 69 | var browser = global.browser = wd.remote(wdOptions); 70 | 71 | // XXX add custom methods to the browser here. 72 | 73 | /** 74 | * Convenience method that waits for an element by CSS selector. 75 | * 76 | * @param {String|Array} selectors 77 | * @param {Number} timeout - optional. 78 | * @param {Function} asserter - optional. 79 | */ 80 | browser.wait = function (selectors, timeout, asserter) { 81 | timeout = timeout || 1000; 82 | // wrap the asserter function. 83 | asserter = wrapAsserter(asserter); 84 | var wait = function (selector) { 85 | browser.waitForElementByCssSelector(selector, asserter, timeout); 86 | }; 87 | if (Array.isArray(selectors)) { 88 | selectors.forEach(wait); 89 | } else { 90 | wait(selectors); 91 | } 92 | }; 93 | 94 | /** 95 | * Convenience method that waits for and retrives one single element 96 | * by CSS selector. 97 | * 98 | * @param {String} selector 99 | * @param {Number} timeout - optional. 100 | * @return {WebDriverElement} 101 | */ 102 | browser.find = function (selector, timeout) { 103 | browser.wait(selector, timeout); 104 | var elements = browser.elementsByCssSelector(selector); 105 | expect(elements.length).to.equal(1); 106 | return elements[0]; 107 | }; 108 | 109 | /** 110 | * Convenience method that waits for and retrives all elements matching 111 | * given selector. 112 | * 113 | * @param {String} selector 114 | * @param {Number} timeout - optional. 115 | * @return {Array} 116 | */ 117 | browser.findAll = function (selector, timeout) { 118 | browser.wait(selector, timeout); 119 | return browser.elementsByCssSelector(selector); 120 | }; 121 | 122 | /** 123 | * Count how many matched elements there are the page. 124 | * 125 | * @param {String} selector 126 | * @return {Number} 127 | */ 128 | browser.count = function (selector) { 129 | return browser.elementsByCssSelector(selector).length; 130 | }; 131 | 132 | /** 133 | * Wait for the given timeout. 134 | * 135 | * @param {Number} timeout 136 | */ 137 | browser.sleep = function (timeout) { 138 | var fut = new Future; 139 | var cb = fut.resolver(); 140 | Fiber(function () { 141 | setTimeout(cb, timeout); 142 | }).run(); 143 | fut.wait(); 144 | }; 145 | 146 | // to be set after browser has initialized 147 | var mainWindowHandle; 148 | 149 | /** 150 | * Convenience method that focuses the main window 151 | */ 152 | browser.focusMainWindow = function () { 153 | browser.window(mainWindowHandle); 154 | if (browser.name.match(/ie/)) { 155 | // Fix Selenium IE-only bug: after switching window, the first click serve 156 | // as the "focus" click and won't trigger click events, so we make that 157 | // click here to make sure subsequent clicks work as intended. 158 | browser.find('body').click(); 159 | } 160 | }; 161 | 162 | /** 163 | * Convenience method that focuses the popup. 164 | * This assumes we are dealing with only 1 popup window. 165 | */ 166 | browser.focusSecondWindow = function () { 167 | var popups = _.without(browser.windowHandles(), mainWindowHandle); 168 | expect(popups).to.have.length(1); 169 | browser.window(popups[0]); 170 | // click to focus for IE 171 | if (browser.name.match(/ie/)) { 172 | // Fix Selenium IE-only bug: after switching window, the first click serve 173 | // as the "focus" click and won't trigger click events, so we make that 174 | // click here to make sure subsequent clicks work as intended. 175 | browser.find('body').click(); 176 | } 177 | }; 178 | 179 | // Which browser are we testing? 180 | var browserOptions = config.browserDescriptors[options.browser]; 181 | if (!browserOptions) { 182 | console.error('Invalid browser: ' + options.browser); 183 | process.exit(1); 184 | } 185 | 186 | // add this so we know what browser we are in 187 | browser.name = options.browser; 188 | 189 | // When running locally on OSX the "platform" field causes a selenium error. 190 | if (options.local && 191 | (options.browser === 'chrome' || 192 | options.browser === 'safari')) { 193 | delete browserOptions.platform; 194 | } 195 | 196 | // use the test directories as tags so we know what tests were 197 | // run on SauceLabs 198 | browserOptions.tags = options._.length ? options._ : ['all']; 199 | // connect the browser, then call the callback. 200 | browser.init(browserOptions, function () { 201 | // store main window handle 202 | var windowHandles = browser.windowHandles(); 203 | expect(windowHandles).to.have.length(1); 204 | mainWindowHandle = windowHandles[0]; 205 | 206 | if (!options.local) { 207 | var sauceLabsUrl = "https://saucelabs.com/tests/" + browser.sessionID; 208 | console.log("Follow this browser run on Sauce Labs: " + sauceLabsUrl); 209 | 210 | // Save the Sauce Labs URL so that it can be printed in the failure banner 211 | browser.sauceLabsUrl = sauceLabsUrl; 212 | } 213 | 214 | // Done! 215 | callback(); 216 | }); 217 | }; 218 | 219 | exports.teardown = function (code) { 220 | global.browser.quit(function () { 221 | process.exit(code); 222 | }); 223 | }; 224 | -------------------------------------------------------------------------------- /specs/auth/email.js: -------------------------------------------------------------------------------- 1 | var browserTestAccount; 2 | // var testURL = 'localhost:3000'; 3 | var testURL = 'rainforest-auth-qa.meteor.com'; 4 | var emailLinkRegex = new RegExp('http:\\/\\/' + testURL + '\\/#\\/[a-zA-z-_\\d\\/]+'); 5 | 6 | // assert content on the first email in the list 7 | var assertEmail = function (options) { 8 | for (var key in options) { 9 | expect(browser.find('.email-log:first-child .email-' + key, 30000).text()) 10 | .to.contain(options[key]); 11 | } 12 | }; 13 | 14 | // open a new window, and login with a different test account 15 | var openNewWindowAndLogin = function () { 16 | browser.newWindow('http://' + testURL); 17 | browser.focusSecondWindow(); 18 | // IE opens a tiny new window which makes screenshots too small 19 | browser.setWindowSize(800, 600); 20 | browser.find('#login-sign-in-link', 30000).click(); 21 | browser.find('#login-email').type('email@qa.com'); 22 | browser.find('#login-password').type('123456'); 23 | browser.find('#login-buttons-password').click(); 24 | expect(browser.find('#login-name-link', 30000).text()) 25 | .to.contain('email@qa.com'); 26 | }; 27 | 28 | // go to the linked found in the top-most email 29 | var goToLinkInEmail = function () { 30 | var text = browser.find('.email-log:first-child .email-text').text(); 31 | var match = text.match(emailLinkRegex); 32 | expect(match).to.exist; 33 | browser.get(match[0]); 34 | browser.refresh(); // force reload because it's a hash link 35 | }; 36 | 37 | var assertSignedIn = function () { 38 | expect(browser.find('#login-name-link', 30000).text()) 39 | .to.contain(browserTestAccount); 40 | }; 41 | 42 | var signOut = function () { 43 | browser.find('#login-name-link', 30000).click(); 44 | browser.find('#login-buttons-logout').click(); 45 | }; 46 | 47 | var assertSignedOut = function () { 48 | expect(browser.find('#login-sign-in-link', 30000).text()) 49 | .to.contain('Sign in ▾'); 50 | }; 51 | 52 | var closeSecondWindow = function () { 53 | browser.focusSecondWindow(); 54 | browser.close(); 55 | browser.focusMainWindow(); 56 | }; 57 | 58 | describe('Auth Email -', function () { 59 | 60 | before(function () { 61 | browser.get('http://' + testURL); 62 | browser.wait('#email-logs', 30000); 63 | // cache browser test account 64 | browserTestAccount = browser.find('#browser-email').text(); 65 | // clear email logs before we start the test 66 | browser.find('#clear-email-logs').click(); 67 | browser.wait('#server-action-ok', 30000); 68 | expect(browser.count('.email-log')).to.equal(0); 69 | }); 70 | 71 | describe('Forgot Password', function () { 72 | 73 | // these steps are sequential and stateful in nature, so stop 74 | // after first failures. 75 | this.bail(true); 76 | 77 | it('should send correct email', function () { 78 | browser.find('#create-test-account').click(); 79 | browser.wait('#server-action-ok', 30000); 80 | browser.find('#login-sign-in-link').click(); 81 | browser.find('#forgot-password-link').click(); 82 | browser.find('#forgot-password-email').type(browserTestAccount); 83 | browser.find('#login-buttons-forgot-password').click(); 84 | expect(browser.find('.message.info-message', 3000).text()) 85 | .to.contain('Email sent'); 86 | assertEmail({ 87 | from: 'Meteor Accounts ', 88 | to: browserTestAccount, 89 | subject: 'How to reset your password on ' + testURL, 90 | text: 'Hello, To reset your password, simply click the link below. ' + 91 | 'http://' + testURL + '/#/reset-password/' 92 | }); 93 | }); 94 | 95 | it('should not be logged in when following the email link', function () { 96 | openNewWindowAndLogin(); 97 | browser.focusMainWindow(); 98 | goToLinkInEmail(); 99 | assertSignedOut(); 100 | }); 101 | 102 | it('should log in after resetting the password', function () { 103 | browser.find('#reset-password-new-password').type('654321'); 104 | browser.find('#login-buttons-reset-password-button').click(); 105 | // expect logged in 106 | assertSignedIn(); 107 | expect(browser.find('.accounts-dialog').text()) 108 | .to.contain('Password reset. You are now logged in as ' + browserTestAccount); 109 | browser.find('#just-verified-dismiss-button').click(); 110 | }); 111 | 112 | it('should transfer the login to another tab', function () { 113 | browser.focusSecondWindow(); 114 | browser.wait('#login-name-link', 30000, function (el) { 115 | return el.text().indexOf(browserTestAccount) > -1; 116 | }); 117 | }); 118 | 119 | it('sign out should affect both tabs', function () { 120 | signOut(); 121 | assertSignedOut(); 122 | browser.focusMainWindow(); 123 | assertSignedOut(); 124 | }); 125 | 126 | it('should not be able to login with old password', function () { 127 | browser.find('#login-sign-in-link').click(); 128 | browser.find('#login-email').type(browserTestAccount); 129 | browser.find('#login-password').type('123456'); 130 | browser.find('#login-buttons-password').click(); 131 | expect(browser.find('.message.error-message', 30000).text()) 132 | .to.contain('Incorrect password'); 133 | }); 134 | 135 | it('should be able to login with changed password', function () { 136 | browser.find('#login-password').clear().type('654321'); 137 | browser.find('#login-buttons-password').click(); 138 | assertSignedIn(); 139 | }); 140 | 141 | it('should not be able to use the same reset link again', function () { 142 | goToLinkInEmail(); 143 | browser.find('#reset-password-new-password', 30000).type('123456'); 144 | browser.find('#login-buttons-reset-password-button').click(); 145 | expect(browser.find('.accounts-dialog .error-message', 30000).text()) 146 | .to.contain('Token expired'); 147 | }); 148 | 149 | after(function () { 150 | closeSecondWindow(); 151 | }); 152 | 153 | }); 154 | 155 | describe('Verification Email', function () { 156 | 157 | // these steps are sequential and stateful in nature, so stop 158 | // after first failures. 159 | this.bail(true); 160 | 161 | before(function () { 162 | browser.refresh(); 163 | browser.find('#remove-test-account').click(); 164 | browser.wait('#server-action-ok', 30000); 165 | assertSignedOut(); // delete account should sign out 166 | }); 167 | 168 | it('should send correct email when creating account', function () { 169 | browser.find('#login-sign-in-link').click(); 170 | browser.find('#signup-link').click(); 171 | browser.find('#login-email').type(browserTestAccount); 172 | browser.find('#login-password').type('123456'); 173 | browser.find('#login-buttons-password').click(); 174 | assertSignedIn(); 175 | assertEmail({ 176 | from: 'Meteor Accounts ', 177 | to: browserTestAccount, 178 | subject: 'How to verify email address on ' + testURL, 179 | text: 'Hello, To verify your account email, simply click the link below. ' + 180 | 'http://' + testURL + '/#/verify-email/' 181 | }); 182 | signOut(); 183 | assertSignedOut(); 184 | }); 185 | 186 | it('should be logged in when following the email link', function () { 187 | openNewWindowAndLogin(); 188 | browser.focusMainWindow(); 189 | goToLinkInEmail(); 190 | // expect signed in 191 | assertSignedIn(); 192 | expect(browser.find('.accounts-dialog', 30000).text()) 193 | .to.contain('Email verified. You are now logged in as ' + browserTestAccount); 194 | browser.find('#just-verified-dismiss-button').click(); 195 | }); 196 | 197 | it('should transfer the login to another tab', function () { 198 | browser.focusSecondWindow(); 199 | browser.wait('#login-name-link', 30000, function (el) { 200 | return el.text().indexOf(browserTestAccount) > -1; 201 | }); 202 | }); 203 | 204 | it('sign out should affect both tabs', function () { 205 | signOut(); 206 | assertSignedOut(); 207 | browser.focusMainWindow(); 208 | assertSignedOut(); 209 | }); 210 | 211 | after(function () { 212 | closeSecondWindow(); 213 | }); 214 | 215 | }); 216 | 217 | describe('Accounts.sendEnrollmentEmail', function () { 218 | 219 | // these steps are sequential and stateful in nature, so stop 220 | // after first failures. 221 | this.bail(true); 222 | 223 | before(function () { 224 | browser.refresh(); 225 | }); 226 | 227 | it('should send correct email', function () { 228 | browser.find('#test-send-enrollment-email').click(); 229 | browser.wait('#server-action-ok', 30000); 230 | assertEmail({ 231 | from: 'Meteor Accounts ', 232 | to: browserTestAccount, 233 | subject: 'An account has been created for you on ' + testURL, 234 | text: 'Hello, To start using the service, simply click the link below. ' + 235 | 'http://' + testURL + '/#/enroll-account/' 236 | }); 237 | }); 238 | 239 | it('should not be logged in when following the email link', function () { 240 | openNewWindowAndLogin(); 241 | browser.focusMainWindow(); 242 | goToLinkInEmail(); 243 | assertSignedOut(); 244 | }); 245 | 246 | it('should be able to log in after setting password', function () { 247 | browser.find('#enroll-account-password').type('123456'); 248 | browser.find('#login-buttons-enroll-account-button').click(); 249 | // expect logged in 250 | assertSignedIn(); 251 | }); 252 | 253 | it('should transfer the login to another tab', function () { 254 | browser.focusSecondWindow(); 255 | browser.wait('#login-name-link', 30000, function (el) { 256 | return el.text().indexOf(browserTestAccount) > -1; 257 | }); 258 | }); 259 | 260 | it('sign out should affect both tabs', function () { 261 | signOut(); 262 | assertSignedOut(); 263 | browser.focusMainWindow(); 264 | assertSignedOut(); 265 | }); 266 | 267 | it('should be able to login with new password', function () { 268 | browser.find('#login-sign-in-link').click(); 269 | browser.find('#login-email').type(browserTestAccount); 270 | browser.find('#login-password').type('123456'); 271 | browser.find('#login-buttons-password').click(); 272 | assertSignedIn(); 273 | }); 274 | 275 | after(function () { 276 | closeSecondWindow(); 277 | signOut(); 278 | }); 279 | 280 | }); 281 | 282 | }); 283 | --------------------------------------------------------------------------------