diff --git a/nightwatch.json b/nightwatch.json index c3ffa2766..e7ee17f63 100644 --- a/nightwatch.json +++ b/nightwatch.json @@ -1,7 +1,7 @@ { - "src_folders": ["spec/smoke"], + "src_folders": ["spec/smoke/tests"], "output_folder": "spec/smoke/reports", - "custom_commands_path": "", + "custom_commands_path": "spec/smoke/commands", "custom_assertions_path": "", "page_objects_path": "", "globals_path": "", diff --git a/spec/smoke/commands/waitForApplicationLoaded.js b/spec/smoke/commands/waitForApplicationLoaded.js new file mode 100644 index 000000000..9bf80bddd --- /dev/null +++ b/spec/smoke/commands/waitForApplicationLoaded.js @@ -0,0 +1,29 @@ +constants = require('../constants'); + +exports.command = function(callback) { + return this + .timeoutsAsyncScript(constants.ASYNC_TIMEOUT * 4) // Make it extra long for production + .executeAsync(function(done) { + + waitForApplication = function () { + if (window.application) { waitForCurrentView() } + else { setTimeout(waitForApplication, 1) } + } + + waitForCurrentView = function () { + if (window.currentView && window.currentView.supermodel) { waitForCurrentViewToLoad() } + else { setTimeout(waitForCurrentView, 1) } + } + + waitForCurrentViewToLoad = function () { + if (window.currentView.supermodel.finished()) { done() } + else { setTimeout(waitForCurrentViewToLoad) } + } + + waitForApplication(); + }, [], function(result) { + if(result.error) + console.log('waitForApplicationLoaded error:', result.error, result) + }) + .pause(constants.PAUSE_TIME) +}; diff --git a/spec/smoke/commands/waitForElementVisibleAndClick.js b/spec/smoke/commands/waitForElementVisibleAndClick.js new file mode 100644 index 000000000..df80d661b --- /dev/null +++ b/spec/smoke/commands/waitForElementVisibleAndClick.js @@ -0,0 +1,8 @@ +constants = require('../constants'); + +exports.command = function(selector) { + return this + .waitForElementVisible(selector, constants.ASYNC_TIMEOUT) + .pause(constants.PAUSE_TIME) + .click(selector); +}; diff --git a/spec/smoke/commands/waitForModalLoaded.js b/spec/smoke/commands/waitForModalLoaded.js new file mode 100644 index 000000000..7c6a25cb4 --- /dev/null +++ b/spec/smoke/commands/waitForModalLoaded.js @@ -0,0 +1,21 @@ +constants = require('../constants'); + +exports.command = function() { + return this + .timeoutsAsyncScript(constants.ASYNC_TIMEOUT) + .executeAsync(function(done) { + try { + window.currentModal.supermodel.finishLoading() + .then(function() { done(); }) + .catch(function(e) { console.error('Promise error', e); done(e); }); + } + catch (e) { + console.error('Caught error:', e); + done(e); + } + }, [], function(result) { + if(result.error) + console.log('waitForModalLoaded error:', result.error) + }) + .pause(constants.PAUSE_TIME) +}; diff --git a/spec/smoke/commands/waitForViewLoaded.js b/spec/smoke/commands/waitForViewLoaded.js new file mode 100644 index 000000000..3b95defba --- /dev/null +++ b/spec/smoke/commands/waitForViewLoaded.js @@ -0,0 +1,21 @@ +constants = require('../constants'); + +exports.command = function() { + return this + .timeoutsAsyncScript(constants.ASYNC_TIMEOUT) + .executeAsync(function(done) { + try { + window.currentView.supermodel.finishLoading() + .then(function() { done(); }) + .catch(function(e) { console.error('Promise error', e); done(e); }); + } + catch (e) { + console.error('Caught error:', e); + done(e); + } + }, [], function(result) { + if(result.error) + console.log('waitForViewLoaded error:', result.error) + }) + .pause(constants.PAUSE_TIME); +}; diff --git a/spec/smoke/constants.js b/spec/smoke/constants.js new file mode 100644 index 000000000..823c1a2d8 --- /dev/null +++ b/spec/smoke/constants.js @@ -0,0 +1,23 @@ +switch (process.env.COCO_SMOKE_DOMAIN) { + case "local": + module.exports.DOMAIN = 'http://localhost:3000'; + break; + case "next": + module.exports.DOMAIN = 'http://next.codecombat.com'; + break; + case "staging": + module.exports.DOMAIN = 'http://staging.codecombat.com'; + break; + case "prod": + module.exports.DOMAIN = 'https://codecombat.com'; + break; + default: + module.exports.DOMAIN = 'http://localhost:3000'; +} + +// General time to wait for elements to appear +module.exports.ASYNC_TIMEOUT = 8000; + +// Used after an element appears, before an action occurs, to give the UI time to catch up, +// and to make the smoke test more 'watchable'. +module.exports.PAUSE_TIME = 300; diff --git a/spec/smoke/individual-account-lifecycle.js b/spec/smoke/individual-account-lifecycle.js deleted file mode 100644 index e6ce06395..000000000 --- a/spec/smoke/individual-account-lifecycle.js +++ /dev/null @@ -1,101 +0,0 @@ -WAIT_TIMEOUT = 8000; - -// TODO: Refactor into shared file -switch (process.env.COCO_SMOKE_DOMAIN) { - case "local": - DOMAIN = 'http://localhost:3000'; - break; - case "next": - DOMAIN = 'http://next.codecombat.com'; - break; - case "staging": - DOMAIN = 'http://staging.codecombat.com'; - break; - case "prod": - DOMAIN = 'https://codecombat.com'; - break; - default: - DOMAIN = 'http://localhost:3000'; -} - -var timestamp = new Date().getTime(), - email = `email${timestamp}@${timestamp}.com`, - name = timestamp.toString(), - password = timestamp.toString(); - -module.exports = { - 'Sign up': function (browser) { - - browser - // Go to home page - .url(DOMAIN) - .resizeWindow(1250, 900) - - // Open login modal - .executeAsync(function(done) { window.currentView.supermodel.finishLoading.then(done); }) - .click('#create-account-link') - - // Sign up - .waitForElementVisible('.individual-path-button', WAIT_TIMEOUT) - .click('.individual-path-button') - .waitForElementVisible('#birthday-month-input', WAIT_TIMEOUT) - .setValue('#birthday-month-input', 'January') - .setValue('#birthday-day-input', '1') - .setValue('#birthday-year-input', '1999') - .click('.next-button') - .waitForElementVisible('input[name="email"]', WAIT_TIMEOUT) - .setValue('input[name="email"]', email) - .setValue('input[name="name"]', name) - .setValue('input[name="password"]', password) - .click('#subscribe-input') - .pause(100) // Sometimes create account button does not get clicked - .click('#create-account-btn') - .waitForElementVisible('#start-btn', WAIT_TIMEOUT) - .click('#start-btn') - - // Confirm we went to campaign view, navigate back to home - .waitForElementVisible('#logout-button', WAIT_TIMEOUT * 3) // takes particularly long - .assert.urlContains('/play') - }, - - 'Logout': function (browser) { - browser - .url(DOMAIN) - .executeAsync(function (done) { - window.currentView.supermodel.finishLoading.then(done); - }) - .click('.dropdown-toggle') - .waitForElementVisible('.dropdown #logout-button', WAIT_TIMEOUT) - .click('.dropdown #logout-button') - }, - - 'Log back in': function (browser) { - browser - // Log back in - .waitForElementVisible('#login-link', WAIT_TIMEOUT) - .click('#login-link') - .waitForElementVisible('#login-btn', WAIT_TIMEOUT) - .setValue('input#username-or-email-input', email) - .setValue('input#password-input', password) - .click('#login-btn') - .pause(100) - .waitForElementVisible('#main-nav', WAIT_TIMEOUT) - }, - - 'Delete account': function (browser) { - browser - // Delete account - .url(`${DOMAIN}/account/settings`) - .pause(100) - .executeAsync(function(done) { window.currentView.supermodel.finishLoading.then(done); }) - .waitForElementVisible('#delete-account-email-or-username', WAIT_TIMEOUT) - .setValue('#delete-account-email-or-username', email) - .setValue('#delete-account-password', password) - .click('#delete-account-btn') - .waitForElementVisible('#confirm-button', WAIT_TIMEOUT) - .click('#confirm-button') - .end(); - } -}; - - diff --git a/spec/smoke/tests/individual-account-lifecycle.js b/spec/smoke/tests/individual-account-lifecycle.js new file mode 100644 index 000000000..c5af0c817 --- /dev/null +++ b/spec/smoke/tests/individual-account-lifecycle.js @@ -0,0 +1,92 @@ +constants = require('../constants'); + +var timestamp = new Date().getTime(), + email = `email${timestamp}@${timestamp}.com`, + name = timestamp.toString(), + password = timestamp.toString(); + +module.exports = { + 'Sign up': function (browser) { + + browser + // Go to home page + .url(constants.DOMAIN) + .resizeWindow(1250, 900) + + // Open login modal + .waitForApplicationLoaded() + .click('#create-account-link') + + // Sign up + .waitForModalLoaded() + .click('.individual-path-button') + .waitForElementVisibleAndClick('#birthday-month-input') + .setValue('#birthday-month-input', 'January') + .setValue('#birthday-day-input', '1') + .setValue('#birthday-year-input', '1999') + .pause(constants.PAUSE_TIME) + .click('.next-button') + .waitForElementVisible('input[name="email"]', constants.ASYNC_TIMEOUT) + .executeAsync(function(done) { + // If G+ or FB load in the middle of execution, they re-render the modal. This code waits for both + // to load before continuing. TODO: Refactor code so this is unnecessary. + check = function() { + if(currentModal.signupState.get('facebookEnabled') && currentModal.signupState.get('gplusEnabled')) { + done() + } + } + currentModal.signupState.on('change', check); + check(); + }, [], function(res) { if(res.error) { console.error('G+/FB wait error:', res.error) } }) + .pause(constants.PAUSE_TIME) + .setValue('input[name="email"]', email) + .setValue('input[name="name"]', name) + .setValue('input[name="password"]', password) + .click('#subscribe-input') + .pause(constants.PAUSE_TIME*2) + .click('#create-account-btn') + .waitForElementVisibleAndClick('#start-btn') + + // Confirm we went to campaign view, navigate back to home + .waitForElementVisible('#logout-button', constants.ASYNC_TIMEOUT * 3) // takes particularly long + .assert.urlContains('/play') + }, + + 'Logout': function (browser) { + browser + .url(constants.DOMAIN) + .waitForViewLoaded() + .pause(constants.PAUSE_TIME) + .click('.dropdown-toggle') + .waitForElementVisibleAndClick('.dropdown #logout-button') + }, + + 'Log back in': function (browser) { + browser + .waitForElementVisibleAndClick('#login-link') + .waitForElementVisible('#login-btn', constants.ASYNC_TIMEOUT) + .setValue('input#username-or-email-input', email) + .setValue('input#password-input', password) + .pause(constants.PAUSE_TIME) + .click('#login-btn') + .waitForElementVisible('#main-nav', constants.ASYNC_TIMEOUT) + .pause(constants.PAUSE_TIME) + }, + + 'Delete account': function (browser) { + browser + .url(`${constants.DOMAIN}/account/settings`) + .pause(constants.PAUSE_TIME) + .waitForViewLoaded() + .waitForElementVisible('#delete-account-email-or-username', constants.ASYNC_TIMEOUT) + .setValue('#delete-account-email-or-username', email) + .setValue('#delete-account-password', password) + .pause(constants.PAUSE_TIME) + .click('#delete-account-btn') + .waitForElementVisibleAndClick('#confirm-button') + .pause(constants.PAUSE_TIME*2) + .end(); + } +}; + + diff --git a/spec/smoke/tests/play-as-anonymous.js b/spec/smoke/tests/play-as-anonymous.js new file mode 100644 index 000000000..2802f9ad2 --- /dev/null +++ b/spec/smoke/tests/play-as-anonymous.js @@ -0,0 +1,45 @@ +constants = require('../constants'); + +module.exports = { + 'Go to dungeon campaign': function (browser) { + browser + .url(constants.DOMAIN + '/play/dungeon') + .resizeWindow(1250, 900) + .waitForApplicationLoaded() + }, + + 'Go to level view for Dungeons of Kithgard': function (browser) { + browser + .click('a[data-level-slug="dungeons-of-kithgard"]') + .pause(constants.PAUSE_TIME) + .click('.start-level') + .pause(constants.PAUSE_TIME) + .waitForModalLoaded() + .waitForElementVisibleAndClick('#confirm-button') + .waitForElementVisibleAndClick('.btn.equip-item') + .click('#play-level-button') + .pause(constants.PAUSE_TIME) + }, + + 'Play Dungeons of Kithgard': function (browser) { + browser + .waitForElementVisibleAndClick('button.start-level-button') + .keys([browser.Keys.ESCAPE]) + .pause(constants.PAUSE_TIME) + .keys('hero.moveDown()\nhero.moveRight()\n') + .pause(constants.PAUSE_TIME) + .click('.cast-button') + .pause(constants.PAUSE_TIME) + .waitForElementVisibleAndClick('.done-button') + }, + + 'Go through victory modal, check that Gems in the Deep is unlocked': function (browser) { + browser + .waitForModalLoaded() + .click('#continue-button') + .pause(constants.PAUSE_TIME) + .waitForElementVisible('a[data-level-slug="gems-in-the-deep"]', constants.ASYNC_TIMEOUT) + .pause(constants.PAUSE_TIME) + .end() + } +}