diff --git a/circle.yml b/circle.yml index 932643ff6..9f35736c6 100644 --- a/circle.yml +++ b/circle.yml @@ -50,7 +50,7 @@ test: - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test # Check dependancies using nsp. - nsp check - # - yarn e2e-ci + - yarn e2e-ci deployment: release: diff --git a/nightwatch-browserstack.conf.js b/nightwatch-browserstack.conf.js index 9e8e334c5..d79f6917a 100644 --- a/nightwatch-browserstack.conf.js +++ b/nightwatch-browserstack.conf.js @@ -26,18 +26,29 @@ const nightwatch_config = { // Disable this, as it makes bs slow and brittle. 'browserstack.networkLogs': false, - } + 'browserstack.resolution': '1600x1200', + }, + screenshots : { + enabled: true, + on_failure: true, + on_error: true, + path: process.env.REPORTS_FOLDER || './test/e2e/reports', + }, }, chrome: { desiredCapabilities: { browser: 'chrome', browser_version: '62', + os: 'Windows', + os_version: '10', }, }, firefox: { desiredCapabilities: { browser: 'firefox', browser_version: '56', + os: 'Windows', + os_version: '10', }, }, safari: { @@ -52,14 +63,21 @@ const nightwatch_config = { desiredCapabilities: { browser: 'internet explorer', os: 'Windows', - os_version: '8.1', + os_version: '10', browser_version: '11', + + // The x64 bit IEDriver that is used by IE 11 has a known issue with sendKeys where + // it may enter incorrect keys (shift + key). + // This adds a delay for each character as temporary fix as advised from the browserstack support. + 'browserstack.customSendKeys': 800, }, }, edge: { desiredCapabilities: { browser: 'edge', browser_version: '15', + os: 'Windows', + os_version: '10', }, }, } diff --git a/nightwatch.conf.js b/nightwatch.conf.js index f843b881a..97eee0e81 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -9,13 +9,13 @@ module.exports = { globals_path: './test/e2e/globals', selenium: { start_process: true, - server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.5.3-server.jar', + server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.6.0-server.jar', log_path: './test/e2e/', host: '127.0.0.1', port: 6666, cli_args: { - 'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.32-x64-chromedriver', - 'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.18.0-x64-geckodriver', + 'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.33-x64-chromedriver', + 'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.19.0-x64-geckodriver', } }, test_settings: { @@ -43,7 +43,7 @@ module.exports = { 'chrome-headless': { desiredCapabilities: { chromeOptions : { - args: ['--headless', '--disable-gpu', 'window-size=1280,800'], + args: ['--headless', '--disable-gpu', 'window-size=1600,1200'], }, }, }, diff --git a/package.json b/package.json index 3e9d5fb05..897efc819 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "redux": "^3.6.0", "redux-thunk": "^2.1.0", "resolve": "^1.4.0", - "selenium-standalone": "6.9.0", + "selenium-standalone": "^6.11.0", "semver": "^5.4.1", "simplemde": "^1.11.2", "smoothscroll-polyfill": "^0.3.5", diff --git a/scripts/e2e-ci.sh b/scripts/e2e-ci.sh index e0e94b4bf..e72d1a6ce 100755 --- a/scripts/e2e-ci.sh +++ b/scripts/e2e-ci.sh @@ -1,32 +1,87 @@ #!/bin/bash +CIRCLE_TEST_REPORTS=${CIRCLE_TEST_REPORTS:-./test/e2e/reports} +CIRCLE_BRANCH=${CIRCLE_BRANCH:-master} + +# Amount of retries before failure. +E2E_MAX_RETRIES=${E2E_MAX_RETRIES:-1} + +# Amount of seconds between tests. +E2E_SLEEP_BETWEEN_TESTS=${E2E_SLEEP_BETWEEN_TESTS:-1} + +# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old. +BROWSERS="chrome firefox ie edge" #safari + if [[ "${CIRCLE_BRANCH}" == "master" ]]; then + # List of failed browsers. + failedBrowsers= + + # List of succeeded browsers. + succeededBrowsers= + exitCode=0 browserstack() { - REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/$1" yarn e2e-browserstack -- --env "$1" + + # Current number of tries. + try=${2:-0} + + echo "-- Start e2e for $1 #$try --" + + REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/$1" yarn e2e-browserstack --env "$1" # Determine exit code. result=$? - if [ "$result" -gt "0" ] - then + if [ "$result" -ne "0" ]; then + echo "-- Failed e2e for $1 #$try --" + + # Try again until E2E_MAX_RETRIES is reached. + if [ "$try" -lt "$E2E_MAX_RETRIES" ]; then + let try=try+1 + + # Sleep a bit to let browserstack-local close properly. + sleep "$E2E_SLEEP_BETWEEN_TESTS" + + browserstack "$1" "$try" + return + fi + + # Failed, add to list of failed browsers. + failedBrowsers="$failedBrowsers $1" + + # Remember exit code. exitCode=$result + else + echo "-- Success e2e for $1 #$try --" + + # Succeeded, add to list of succeeded browsers. + succeededBrowsers="$succeededBrowsers $1" + eval "browser_${1}_succeeded_at=$try" fi - # Sleep a bit to let browserstack-local to close properly. - sleep 2 + # Sleep a bit to let browserstack-local close properly. + sleep "$E2E_SLEEP_BETWEEN_TESTS" } # Test using browserstack. - browserstack chrome - browserstack firefox - browserstack ie - browserstack edge + for browser in $BROWSERS + do + browserstack "$browser" + done - # Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old. - # browserstack safari + # Print information about succeeded browsers. + for x in $succeededBrowsers + do + echo "Succeeded $x at try #$(eval "echo \$browser_${x}_succeeded_at")" + done + + # Print information about failed browsers. + for x in $failedBrowsers + do + echo "Failed $x" + done exit $exitCode else # When browserstack is not available test locally using chrome headless. diff --git a/services/email/banned.html.ejs b/services/email/banned.html.ejs new file mode 100644 index 000000000..4fd8f191c --- /dev/null +++ b/services/email/banned.html.ejs @@ -0,0 +1 @@ +<%= body.replace(/\n/g, '
') %> diff --git a/services/email/banned.txt.ejs b/services/email/banned.txt.ejs new file mode 100644 index 000000000..b36560ec5 --- /dev/null +++ b/services/email/banned.txt.ejs @@ -0,0 +1 @@ +<%= body %> diff --git a/services/users.js b/services/users.js index 9906ef6c0..e0f7d1599 100644 --- a/services/users.js +++ b/services/users.js @@ -398,7 +398,7 @@ module.exports = class UsersService { // TODO: current updating status behavior is weird. // once a user has been `APPROVED` its status cannot be // changed anymore. - return UserModel.findOneAndUpdate({ + const user = await UserModel.findOneAndUpdate({ id, status: { $ne: 'APPROVED' @@ -410,6 +410,25 @@ module.exports = class UsersService { }, { new: true, }); + + if (status === 'BANNED') { + let localProfile = user.profiles.find((profile) => profile.provider === 'local'); + if (localProfile) { + const options = + { + template: 'banned', // needed to know which template to render! + locals: { // specifies the template locals. + body: 'In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.' + }, + subject: 'Your account has been banned', + to: localProfile.id // This only works if the user has registered via e-mail. + // We may want a standard way to access a user's e-mail address in the future + }; + await MailerService.sendSimple(options); + } + + } + return user; } /** diff --git a/test/e2e/page_objects/embedStream.js b/test/e2e/page_objects/embedStream.js index 76065ba5e..f148ae7bc 100644 --- a/test/e2e/page_objects/embedStream.js +++ b/test/e2e/page_objects/embedStream.js @@ -8,6 +8,11 @@ module.exports = { }, getEmbedSection: function() { this.waitForElementVisible('@iframe'); + + // Pause a bit to let iframe initialize in the hope that it'll + // fix https://www.browserstack.com/automate/builds/96419cf46e3d6376a36ae6d3f90934112df1ed91/sessions/224f1a1566c1c8c7859e2e76ece51862200f0173#automate_button + this.api.pause(200); + this.api.frame(iframeId); this.expect.section('@embed').to.be.present; return this.section.embed; @@ -23,11 +28,13 @@ module.exports = { embed: { commands: [{ getProfileSection: function() { + this.waitForElementVisible('@profileTabButton'); this.click('@profileTabButton'); this.expect.section('@profile').to.be.present; return this.section.profile; }, getCommentsSection: function() { + this.waitForElementVisible('@commentsTabButton'); this.click('@commentsTabButton'); this.expect.section('@comments').to.be.present; return this.section.comments; diff --git a/test/e2e/specs/02_admin.js b/test/e2e/specs/02_admin.js index 4a60780bb..4744d2116 100644 --- a/test/e2e/specs/02_admin.js +++ b/test/e2e/specs/02_admin.js @@ -1,9 +1,9 @@ module.exports = { '@tags': ['admin', 'login'], beforeEach: (client) => { - + client.resizeWindow(1024, 800); - }, + }, 'Admin logs in': (client) => { const adminPage = client.page.admin(); const {testData: {admin}} = client.globals; @@ -17,8 +17,6 @@ module.exports = { .waitForElementVisible('@signInButton') .click('@signInButton'); - client.pause(3000); - adminPage .waitForElementVisible('@moderationContainer'); }, diff --git a/test/e2e/specs/03_embedStream.js b/test/e2e/specs/03_embedStream.js index 57f9a8fd2..53da1e87d 100644 --- a/test/e2e/specs/03_embedStream.js +++ b/test/e2e/specs/03_embedStream.js @@ -1,3 +1,5 @@ +const SortedWindowHandler = require('../utils/SortedWindowHandler'); + module.exports = { '@tags': ['embedStream', 'login'], 'creates a new asset': (client) => { @@ -18,16 +20,19 @@ module.exports = { .navigate() .getEmbedSection(); + const windowHandler = new SortedWindowHandler(client); + embed .waitForElementVisible('@signInButton') .click('@signInButton'); - client.pause(3000); + // Wait for window to be created + // https://www.browserstack.com/automate/builds/1ceccf4efb4683b7feb890f45a32b5922b40ed3f/sessions/17b1a79682bef2498cb0be86eac317a08c976b0a#automate_button + client.pause(200); // Focusing on the Login PopUp - client.windowHandles((result) => { - const handle = result.value[1]; - client.switchWindow(handle); + windowHandler.windowHandles((handles) => { + client.switchWindow(handles[1]); }); const login = client.page.login(); @@ -45,10 +50,19 @@ module.exports = { .waitForElementVisible('@loginButton') .click('@loginButton'); + // Give a tiny bit of time to let popup close. + client.pause(50); + + if (client.capabilities.browserName === 'MicrosoftEdge') { + + // More time for edge. + // https://www.browserstack.com/automate/builds/1ceccf4efb4683b7feb890f45a32b5922b40ed3f/sessions/7393dbfda8387e43b6d5851f359b0c07db414973 + client.pause(1000); + } + // Focusing on the Embed Window - client.windowHandles((result) => { - const handle = result.value[0]; - client.switchWindow(handle); + windowHandler.windowHandles((handles) => { + client.switchWindow(handles[0]); }); }, 'user posts a comment': (client) => { diff --git a/test/e2e/utils/SortedWindowHandler.js b/test/e2e/utils/SortedWindowHandler.js new file mode 100644 index 000000000..f57856814 --- /dev/null +++ b/test/e2e/utils/SortedWindowHandler.js @@ -0,0 +1,41 @@ + +/** + * SortedWindowHandler assists in making e2e tests more robust by returning + * deterministic window handles. An instance must be created before new windows + * are created and windowHandles must be called each time a window was created or + * closed. + */ +class SortedWindowHandler { + + /** + * Constructor, must be called before new windows were created. + */ + constructor(client) { + this.client = client; + this.client.windowHandles((result) => { + this.handles = result.value; + if (this.handles.length > 2) { + throw new Error('SortedWindowHandler must be created before new windows were created.'); + } + }); + } + + /** + * windowHandles will call given `callback` with an array of window handles. + */ + windowHandles(callback) { + this.client.windowHandles((result) => { + this.handles = this.handles.filter((handle) => result.value.includes(handle)); + const remaining = result.value.filter((handle) => !this.handles.includes(handle)); + if (remaining.length === 1) { + this.handles.push(remaining[0]); + } + if (remaining.length > 1) { + throw new Error('Cannot detect new window handle, because more than one windows was created.'); + } + callback(this.handles); + }); + } +} + +module.exports = SortedWindowHandler; diff --git a/test/server/services/users.js b/test/server/services/users.js index dddd5eda8..b4a15e3ba 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -1,8 +1,11 @@ const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); +const MailerService = require('../../../services/mailer'); const chai = require('chai'); chai.use(require('chai-as-promised')); +const sinon = require('sinon'); +chai.use(require('sinon-chai')); const expect = chai.expect; describe('services.UsersService', () => { @@ -15,7 +18,7 @@ describe('services.UsersService', () => { mockUsers = await UsersService.createLocalUsers([{ email: 'stampi@gmail.com', username: 'Stampi', - password: '1Coral!-' + password: '1Coral!-', }, { email: 'sockmonster@gmail.com', username: 'Sockmonster', @@ -25,6 +28,12 @@ describe('services.UsersService', () => { username: 'Marvel', password: '3Coral!3' }]); + + sinon.spy(MailerService, 'sendSimple'); + }); + + afterEach(() => { + MailerService.sendSimple.restore(); }); describe('#findById()', () => { @@ -149,7 +158,11 @@ describe('services.UsersService', () => { .then(() => UsersService.findById(mockUsers[0].id)) .then((user) => { expect(user).to.have.property('status', 'ACTIVE'); + }) + .then(() => { + expect(MailerService.sendSimple).to.not.have.been.called; }); + }); }); @@ -188,6 +201,12 @@ describe('services.UsersService', () => { .then(() => UsersService.findById(mockUsers[0].id)) .then((user) => { expect(user).to.have.property('status', 'BANNED'); + }) + .then(() => { + expect(MailerService.sendSimple).to.have.been.calledWithMatch({ + template: 'banned', + to: mockUsers[0].profiles[0].id + }); }); }); diff --git a/yarn.lock b/yarn.lock index 6ea60fc82..7996e549f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7953,9 +7953,9 @@ select@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" -selenium-standalone@6.9.0: - version "6.9.0" - resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.9.0.tgz#5805f403c82b48b4e136d8a97f1832aad7852bd5" +selenium-standalone@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.11.0.tgz#eb21ff65f3b2ece75061b35d4f9e7572ac0280c2" dependencies: async "^2.1.4" commander "^2.9.0"