Added waitUntilVisibleAfterRefresh and textsToWaitFor - both discussed with @witzig.
Page objects refactored to exploit textsToWaitFor if relevant. Login tests refactored for the newer API. Some additional tests in subscription. The rest at least included as "pending".
This commit is contained in:
parent
ccd37ac792
commit
bb2b3da9dd
14 changed files with 446 additions and 393 deletions
32
test/e2e/lib/config.js
Normal file
32
test/e2e/lib/config.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
|
||||
module.exports = {
|
||||
app: config,
|
||||
baseUrl: 'http://localhost:' + config.www.port,
|
||||
mailUrl: 'http://localhost:' + config.testserver.mailboxserverport,
|
||||
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
|
||||
}
|
||||
};
|
21
test/e2e/lib/exit-unless-test.js
Normal file
21
test/e2e/lib/exit-unless-test.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
'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);
|
||||
}
|
||||
|
||||
if (config.app.www.port !== 3000) {
|
||||
log.error('e2e', 'This script requires Mailtrain to be running on port 3000. Check config/test.toml');
|
||||
process.exit(1);
|
||||
}
|
19
test/e2e/lib/mail.js
Normal file
19
test/e2e/lib/mail.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const driver = require('./mocha-e2e').driver;
|
||||
const page = require('./page');
|
||||
|
||||
module.exports = (...extras) => page({
|
||||
|
||||
async fetchMail(address) {
|
||||
await driver.sleep(1000);
|
||||
await driver.navigate().to(`${config.mailUrl}/${address}`);
|
||||
await this.waitUntilVisible();
|
||||
},
|
||||
|
||||
async ensureUrl(path) {
|
||||
throw new Error('Unsupported method.');
|
||||
},
|
||||
|
||||
}, ...extras);
|
191
test/e2e/lib/mocha-e2e.js
Normal file
191
test/e2e/lib/mocha-e2e.js
Normal file
|
@ -0,0 +1,191 @@
|
|||
'use strict';
|
||||
|
||||
const Mocha = require('mocha');
|
||||
const color = Mocha.reporters.Base.color;
|
||||
const Semaphore = require('./semaphore');
|
||||
const fs = require('fs-extra');
|
||||
const config = require('./config');
|
||||
const webdriver = require('selenium-webdriver');
|
||||
|
||||
const driver = new webdriver.Builder()
|
||||
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
||||
.build();
|
||||
|
||||
|
||||
const failHandlerRunning = new Semaphore();
|
||||
|
||||
|
||||
function UseCaseReporter(runner) {
|
||||
Mocha.reporters.Base.call(this, runner);
|
||||
|
||||
const self = this;
|
||||
let indents = 0;
|
||||
|
||||
function indent () {
|
||||
return Array(indents).join(' ');
|
||||
}
|
||||
|
||||
runner.on('start', function () {
|
||||
console.log();
|
||||
});
|
||||
|
||||
runner.on('suite', suite => {
|
||||
++indents;
|
||||
console.log(color('suite', '%s%s'), indent(), suite.title);
|
||||
});
|
||||
|
||||
runner.on('suite end', () => {
|
||||
--indents;
|
||||
if (indents === 1) {
|
||||
console.log();
|
||||
}
|
||||
});
|
||||
|
||||
runner.on('use-case', useCase => {
|
||||
++indents;
|
||||
console.log();
|
||||
console.log(color('suite', '%sUse case: %s'), indent(), useCase.title);
|
||||
});
|
||||
|
||||
runner.on('use-case end', () => {
|
||||
--indents;
|
||||
});
|
||||
|
||||
runner.on('step pass', step => {
|
||||
console.log(indent() + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title);
|
||||
});
|
||||
|
||||
runner.on('step fail', step => {
|
||||
console.log(indent() + color('fail', ' %s'), step.title);
|
||||
});
|
||||
|
||||
runner.on('pending', test => {
|
||||
const fmt = indent() + color('pending', ' - %s');
|
||||
console.log(fmt, test.title);
|
||||
});
|
||||
|
||||
runner.on('pass', test => {
|
||||
let fmt;
|
||||
if (test.speed === 'fast') {
|
||||
fmt = indent() +
|
||||
color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) +
|
||||
color('pass', ' %s');
|
||||
console.log(fmt, test.title);
|
||||
} else {
|
||||
fmt = indent() +
|
||||
color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) +
|
||||
color('pass', ' %s') +
|
||||
color(test.speed, ' (%dms)');
|
||||
console.log(fmt, test.title, test.duration);
|
||||
}
|
||||
});
|
||||
|
||||
runner.on('fail', (test, err) => {
|
||||
failHandlerRunning.enter();
|
||||
(async () => {
|
||||
const currentUrl = await driver.getCurrentUrl();
|
||||
const info = `URL: ${currentUrl}`;
|
||||
await fs.writeFile('last-failed-e2e-test.info', info);
|
||||
await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource());
|
||||
await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64'));
|
||||
failHandlerRunning.exit();
|
||||
})();
|
||||
|
||||
console.log(indent() + color('fail', ' %s'), test.title);
|
||||
console.log();
|
||||
console.log(err);
|
||||
console.log();
|
||||
console.log(`Snaphot of and info about the current page are in last-failed-e2e-test.*`);
|
||||
});
|
||||
|
||||
runner.on('end', () => {
|
||||
const stats = self.stats;
|
||||
let fmt;
|
||||
|
||||
console.log();
|
||||
|
||||
// passes
|
||||
fmt = color('bright pass', ' ') + color('green', ' %d passing');
|
||||
console.log(fmt, stats.passes);
|
||||
|
||||
// pending
|
||||
if (stats.pending) {
|
||||
fmt = color('pending', ' ') + color('pending', ' %d pending');
|
||||
console.log(fmt, stats.pending);
|
||||
}
|
||||
|
||||
// failures
|
||||
if (stats.failures) {
|
||||
fmt = color('fail', ' %d failing');
|
||||
console.log(fmt, stats.failures);
|
||||
}
|
||||
|
||||
console.log();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const mocha = new Mocha()
|
||||
.timeout(120000)
|
||||
.reporter(UseCaseReporter)
|
||||
.ui('tdd');
|
||||
|
||||
mocha._originalRun = mocha.run;
|
||||
|
||||
|
||||
let runner;
|
||||
mocha.run = fn => {
|
||||
runner = mocha._originalRun(async () => {
|
||||
await failHandlerRunning.waitForEmpty();
|
||||
await driver.quit();
|
||||
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
async function useCaseExec(name, asyncFn) {
|
||||
runner.emit('use-case', {title: name});
|
||||
|
||||
try {
|
||||
await asyncFn();
|
||||
runner.emit('use-case end');
|
||||
} catch (err) {
|
||||
runner.emit('use-case end');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function useCase(name, asyncFn) {
|
||||
if (asyncFn) {
|
||||
return test('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||
} else {
|
||||
// Pending test
|
||||
return test('Use case: ' + name);
|
||||
}
|
||||
}
|
||||
|
||||
useCase.only = (name, asyncFn) => {
|
||||
return test.only('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||
};
|
||||
|
||||
useCase.skip = (name, asyncFn) => {
|
||||
return test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||
};
|
||||
|
||||
async function step(name, asyncFn) {
|
||||
try {
|
||||
await asyncFn();
|
||||
runner.emit('step pass', {title: name});
|
||||
} catch (err) {
|
||||
runner.emit('step fail', {title: name});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mocha,
|
||||
useCase,
|
||||
step,
|
||||
driver
|
||||
};
|
122
test/e2e/lib/page.js
Normal file
122
test/e2e/lib/page.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const webdriver = require('selenium-webdriver');
|
||||
const By = webdriver.By;
|
||||
const until = webdriver.until;
|
||||
const fs = require('fs-extra');
|
||||
const driver = require('./mocha-e2e').driver;
|
||||
const url = require('url');
|
||||
const UrlPattern = require('url-pattern');
|
||||
|
||||
const waitTimeout = 10000;
|
||||
|
||||
module.exports = (...extras) => Object.assign({
|
||||
elements: {},
|
||||
|
||||
async getElement(key) {
|
||||
return await driver.findElement(By.css(this.elements[key]));
|
||||
},
|
||||
|
||||
async getLinkParams(key) {
|
||||
const elem = await driver.findElement(By.css(this.elements[key]));
|
||||
|
||||
const linkUrl = await elem.getAttribute('href');
|
||||
const linkPath = url.parse(linkUrl).path;
|
||||
|
||||
const urlPattern = new UrlPattern(this.links[key]);
|
||||
|
||||
const params = urlPattern.match(linkPath);
|
||||
if (!params) {
|
||||
throw new Error(`Cannot match URL pattern ${this.links[key]}`);
|
||||
}
|
||||
return params;
|
||||
},
|
||||
|
||||
async waitUntilVisible(selector) {
|
||||
await driver.wait(until.elementLocated(By.css('body')), waitTimeout);
|
||||
|
||||
for (const elem of (this.elementsToWaitFor || [])) {
|
||||
const sel = this.elements[elem];
|
||||
if (!sel) {
|
||||
throw new Error(`Element "${elem}" not found.`);
|
||||
}
|
||||
await driver.wait(until.elementLocated(By.css(sel)), waitTimeout);
|
||||
}
|
||||
|
||||
for (const text of (this.textsToWaitFor || [])) {
|
||||
await driver.wait(new webdriver.Condition(`for text "${text}"`, async (driver) => {
|
||||
return await this.containsText(text);
|
||||
}), waitTimeout);
|
||||
}
|
||||
|
||||
if (this.url) {
|
||||
await this.ensureUrl();
|
||||
}
|
||||
|
||||
await driver.executeScript('document.mailTrainRefreshAcknowledged = true;');
|
||||
},
|
||||
|
||||
async waitUntilVisibleAfterRefresh(selector) {
|
||||
await driver.wait(new webdriver.Condition('for refresh', async (driver) => {
|
||||
const val = await driver.executeScript('return document.mailTrainRefreshAcknowledged;');
|
||||
return !val;
|
||||
}), waitTimeout);
|
||||
|
||||
await this.waitUntilVisible(selector);
|
||||
},
|
||||
|
||||
async click(key) {
|
||||
const elem = await this.getElement(key);
|
||||
await elem.click();
|
||||
},
|
||||
|
||||
async getHref(key) {
|
||||
const elem = await this.getElement(key);
|
||||
return await elem.getAttribute('href');
|
||||
},
|
||||
|
||||
async getText(key) {
|
||||
const elem = await this.getElement(key);
|
||||
return await elem.getText();
|
||||
},
|
||||
|
||||
async getValue(key) {
|
||||
const elem = await this.getElement(key);
|
||||
return await elem.getAttribute('value');
|
||||
},
|
||||
|
||||
async containsText(str) {
|
||||
return await driver.executeScript(`
|
||||
return (document.documentElement.innerText || document.documentElement.textContent).indexOf('${str}') > -1;
|
||||
`);
|
||||
},
|
||||
|
||||
async getSource() {
|
||||
return await driver.getPageSource();
|
||||
},
|
||||
|
||||
async saveSource(destPath) {
|
||||
const src = await this.getSource();
|
||||
await fs.writeFile(destPath, src);
|
||||
},
|
||||
|
||||
async saveScreenshot(destPath) {
|
||||
const pngData = await driver.takeScreenshot();
|
||||
const buf = new Buffer(pngData, 'base64');
|
||||
await fs.writeFile(destPath, buf);
|
||||
},
|
||||
|
||||
async saveSnapshot(destPathBase) {
|
||||
destPathBase = destPathBase || 'last-failed-e2e-test';
|
||||
const currentUrl = await driver.getCurrentUrl();
|
||||
const info = `URL: ${currentUrl}`;
|
||||
await fs.writeFile(destPathBase + '.info', info);
|
||||
await this.saveSource(destPathBase + '.html');
|
||||
await this.saveScreenshot(destPathBase + '.png');
|
||||
},
|
||||
|
||||
async sleep(ms) {
|
||||
await driver.sleep(ms);
|
||||
}
|
||||
}, ...extras);
|
35
test/e2e/lib/semaphore.js
Normal file
35
test/e2e/lib/semaphore.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const Promise = require('bluebird');
|
||||
|
||||
class Semaphore {
|
||||
constructor() {
|
||||
this.counter = 0;
|
||||
}
|
||||
|
||||
enter() {
|
||||
this.counter++;
|
||||
}
|
||||
|
||||
exit() {
|
||||
this.counter--;
|
||||
}
|
||||
|
||||
async waitForEmpty() {
|
||||
const self = this;
|
||||
|
||||
function wait(resolve) {
|
||||
if (self.counter == 0) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(wait, 500, resolve);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(wait, 500, resolve);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Semaphore;
|
77
test/e2e/lib/web.js
Normal file
77
test/e2e/lib/web.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const By = require('selenium-webdriver').By;
|
||||
const url = require('url');
|
||||
const UrlPattern = require('url-pattern');
|
||||
const driver = require('./mocha-e2e').driver;
|
||||
const page = require('./page');
|
||||
|
||||
module.exports = (...extras) => page({
|
||||
|
||||
async navigate(pathOrParams) {
|
||||
let path;
|
||||
if (typeof pathOrParams === 'string') {
|
||||
path = pathOrParams;
|
||||
} else {
|
||||
const urlPattern = new UrlPattern(this.requestUrl || this.url);
|
||||
path = urlPattern.stringify(pathOrParams)
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(path);
|
||||
let absolutePath;
|
||||
if (parsedUrl.host) {
|
||||
absolutePath = path;
|
||||
} else {
|
||||
absolutePath = config.baseUrl + path;
|
||||
}
|
||||
|
||||
await driver.navigate().to(absolutePath);
|
||||
await this.waitUntilVisible();
|
||||
},
|
||||
|
||||
async ensureUrl(path) {
|
||||
const desiredUrl = path || this.url;
|
||||
|
||||
if (desiredUrl) {
|
||||
const currentUrl = url.parse(await driver.getCurrentUrl());
|
||||
const urlPattern = new UrlPattern(desiredUrl);
|
||||
const params = urlPattern.match(currentUrl.pathname);
|
||||
if (!params || config.baseUrl !== `${currentUrl.protocol}//${currentUrl.host}`) {
|
||||
throw new Error(`Unexpected URL. Expecting ${config.baseUrl}${this.url} got ${currentUrl.protocol}//${currentUrl.host}/${currentUrl.pathname}`);
|
||||
}
|
||||
|
||||
this.params = params;
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const submitButton = await this.getElement('submitButton');
|
||||
await submitButton.click();
|
||||
},
|
||||
|
||||
async waitForFlash() {
|
||||
await this.waitUntilVisible('div.alert:not(.js-warning)');
|
||||
},
|
||||
|
||||
async getFlash() {
|
||||
const elem = await driver.findElement(By.css('div.alert:not(.js-warning)'));
|
||||
return await elem.getText();
|
||||
},
|
||||
|
||||
async clearFlash() {
|
||||
await driver.executeScript(`
|
||||
var elements = document.getElementsByClassName('alert');
|
||||
while(elements.length > 0){
|
||||
elements[0].parentNode.removeChild(elements[0]);
|
||||
}
|
||||
`);
|
||||
},
|
||||
|
||||
async setValue(key, value) {
|
||||
const elem = await this.getElement(key);
|
||||
await elem.clear();
|
||||
await elem.sendKeys(value);
|
||||
}
|
||||
|
||||
}, ...extras);
|
Loading…
Add table
Add a link
Reference in a new issue