├── 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 |
2 |
11 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
Email Logs
4 |
5 | {{#each logs}}
6 |
7 | at: {{timestamp}}
8 | from: {{from}}
9 | to: {{to}}
10 | subject: {{subject}}
11 | {{text}}
12 |
13 | {{/each}}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------