e2e tests (draft)
This commit is contained in:
parent
408db13fd4
commit
6c35046ab2
26 changed files with 1659 additions and 29 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,8 +3,10 @@ npm-debug.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
config/development.*
|
config/development.*
|
||||||
config/production.*
|
config/production.*
|
||||||
|
config/test.*
|
||||||
workers/reports/config/development.*
|
workers/reports/config/development.*
|
||||||
workers/reports/config/production.*
|
workers/reports/config/production.*
|
||||||
|
workers/reports/config/test.*
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
|
||||||
# generate POT file every time you want to update your PO file
|
# generate POT file every time you want to update your PO file
|
||||||
|
|
|
@ -9,7 +9,7 @@ module.exports = function (grunt) {
|
||||||
},
|
},
|
||||||
|
|
||||||
nodeunit: {
|
nodeunit: {
|
||||||
all: ['test/**/*-test.js']
|
all: ['test/nodeunit/**/*-test.js']
|
||||||
},
|
},
|
||||||
|
|
||||||
jsxgettext: {
|
jsxgettext: {
|
||||||
|
|
5
app.js
5
app.js
|
@ -183,6 +183,11 @@ app.use((req, res, next) => {
|
||||||
res.locals.customStyles = config.customstyles || [];
|
res.locals.customStyles = config.customstyles || [];
|
||||||
res.locals.customScripts = config.customscripts || [];
|
res.locals.customScripts = config.customscripts || [];
|
||||||
|
|
||||||
|
let bodyClasses = [];
|
||||||
|
app.get('env') === 'test' && bodyClasses.push('page--' + (req.path.substring(1).replace(/\//g, '--') || 'home'));
|
||||||
|
req.user && bodyClasses.push('logged-in user-' + req.user.username);
|
||||||
|
res.locals.bodyClass = bodyClasses.join(' ');
|
||||||
|
|
||||||
settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => {
|
settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
|
|
|
@ -110,16 +110,6 @@ host="0.0.0.0"
|
||||||
# VERP hostname is in the same domain as the From address.
|
# VERP hostname is in the same domain as the From address.
|
||||||
# disablesenderheader=true
|
# disablesenderheader=true
|
||||||
|
|
||||||
[testserver]
|
|
||||||
# Starts a vanity server that redirects all mail to /dev/null
|
|
||||||
# Mostly needed for local development
|
|
||||||
enabled=false
|
|
||||||
port=5587
|
|
||||||
host="0.0.0.0"
|
|
||||||
username="testuser"
|
|
||||||
password="testpass"
|
|
||||||
logger=false
|
|
||||||
|
|
||||||
[ldap]
|
[ldap]
|
||||||
# enable to use ldap user backend
|
# enable to use ldap user backend
|
||||||
enabled=false
|
enabled=false
|
||||||
|
@ -177,3 +167,17 @@ templates=[["demo", "Demo Template"]]
|
||||||
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
|
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
|
||||||
# then it's safer to switch off the reporting functionality below.
|
# then it's safer to switch off the reporting functionality below.
|
||||||
enabled=false
|
enabled=false
|
||||||
|
|
||||||
|
[testserver]
|
||||||
|
# Starts a vanity server that redirects all mail to /dev/null
|
||||||
|
# Mostly needed for local development
|
||||||
|
enabled=false
|
||||||
|
port=5587
|
||||||
|
mailboxserverport=3001
|
||||||
|
host="0.0.0.0"
|
||||||
|
username="testuser"
|
||||||
|
password="testpass"
|
||||||
|
logger=false
|
||||||
|
|
||||||
|
[seleniumwebdriver]
|
||||||
|
browser="phantomjs"
|
||||||
|
|
|
@ -107,13 +107,14 @@ function getSql(path, data, callback) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
let renderer = Handlebars.compile(source);
|
const rendered = data ? Handlebars.compile(source)(data) : source;
|
||||||
return callback(null, renderer(data || {}));
|
return callback(null, rendered);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function runInitial(callback) {
|
function runInitial(callback) {
|
||||||
let fname = process.env.DB_FROM_START ? 'base.sql' : 'mailtrain.sql';
|
let dump = process.env.NODE_ENV === 'test' ? 'mailtrain-test.sql' : 'mailtrain.sql';
|
||||||
|
let fname = process.env.DB_FROM_START ? 'base.sql' : dump;
|
||||||
let path = pathlib.join(__dirname, '..', 'setup', 'sql', fname);
|
let path = pathlib.join(__dirname, '..', 'setup', 'sql', fname);
|
||||||
log.info('sql', 'Loading tables from %s', fname);
|
log.info('sql', 'Loading tables from %s', fname);
|
||||||
applyUpdate({
|
applyUpdate({
|
||||||
|
|
17
package.json
17
package.json
|
@ -8,12 +8,17 @@
|
||||||
"test": "grunt",
|
"test": "grunt",
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"sqlinit": "node setup/sql/init.js",
|
"sqlinit": "node setup/sql/init.js",
|
||||||
"sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain.sql",
|
"sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain${DUMP_NAME_SUFFIX}.sql",
|
||||||
"sqldrop": "node setup/sql/drop.js",
|
"sqldrop": "node setup/sql/drop.js",
|
||||||
"sqlgen": "npm run sqldrop && DB_FROM_START=Y npm run sqlinit && npm run sqldump",
|
"sqlgen": "npm run sqldrop && DB_FROM_START=Y npm run sqlinit && npm run sqldump",
|
||||||
"langs:hbs": "jsxgettext -L handlebars -k translate -o langs/hbs.pot views/layout.hbs views/index.hbs",
|
"langs:hbs": "jsxgettext -L handlebars -k translate -o langs/hbs.pot views/layout.hbs views/index.hbs",
|
||||||
"langs:js": "jsxgettext -o languages/js.pot routes/index.js",
|
"langs:js": "jsxgettext -o languages/js.pot routes/index.js",
|
||||||
"langs": "npm run langs:hbs && npm run langs:js"
|
"langs": "npm run langs:hbs && npm run langs:js",
|
||||||
|
"sqldumptest": "NODE_ENV=test DUMP_NAME_SUFFIX=-test npm run sqldump",
|
||||||
|
"sqlresettest": "NODE_ENV=test npm run sqldrop && NODE_ENV=test npm run sqlinit",
|
||||||
|
"starttest": "NODE_ENV=test node index.js",
|
||||||
|
"_e2e": "PATH=$PATH:./node_modules/phantomjs/lib/phantom/bin:./test/e2e/bin NODE_ENV=test ./node_modules/.bin/mocha test/e2e/index.js",
|
||||||
|
"e2e": "npm run sqlresettest && npm run _e2e"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -26,12 +31,18 @@
|
||||||
"node": ">=5.0.0"
|
"node": ">=5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"babel-eslint": "^7.2.3",
|
||||||
|
"chai": "^3.5.0",
|
||||||
"eslint-config-nodemailer": "^1.0.0",
|
"eslint-config-nodemailer": "^1.0.0",
|
||||||
"grunt": "^1.0.1",
|
"grunt": "^1.0.1",
|
||||||
"grunt-cli": "^1.2.0",
|
"grunt-cli": "^1.2.0",
|
||||||
"grunt-contrib-nodeunit": "^1.0.0",
|
"grunt-contrib-nodeunit": "^1.0.0",
|
||||||
"grunt-eslint": "^19.0.0",
|
"grunt-eslint": "^19.0.0",
|
||||||
"jsxgettext-andris": "^0.9.0-patch.1"
|
"jsxgettext-andris": "^0.9.0-patch.1",
|
||||||
|
"mailparser": "^2.0.5",
|
||||||
|
"mocha": "^3.3.0",
|
||||||
|
"phantomjs": "^2.1.7",
|
||||||
|
"selenium-webdriver": "^3.4.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"posix": "^4.1.1"
|
"posix": "^4.1.1"
|
||||||
|
|
|
@ -4,12 +4,37 @@ let log = require('npmlog');
|
||||||
let config = require('config');
|
let config = require('config');
|
||||||
let crypto = require('crypto');
|
let crypto = require('crypto');
|
||||||
let humanize = require('humanize');
|
let humanize = require('humanize');
|
||||||
|
let http = require('http');
|
||||||
|
|
||||||
let SMTPServer = require('smtp-server').SMTPServer;
|
let SMTPServer = require('smtp-server').SMTPServer;
|
||||||
|
let simpleParser = require('mailparser').simpleParser;
|
||||||
|
|
||||||
let totalMessages = 0;
|
let totalMessages = 0;
|
||||||
let received = 0;
|
let received = 0;
|
||||||
|
|
||||||
|
let mailstore = {
|
||||||
|
accounts: {},
|
||||||
|
saveMessage(address, message) {
|
||||||
|
if (!this.accounts[address]) {
|
||||||
|
this.accounts[address] = [];
|
||||||
|
}
|
||||||
|
this.accounts[address].push(message);
|
||||||
|
},
|
||||||
|
getMail(address, callback) {
|
||||||
|
if (!this.accounts[address] || this.accounts[address].length === 0) {
|
||||||
|
let err = new Error('No mail for ' + address);
|
||||||
|
err.status = 404;
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
simpleParser(this.accounts[address].shift(), (err, mail) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err.message || err);
|
||||||
|
}
|
||||||
|
callback(null, mail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Setup server
|
// Setup server
|
||||||
let server = new SMTPServer({
|
let server = new SMTPServer({
|
||||||
|
|
||||||
|
@ -74,8 +99,12 @@ let server = new SMTPServer({
|
||||||
// Handle message stream
|
// Handle message stream
|
||||||
onData: (stream, session, callback) => {
|
onData: (stream, session, callback) => {
|
||||||
let hash = crypto.createHash('md5');
|
let hash = crypto.createHash('md5');
|
||||||
|
let message = '';
|
||||||
stream.on('data', chunk => {
|
stream.on('data', chunk => {
|
||||||
hash.update(chunk);
|
hash.update(chunk);
|
||||||
|
if (/^keep/i.test(session.envelope.rcptTo[0].address)) {
|
||||||
|
message += chunk;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
stream.on('end', () => {
|
stream.on('end', () => {
|
||||||
let err;
|
let err;
|
||||||
|
@ -84,6 +113,12 @@ let server = new SMTPServer({
|
||||||
err.responseCode = 552;
|
err.responseCode = 552;
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store message for e2e tests
|
||||||
|
if (/^keep/i.test(session.envelope.rcptTo[0].address)) {
|
||||||
|
mailstore.saveMessage(session.envelope.rcptTo[0].address, message);
|
||||||
|
}
|
||||||
|
|
||||||
received++;
|
received++;
|
||||||
callback(null, 'Message queued as ' + hash.digest('hex')); // accept the message once the stream is ended
|
callback(null, 'Message queued as ' + hash.digest('hex')); // accept the message once the stream is ended
|
||||||
});
|
});
|
||||||
|
@ -94,6 +129,41 @@ server.on('error', err => {
|
||||||
log.error('Test SMTP', err.stack);
|
log.error('Test SMTP', err.stack);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mailBoxServer = http.createServer((req, res) => {
|
||||||
|
let renderer = data => (
|
||||||
|
'<!doctype html><html><head><title>' + data.title + '</title></head><body>' + data.body + '</body></html>'
|
||||||
|
);
|
||||||
|
|
||||||
|
let address = req.url.substring(1);
|
||||||
|
mailstore.getMail(address, (err, mail) => {
|
||||||
|
if (err) {
|
||||||
|
let html = renderer({
|
||||||
|
title: 'error',
|
||||||
|
body: err.message || err
|
||||||
|
});
|
||||||
|
res.writeHead(err.status || 500, { 'Content-Type': 'text/html' });
|
||||||
|
return res.end(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = mail.html || renderer({
|
||||||
|
title: 'error',
|
||||||
|
body: 'This mail has no HTML part'
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://nodemailer.com/extras/mailparser/#mail-object
|
||||||
|
delete mail.html;
|
||||||
|
delete mail.textAsHtml;
|
||||||
|
delete mail.attachments;
|
||||||
|
|
||||||
|
let script = '<script> var mailObject = ' + JSON.stringify(mail) + '; console.log(mailObject); </script>';
|
||||||
|
html = html.replace(/<\/body\b/i, match => script + match);
|
||||||
|
html = html.replace(/target="_blank"/g, 'target="_self"');
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = callback => {
|
module.exports = callback => {
|
||||||
if (config.testserver.enabled) {
|
if (config.testserver.enabled) {
|
||||||
server.listen(config.testserver.port, config.testserver.host, () => {
|
server.listen(config.testserver.port, config.testserver.host, () => {
|
||||||
|
@ -112,7 +182,10 @@ module.exports = callback => {
|
||||||
}
|
}
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
setImmediate(callback);
|
mailBoxServer.listen(config.testserver.mailboxserverport, config.testserver.host, () => {
|
||||||
|
log.info('Test SMTP', 'Mail Box Server listening on port %s', config.testserver.mailboxserverport);
|
||||||
|
setImmediate(callback);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
console.log('This script does not run in production'); // eslint-disable-line no-console
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = require('config');
|
let config = require('config');
|
||||||
let spawn = require('child_process').spawn;
|
let spawn = require('child_process').spawn;
|
||||||
let log = require('npmlog');
|
let log = require('npmlog');
|
||||||
let path = require('path');
|
let path = require('path');
|
||||||
|
let fs = require('fs');
|
||||||
|
|
||||||
log.level = 'verbose';
|
log.level = 'verbose';
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
log.error('sqldrop', 'This script does not run in production');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) {
|
||||||
|
log.error('sqldrop', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
function createDump(callback) {
|
function createDump(callback) {
|
||||||
let cmd = spawn(path.join(__dirname, 'drop.sh'), [], {
|
let cmd = spawn(path.join(__dirname, 'drop.sh'), [], {
|
||||||
env: {
|
env: {
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
console.log('This script does not run in production'); // eslint-disable-line no-console
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dbcheck = require('../../lib/dbcheck');
|
let dbcheck = require('../../lib/dbcheck');
|
||||||
let log = require('npmlog');
|
let log = require('npmlog');
|
||||||
|
let path = require('path');
|
||||||
|
let fs = require('fs');
|
||||||
|
|
||||||
log.level = 'verbose';
|
log.level = 'verbose';
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
log.error('sqlinit', 'This script does not run in production');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) {
|
||||||
|
log.error('sqlinit', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
dbcheck(err => {
|
dbcheck(err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('DB', err);
|
log.error('DB', err);
|
||||||
|
|
1023
setup/sql/mailtrain-test.sql
Normal file
1023
setup/sql/mailtrain-test.sql
Normal file
File diff suppressed because it is too large
Load diff
11
test/e2e/.eslintrc
Normal file
11
test/e2e/.eslintrc
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"rules": {
|
||||||
|
"strict": 0,
|
||||||
|
"no-invalid-this": 0,
|
||||||
|
"no-unused-expressions": 0
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
}
|
||||||
|
}
|
6
test/e2e/README.md
Normal file
6
test/e2e/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Running e2e tests requires Node 7.6 or later and a dedicated test database.
|
||||||
|
|
||||||
|
1. Start Mailtrain with `npm run startest`
|
||||||
|
2. Start e2e tests with `npm run e2e`
|
||||||
|
|
||||||
|
By default the tests run with `phantomjs`. To use different browsers see `test/e2e/bin/README.md`.
|
8
test/e2e/bin/README.md
Normal file
8
test/e2e/bin/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
This directory serves for custom browser drivers.
|
||||||
|
|
||||||
|
1. https://seleniumhq.github.io/selenium/docs/api/javascript/
|
||||||
|
2. Download a driver of your choice and put it into this directory
|
||||||
|
3. chmod +x driver
|
||||||
|
4. Edit config/test.toml
|
||||||
|
|
||||||
|
Current Firefox issue (and patch): https://github.com/mozilla/geckodriver/issues/683
|
31
test/e2e/helpers/config.js
Normal file
31
test/e2e/helpers/config.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
app: config,
|
||||||
|
baseUrl: 'http://localhost:' + config.www.port,
|
||||||
|
users: {
|
||||||
|
admin: {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'test'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lists: {
|
||||||
|
one: {
|
||||||
|
id: 1,
|
||||||
|
cid: 'Hkj1vCoJb',
|
||||||
|
publicSubscribe: 1,
|
||||||
|
unsubscriptionMode: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'service-url' : 'http://localhost:' + config.www.port + '/',
|
||||||
|
'default-homepage': 'https://mailtrain.org',
|
||||||
|
'smtp-hostname': config.testserver.host,
|
||||||
|
'smtp-port': config.testserver.port,
|
||||||
|
'smtp-encryption': 'NONE',
|
||||||
|
'smtp-user': config.testserver.username,
|
||||||
|
'smtp-pass': config.testserver.password
|
||||||
|
}
|
||||||
|
};
|
15
test/e2e/helpers/driver.js
Normal file
15
test/e2e/helpers/driver.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('./config');
|
||||||
|
const webdriver = require('selenium-webdriver');
|
||||||
|
|
||||||
|
const driver = new webdriver.Builder()
|
||||||
|
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (global.USE_SHARED_DRIVER === true) {
|
||||||
|
driver.originalQuit = driver.quit;
|
||||||
|
driver.quit = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = driver;
|
16
test/e2e/helpers/exit-unless-test.js
Normal file
16
test/e2e/helpers/exit-unless-test.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('./config');
|
||||||
|
const log = require('npmlog');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test' || !fs.existsSync(path.join(__dirname, '..', '..', '..', 'config', 'test.toml'))) {
|
||||||
|
log.error('e2e', 'This script only runs in test and config/test.toml (i.e. a dedicated test database) is present');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.app.testserver.enabled !== true) {
|
||||||
|
log.error('e2e', 'This script only runs if the testserver is enabled. Check config/test.toml');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
36
test/e2e/index.js
Normal file
36
test/e2e/index.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('./helpers/exit-unless-test');
|
||||||
|
|
||||||
|
global.USE_SHARED_DRIVER = true;
|
||||||
|
|
||||||
|
const driver = require('./helpers/driver');
|
||||||
|
const only = 'only';
|
||||||
|
const skip = 'skip';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let tests = [
|
||||||
|
['tests/login'],
|
||||||
|
['tests/subscription']
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tests = tests.filter(t => t[1] !== skip);
|
||||||
|
|
||||||
|
if (tests.some(t => t[1] === only)) {
|
||||||
|
tests = tests.filter(t => t[1] === only);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('e2e', function() {
|
||||||
|
this.timeout(10000);
|
||||||
|
|
||||||
|
tests.forEach(t => {
|
||||||
|
describe(t[0], () => {
|
||||||
|
require('./' + t[0]); // eslint-disable-line global-require
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => driver.originalQuit());
|
||||||
|
});
|
25
test/e2e/page-objects/flash.js
Normal file
25
test/e2e/page-objects/flash.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Page = require('./page');
|
||||||
|
let flash;
|
||||||
|
|
||||||
|
class Flash extends Page {
|
||||||
|
getText() {
|
||||||
|
return this.element('alert').getText();
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
return this.driver.executeScript(`
|
||||||
|
var elements = document.getElementsByClassName('alert');
|
||||||
|
while(elements.length > 0){
|
||||||
|
elements[0].parentNode.removeChild(elements[0]);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = driver => flash || new Flash(driver, {
|
||||||
|
elementToWaitFor: 'alert',
|
||||||
|
elements: {
|
||||||
|
alert: 'div.alert:not(.js-warning)'
|
||||||
|
}
|
||||||
|
});
|
12
test/e2e/page-objects/home.js
Normal file
12
test/e2e/page-objects/home.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Page = require('./page');
|
||||||
|
let home;
|
||||||
|
|
||||||
|
module.exports = driver => home || new Page(driver, {
|
||||||
|
url: '/',
|
||||||
|
elementToWaitFor: 'body',
|
||||||
|
elements: {
|
||||||
|
body: 'body.page--home'
|
||||||
|
}
|
||||||
|
});
|
61
test/e2e/page-objects/page.js
Normal file
61
test/e2e/page-objects/page.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('../helpers/config');
|
||||||
|
const webdriver = require('selenium-webdriver');
|
||||||
|
const By = webdriver.By;
|
||||||
|
const until = webdriver.until;
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
constructor(driver, props) {
|
||||||
|
this.driver = driver;
|
||||||
|
this.props = props || {
|
||||||
|
elements: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
element(key) {
|
||||||
|
return this.driver.findElement(By.css(this.props.elements[key] || key));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate() {
|
||||||
|
this.driver.navigate().to(config.baseUrl + this.props.url);
|
||||||
|
return this.waitUntilVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
waitUntilVisible() {
|
||||||
|
let selector = this.props.elements[this.props.elementToWaitFor];
|
||||||
|
if (!selector && this.props.url) {
|
||||||
|
selector = 'body.page--' + (this.props.url.substring(1).replace(/\//g, '--') || 'home');
|
||||||
|
}
|
||||||
|
return selector ? this.driver.wait(until.elementLocated(By.css(selector))) : this.driver.sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
return this.element('submitButton').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
click(key) {
|
||||||
|
return this.element(key).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
getText(key) {
|
||||||
|
return this.element(key).getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(key) {
|
||||||
|
return this.element(key).getAttribute('value');
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(key, value) {
|
||||||
|
return this.element(key).sendKeys(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
containsText(str) {
|
||||||
|
// let text = await driver.findElement({ css: 'body' }).getText();
|
||||||
|
return this.driver.executeScript(`
|
||||||
|
return (document.documentElement.textContent || document.documentElement.innerText).indexOf('${str}') > -1;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Page;
|
84
test/e2e/page-objects/subscription.js
Normal file
84
test/e2e/page-objects/subscription.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('../helpers/config');
|
||||||
|
const Page = require('./page');
|
||||||
|
|
||||||
|
class Web extends Page {
|
||||||
|
enterEmail(value) {
|
||||||
|
this.element('emailInput').clear();
|
||||||
|
return this.element('emailInput').sendKeys(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Mail extends Page {
|
||||||
|
navigate(address) {
|
||||||
|
this.driver.sleep(100);
|
||||||
|
this.driver.navigate().to(`http://localhost:${config.app.testserver.mailboxserverport}/${address}`);
|
||||||
|
return this.waitUntilVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (driver, list) => ({
|
||||||
|
|
||||||
|
webSubscribe: new Web(driver, {
|
||||||
|
url: `/subscription/${list.cid}`,
|
||||||
|
elementToWaitFor: 'form',
|
||||||
|
elements: {
|
||||||
|
form: `form[action="/subscription/${list.cid}/subscribe"]`,
|
||||||
|
emailInput: '#main-form input[name="email"]',
|
||||||
|
submitButton: 'a[href="#submit"]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
webConfirmSubscriptionNotice: new Web(driver, {
|
||||||
|
url: `/subscription/${list.cid}/confirm-notice`,
|
||||||
|
elementToWaitFor: 'homepageButton',
|
||||||
|
elements: {
|
||||||
|
homepageButton: `a[href="${config.settings['default-homepage']}"]`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailConfirmSubscription: new Mail(driver, {
|
||||||
|
elementToWaitFor: 'confirmLink',
|
||||||
|
elements: {
|
||||||
|
confirmLink: `a[href^="${config.settings['service-url']}subscription/subscribe/"]`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
webSubscribedNotice: new Web(driver, {
|
||||||
|
elementToWaitFor: 'homepageButton',
|
||||||
|
elements: {
|
||||||
|
homepageButton: 'a[href^="https://mailtrain.org"]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailSubscriptionConfirmed: new Mail(driver, {
|
||||||
|
elementToWaitFor: 'unsubscribeLink',
|
||||||
|
elements: {
|
||||||
|
unsubscribeLink: 'a[href*="/unsubscribe/"]',
|
||||||
|
manageLink: 'a[href*="/manage/"]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
webUnsubscribe: new Web(driver, {
|
||||||
|
elementToWaitFor: 'submitButton',
|
||||||
|
elements: {
|
||||||
|
submitButton: 'a[href="#submit"]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
webUnsubscribedNotice: new Web(driver, {
|
||||||
|
elementToWaitFor: 'homepageButton',
|
||||||
|
elements: {
|
||||||
|
homepageButton: 'a[href^="https://mailtrain.org"]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
mailUnsubscriptionConfirmed: new Mail(driver, {
|
||||||
|
elementToWaitFor: 'resubscribeLink',
|
||||||
|
elements: {
|
||||||
|
resubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}"]`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
35
test/e2e/page-objects/users.js
Normal file
35
test/e2e/page-objects/users.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Page = require('./page');
|
||||||
|
|
||||||
|
class Login extends Page {
|
||||||
|
enterUsername(value) {
|
||||||
|
// this.element('usernameInput').clear();
|
||||||
|
return this.element('usernameInput').sendKeys(value);
|
||||||
|
}
|
||||||
|
enterPassword(value) {
|
||||||
|
return this.element('passwordInput').sendKeys(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = driver => ({
|
||||||
|
|
||||||
|
login: new Login(driver, {
|
||||||
|
url: '/users/login',
|
||||||
|
elementToWaitFor: 'submitButton',
|
||||||
|
elements: {
|
||||||
|
usernameInput: 'form[action="/users/login"] input[name="username"]',
|
||||||
|
passwordInput: 'form[action="/users/login"] input[name="password"]',
|
||||||
|
submitButton: 'form[action="/users/login"] [type=submit]'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
account: new Page(driver, {
|
||||||
|
url: '/users/account',
|
||||||
|
elementToWaitFor: 'emailInput',
|
||||||
|
elements: {
|
||||||
|
emailInput: 'form[action="/users/account"] input[name="email"]'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
57
test/e2e/tests/login.js
Normal file
57
test/e2e/tests/login.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('../helpers/config');
|
||||||
|
const expect = require('chai').expect;
|
||||||
|
const driver = require('../helpers/driver');
|
||||||
|
const home = require('../page-objects/home')(driver);
|
||||||
|
const flash = require('../page-objects/flash')(driver);
|
||||||
|
const {
|
||||||
|
login,
|
||||||
|
account
|
||||||
|
} = require('../page-objects/users')(driver);
|
||||||
|
|
||||||
|
describe('login', function() {
|
||||||
|
this.timeout(10000);
|
||||||
|
|
||||||
|
before(() => driver.manage().deleteAllCookies());
|
||||||
|
|
||||||
|
it('can access home page', async () => {
|
||||||
|
await home.navigate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can not access restricted content', async () => {
|
||||||
|
driver.navigate().to(config.baseUrl + '/settings');
|
||||||
|
flash.waitUntilVisible();
|
||||||
|
expect(await flash.getText()).to.contain('Need to be logged in to access restricted content');
|
||||||
|
await flash.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can not login with false credentials', async () => {
|
||||||
|
login.enterUsername(config.users.admin.username);
|
||||||
|
login.enterPassword('invalid');
|
||||||
|
login.submit();
|
||||||
|
flash.waitUntilVisible();
|
||||||
|
expect(await flash.getText()).to.contain('Incorrect username or password');
|
||||||
|
await flash.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can login as admin', async () => {
|
||||||
|
login.enterUsername(config.users.admin.username);
|
||||||
|
login.enterPassword(config.users.admin.password);
|
||||||
|
login.submit();
|
||||||
|
flash.waitUntilVisible();
|
||||||
|
expect(await flash.getText()).to.contain('Logged in as admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can access account page as admin', async () => {
|
||||||
|
await account.navigate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can logout', async () => {
|
||||||
|
driver.navigate().to(config.baseUrl + '/users/logout');
|
||||||
|
flash.waitUntilVisible();
|
||||||
|
expect(await flash.getText()).to.contain('logged out');
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => driver.quit());
|
||||||
|
});
|
101
test/e2e/tests/subscription.js
Normal file
101
test/e2e/tests/subscription.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('../helpers/config');
|
||||||
|
const shortid = require('shortid');
|
||||||
|
const expect = require('chai').expect;
|
||||||
|
const driver = require('../helpers/driver');
|
||||||
|
const Page = require('../page-objects/page');
|
||||||
|
|
||||||
|
const page = new Page(driver);
|
||||||
|
const flash = require('../page-objects/flash')(driver);
|
||||||
|
const {
|
||||||
|
webSubscribe,
|
||||||
|
webConfirmSubscriptionNotice,
|
||||||
|
mailConfirmSubscription,
|
||||||
|
webSubscribedNotice,
|
||||||
|
mailSubscriptionConfirmed,
|
||||||
|
webUnsubscribe,
|
||||||
|
webUnsubscribedNotice,
|
||||||
|
mailUnsubscriptionConfirmed
|
||||||
|
} = require('../page-objects/subscription')(driver, config.lists.one);
|
||||||
|
|
||||||
|
const testuser = {
|
||||||
|
email: 'keep.' + shortid.generate() + '@mailtrain.org'
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log(testuser.email);
|
||||||
|
|
||||||
|
describe('subscribe (list one)', function() {
|
||||||
|
this.timeout(10000);
|
||||||
|
|
||||||
|
before(() => driver.manage().deleteAllCookies());
|
||||||
|
|
||||||
|
it('visits web-subscribe', async () => {
|
||||||
|
await webSubscribe.navigate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits invalid email (error)', async () => {
|
||||||
|
webSubscribe.enterEmail('foo@bar.nope');
|
||||||
|
webSubscribe.submit();
|
||||||
|
flash.waitUntilVisible();
|
||||||
|
expect(await flash.getText()).to.contain('Invalid email address');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits valid email', async () => {
|
||||||
|
webSubscribe.enterEmail(testuser.email);
|
||||||
|
await webSubscribe.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sees web-confirm-subscription-notice', async () => {
|
||||||
|
webConfirmSubscriptionNotice.waitUntilVisible();
|
||||||
|
expect(await page.containsText('Almost Finished')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives mail-confirm-subscription', async () => {
|
||||||
|
mailConfirmSubscription.navigate(testuser.email);
|
||||||
|
expect(await page.containsText('Please Confirm Subscription')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicks confirm subscription', async () => {
|
||||||
|
await mailConfirmSubscription.click('confirmLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sees web-subscribed-notice', async () => {
|
||||||
|
webSubscribedNotice.waitUntilVisible();
|
||||||
|
expect(await page.containsText('Subscription Confirmed')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives mail-subscription-confirmed', async () => {
|
||||||
|
mailSubscriptionConfirmed.navigate(testuser.email);
|
||||||
|
expect(await page.containsText('Subscription Confirmed')).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsubscribe (list one)', function() {
|
||||||
|
this.timeout(10000);
|
||||||
|
|
||||||
|
it('clicks unsubscribe', async () => {
|
||||||
|
await mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sees web-unsubscribe', async () => {
|
||||||
|
webUnsubscribe.waitUntilVisible();
|
||||||
|
expect(await page.containsText('Unsubscribe')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicks confirm unsubscription', async () => {
|
||||||
|
await webUnsubscribe.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sees web-unsubscribed-notice', async () => {
|
||||||
|
webUnsubscribedNotice.waitUntilVisible();
|
||||||
|
expect(await page.containsText('Unsubscribe Successful')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('receives mail-unsubscription-confirmed', async () => {
|
||||||
|
mailUnsubscriptionConfirmed.navigate(testuser.email);
|
||||||
|
expect(await page.containsText('You Are Now Unsubscribed')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => driver.quit());
|
||||||
|
});
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="{{#if user}}logged-in user-{{user.username}}{{/if}}">
|
<body class="{{bodyClass}}">
|
||||||
|
|
||||||
<nav class="navbar navbar-default navbar-static-top">
|
<nav class="navbar navbar-default navbar-static-top">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue