protractor-screenshoter-plugin
Version:
A jasmine2 protractor plugin that captures for each browser instance a screenshot and browsers' console logs. The snapshot is made optionally for each expect or spec. Plugins comes with a beautiful angular based analytics tool to visually check and fix te
724 lines (647 loc) • 23.9 kB
JavaScript
var fse = require('fs-extra');
var klawSync = require('klaw-sync');
var mkdirp = require('mkdirp');
var _ = require('lodash');
var uuid = require('uuid');
var moment = require('moment');
var path = require('path');
var CircularJSON = require('circular-json');
var q = require('q');
var assert = require('assert');
try { // optional dependency, ignore if not installed
var imageToAscii = require("image-to-ascii");
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
/**
* This plugin does few things:
* 1. Takes a screenshot for each jasmine expect/matcher failure
* 2. Takes a screenshot for each test/spec failure
* 3. Generates a HTML report
* 4. Marks the test as failure if browser console log has error - Chrome only
*
* exports.config = {
* plugins: [{
* package: 'protractor-screenshoter-plugin',
* screenshotOnExpect: {String} (Default - 'failure+success', 'failure', 'none'),
* screenshotOnSpec: {String} (Default - 'failure+success', 'failure', 'none'),
* withLogs: {Boolean} (Default - true),
* htmlReport: {Boolean} (Default - true),
* writeReportFreq: {String} (Default - 'end', 'spec', 'asap'),
* screenshotPath: {String} (Default - 'reports/screenshots')
* clearFoldersBeforeTest: {Boolean} (Default - false),
* failTestOnErrorLog: {
* failTestOnErrorLogLevel: {Number}, (Default - 900)
* excludeKeywords: {A JSON Array}
* suites: {A JSON Array}
* }
* }]
* };
* @author Andrej Zachar, Abhishek Swain
* @created December 01 2015, forked on October 2016
*/
var protractorUtil = function() {};
protractorUtil.logDebug = function() {};
protractorUtil.logInfo = console.info;
protractorUtil.logError = console.error;
protractorUtil.takeDump = function(context, done) {
if (context.config.dump) {
try {
var result = context.config.dump(function(err, result1) {
done(result1);
});
if (result) {
return done(result);
}
} catch (err) {
protractorUtil.logError('Unable to execute spec\'s dump', err);
done();
}
}
}
protractorUtil.forEachBrowser = function(action) {
function catchError(err) {
console.warn('Unknown error:');
console.warn(err);
}
try {
if (global.screenshotBrowsers && Object.keys(global.screenshotBrowsers).length > 0) {
_.forOwn(global.screenshotBrowsers, function(instance, name) {
instance.getCapabilities().then(function(capabilities) {
action(instance, name + ' [' + capabilities.get('browserName') + ']', protractorUtil.newLongRunningOperationCounter());
}).catch(catchError);
});
} else {
global.browser.getCapabilities().then(function(capabilities) {
action(global.browser, capabilities.get('browserName'), protractorUtil.newLongRunningOperationCounter());
}).catch(catchError);
}
} catch (err) {
catchError(err);
}
};
protractorUtil.takeScreenshot = function(context, report) {
function takeInstanceScreenshot(browserInstance, browserName, cb) {
var screenshotFile = 'screenshots/' + uuid.v1() + '.png';
// protractorUtil.logDebug('Taking screenshot ' + screenshotFile + ' from browser instance ' + browserName);
var finalFile = context.config.screenshotPath + '/' + screenshotFile;
browserInstance.takeScreenshot().then(function(png) {
var stream = fse.createWriteStream(finalFile);
stream.write(new Buffer(png, 'base64'));
stream.on('finish', function() {
report(screenshotFile, browserName, finalFile, browserInstance, cb);
});
stream.on('error', function(e) {
cb(e);
});
stream.end();
}, function(err) {
console.warn('Error in browser instance ' + browserName + ' while taking the screenshot: ' + finalFile + ' - ' + err.message);
cb(err);
});
}
protractorUtil.forEachBrowser(takeInstanceScreenshot);
};
protractorUtil.takeLogs = function(context, report) {
function takeLog(browserInstance, browserName, cb) {
// protractorUtil.logDebug('Taking logs from browser instance ' + browserName);
browserInstance.manage().logs().get('browser').then(function(browserLogs) {
if (browserLogs && browserLogs.length > 0) {
report(browserLogs, browserName, cb);
} else {
cb();
}
}, function(err) {
console.warn('Error in browser instance ' + browserName + ' while taking the logs:' + err.message);
cb(err);
});
}
protractorUtil.forEachBrowser(takeLog);
};
protractorUtil.takeRawHtml = function(context, report) {
function takeInstanceRawHtml(browserInstance, browserName, cb) {
var snapshotFile = 'htmls/' + uuid.v1() + '.html';
// protractorUtil.logDebug('Taking raw HTML ' + snapshotFile + ' from browser instance ' + browserName);
var finalFile = context.config.screenshotPath + '/' + snapshotFile;
browserInstance.getPageSource().then(function(html) {
fse.writeFile(finalFile, html, 'utf8', function(err) {
if (err) {
return cb(err);
}
report(snapshotFile, browserName, finalFile, browserInstance, cb);
});
},
function(err) {
console.warn('Error in browser instance ' + browserName + ' while taking the raw html: ' + finalFile + ' - ' + err.message);
cb(err);
});
}
protractorUtil.forEachBrowser(takeInstanceRawHtml);
};
protractorUtil.takeScreenshotOnExpectDone = function(context) {
//Takes screen shot for expect failures
var originalAddExpectationResult = jasmine.Spec.prototype.addExpectationResult;
jasmine.Spec.prototype.addExpectationResult = function(passed, expectation) {
var self = this;
var now = moment();
expectation.screenshots = [];
expectation.logs = [];
expectation.htmls = [];
expectation.when = now.toDate();
if (!passed && context.config.pauseOn === 'failure') {
protractorUtil.logInfo('\n\nPause browser because of a failure: %s', expectation.message);
protractorUtil.logInfo('\nAt spec: %s\n\n', self.result.description)
protractorUtil.logDebug(expectation.stack);
global.browser.pause();
}
var makeScreenshotsFromEachBrowsers = false;
var makeAsciiLog = false;
var makeHtmlSnapshot = false;
var makeDump = false;
if (protractorUtil.test) {
if (passed) {
protractorUtil.test.passedExpectations.push(expectation);
makeScreenshotsFromEachBrowsers = context.config.screenshotOnExpect === 'failure+success';
makeAsciiLog = context.config.imageToAscii === 'failure+success';
makeHtmlSnapshot = context.config.htmlOnExpect === 'failure+success';
makeDump = context.config.dumpOnExpect === 'failure+success';
} else {
protractorUtil.test.failedExpectations.push(expectation);
makeScreenshotsFromEachBrowsers = context.config.screenshotOnExpect === 'failure+success' || context.config.screenshotOnExpect === 'failure';
makeAsciiLog = context.config.imageToAscii === 'failure+success' || context.config.imageToAscii === 'failure';
makeHtmlSnapshot = context.config.htmlOnExpect === 'failure+success' || context.config.htmlOnExpect === 'failure';
makeDump = context.config.dumpOnExpect === 'failure+success' || context.config.dumpOnExpect === 'failure';
}
} else {
console.warn('Calling addExpectationResult before specStarted!');
}
if (makeDump) {
protractorUtil.takeDump(context, function(dump) {
expectation.dump = dump;
if (context.config.writeReportFreq === 'asap') {
protractorUtil.writeReport(context, protractorUtil.newLongRunningOperationCounter());
}
});
}
if (makeScreenshotsFromEachBrowsers) {
protractorUtil.takeScreenshot(context, function(filename, browserName, finalFile, browserInstance, done) {
expectation.screenshots.push({
img: filename,
browser: browserName,
when: new Date()
});
if (makeAsciiLog && !browserInstance.skipImageToAscii) {
try {
imageToAscii(finalFile, context.config.imageToAsciiOpts, function(err, converted) {
var asciImage;
asciImage += '\n\n============================\n';
asciImage += browserName + ' ' + now.format() + ' ' + expectation.message;
asciImage += '\n============================\n';
asciImage += err || converted;
protractorUtil.logDebug(asciImage);
});
} catch (err) {
console.warn(err);
console.warn('Please check the installation at https://github.com/IonicaBizau/image-to-ascii/blob/master/INSTALLATION.md');
}
}
if (context.config.writeReportFreq === 'asap') {
protractorUtil.writeReport(context, done);
} else {
done();
}
});
}
if (makeHtmlSnapshot) {
protractorUtil.takeRawHtml(context, function(filename, browserName, finalFile, browserInstance, done) {
expectation.htmls.push({
file: filename,
browser: browserName,
when: new Date()
});
if (context.config.writeReportFreq === 'asap') {
protractorUtil.writeReport(context, done);
} else {
done();
}
});
}
if (context.config.withLogs) {
protractorUtil.takeLogs(context, function(logs, browserName, done) {
expectation.logs.push({
logs: logs,
browser: browserName
});
if (context.config.writeReportFreq === 'asap') {
protractorUtil.writeReport(context, done);
} else {
done();
}
});
}
return originalAddExpectationResult.apply(this, arguments);
};
};
protractorUtil.takeOnSpecDone = function(result, context, test) {
var makeScreenshotsFromEachBrowsers = false;
var makeHtmlSnapshot = false;
var makeDump = false;
if (result.failedExpectations.length === 0) {
makeScreenshotsFromEachBrowsers = context.config.screenshotOnSpec === 'failure+success';
makeHtmlSnapshot = context.config.htmlOnSpec === 'failure+success';
makeDump = context.config.dumpOnSpec === 'failure+success';
} else {
makeScreenshotsFromEachBrowsers = context.config.screenshotOnSpec === 'failure+success' || context.config.screenshotOnSpec === 'failure';
makeHtmlSnapshot = context.config.htmlOnSpec === 'failure+success' || context.config.htmlOnSpec === 'failure';
makeDump = context.config.dumpOnSpec === 'failure+success' || context.config.dumpOnSpec === 'failure';
}
if (result.status === 'disabled' || result.status === 'pending') {
makeScreenshotsFromEachBrowsers = false;
makeHtmlSnapshot = false;
makeDump = false;
}
if (makeDump) {
protractorUtil.takeDump(context, function(dump) {
test.specDump = dump;
if (context.config.writeReportFreq === 'asap' || context.config.writeReportFreq === 'spec') {
protractorUtil.writeReport(context, protractorUtil.newLongRunningOperationCounter());
}
});
}
if (makeScreenshotsFromEachBrowsers) {
protractorUtil.takeScreenshot(context, function(file, browserName, finalFile, browserInstance, done) {
test.specScreenshots.push({
img: file,
browser: browserName,
when: new Date()
});
if (context.config.writeReportFreq === 'asap' || context.config.writeReportFreq === 'spec') {
protractorUtil.writeReport(context, done);
} else {
done();
}
});
}
if (makeHtmlSnapshot) {
protractorUtil.takeRawHtml(context, function(file, browserName, finalFile, browserInstance, done) {
test.specHtmls.push({
file: file,
browser: browserName,
when: new Date()
});
if (context.config.writeReportFreq === 'asap' || context.config.writeReportFreq === 'spec') {
protractorUtil.writeReport(context, done);
} else {
done();
}
});
}
if (context.config.screenshotOnSpec != 'none' && context.config.withLogs) {
protractorUtil.takeLogs(context, function(logs, browserName, done) {
test.specLogs.push({
logs: logs,
browser: browserName
});
if (context.config.writeReportFreq === 'asap' || context.config.writeReportFreq === 'spec') {
protractorUtil.writeReport(context, done);
} else {
done();
}
});
}
}
protractorUtil.writeReport = function(context, done) {
assert(done);
var file = context.config.reportFile;
// protractorUtil.logDebug('Generating ' + file);
var data = {
tests: protractorUtil.testResults,
stat: protractorUtil.stat,
generatedOn: new Date()
};
fse.outputFile(file, CircularJSON.stringify(data), function(err) {
if (err) {
return done(err);
}
protractorUtil.joinReports(context, done);
});
};
protractorUtil.joinReports = function(context, done) {
assert(done);
var file = context.config.screenshotPath + '/report.js';
var reports = klawSync(context.config.screenshotPath + '/reports/', {
nodir: true
});
var data = {
tests: [],
stat: {
passed: 0,
failed: 0,
pending: 0,
disabled: 0
},
ci: context.ci,
generatedOn: new Date()
};
//concat all tests
for (var i = 0; i < reports.length; i++) {
try {
var report = fse.readJsonSync(reports[i].path);
for (var j = 0; j < report.tests.length; j++) {
var test = report.tests[j];
data.tests.push(test);
}
data.stat.passed += report.stat.passed || 0;
data.stat.failed += report.stat.failed || 0;
data.stat.pending += report.stat.pending || 0;
data.stat.disabled += report.stat.disabled || 0;
} catch (err) {
// need to refactor cb to promises + use promise all to avoid concurent writes and reads
// protractorUtil.logDebug('Unknown error while process report %s', reports[i]);
return done(err);
}
}
var before = "angular.module('reporter').constant('data',";
var after = ");";
fse.outputFile(file, before + JSON.stringify(data) + after, function(err) {
if (err) {
return done(err);
}
return done(null);
});
};
protractorUtil.installReporter = function(context) {
var dest = context.config.screenshotPath + '/';
protractorUtil.logInfo('Creating reporter at ' + dest);
try {
var src = path.join(require.resolve('screenshoter-report-analyzer/dist/index.html'), '../');
fse.copy(src, dest);
} catch (err) {
console.error(err);
return;
}
};
protractorUtil.registerJasmineReporter = function(context) {
jasmine.getEnv().addReporter({
jasmineStarted: function() {
global.screenshotBrowsers = {};
protractorUtil.testResults = [];
protractorUtil.stat = {};
if (context.config.htmlReport) {
protractorUtil.installReporter(context);
}
},
specStarted: function() {
protractorUtil.test = {
start: moment(),
specScreenshots: [],
specLogs: [],
specHtmls: [],
failedExpectations: [],
passedExpectations: []
};
protractorUtil.testResults.push(protractorUtil.test);
},
specDone: function(result) {
protractorUtil.takeOnSpecDone(result, context, protractorUtil.test); //exec async operation
//calculate total fails, success and so on
if (!protractorUtil.stat[result.status]) {
protractorUtil.stat[result.status] = 0;
}
protractorUtil.stat[result.status]++;
//calculate diff
protractorUtil.test.end = moment();
protractorUtil.test.diff = protractorUtil.test.end.diff(protractorUtil.test.start, 'ms');
protractorUtil.test.timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
_.merge(protractorUtil.test, result);
if (context.config.writeReportFreq === 'asap' || context.config.writeReportFreq === 'spec') {
protractorUtil.writeReport(context, protractorUtil.newLongRunningOperationCounter());
}
var passed = result.failedExpectations.length === 0;
if (!passed && context.config.pauseOn === 'spec') {
protractorUtil.logInfo('Pause browser because of a spec failed - %s', result.name);
protractorUtil.logDebug(result.failedExpectations[0].message);
protractorUtil.logDebug(result.failedExpectations[0].stack);
global.browser.pause();
}
}
});
};
/**
* Fails the test/spec if browser has console logs
*
* @param {Object} context The plugin context object
* @return {!webdriver.promise.Promise.<R>} A promise
*/
protractorUtil.failTestOnErrorLog = function(context) {
return global.browser.getProcessedConfig().then(function() {
beforeEach(function() {
/*
* A Jasmine custom matcher
*/
var matchers = {
toEqualBecause: function() {
return {
compare: function(actual, expected, custMsg) {
var result = {
pass: jasmine.pp(actual) === jasmine.pp(expected),
message: 'Expected ' + jasmine.pp(actual) + ' to equal ' + jasmine.pp(expected) + ' Because: ' + custMsg
};
return result;
}
};
}
};
global.jasmine.addMatchers(matchers);
});
afterEach(function() {
/*
* Verifies if the `suite` where tests are running is present on the `failTestOnErrorLog.suites` list
*/
function isASuiteToCheck() {
//If there are no suites defined the default value is 'ALL'
if (!context.config.failTestOnErrorLog.suites) {
return true;
}
return (context.config.failTestOnErrorLog.suites.indexOf(browser.getProcessedConfig().value_.suite) > -1);
}
/*
* Verifies that console has no error logs, if error log is there test is marked as failure
*/
function verifyConsole(browserLogs, browserName, done) {
// browserLogs is an array of objects with level and message fields
if (browserLogs) {
browserLogs.forEach(function(log) {
var logLevel = context.config.failTestOnErrorLog.failTestOnErrorLogLevel ? context.config.failTestOnErrorLog.failTestOnErrorLogLevel : 900;
var flag = false;
if ((log.level.value > logLevel) && isASuiteToCheck()) { // it's an error log && is a valid suite
if (context.config.failTestOnErrorLog.excludeKeywords) {
context.config.failTestOnErrorLog.excludeKeywords.forEach(function(keyword) {
if (log.message.search(keyword) > -1) {
flag = true;
}
});
}
expect(log.level.value > logLevel && flag).toEqualBecause(true, 'Browser instance ' + browserName + ': Error logs present in console:' + require('util').inspect(log));
}
});
}
done();
}
protractorUtil.takeLogs(context, verifyConsole);
});
});
};
protractorUtil.prototype.obtainCIVariables = function(env) {
if (env.GITLAB_CI) {
return {
build: env.CI_JOB_ID,
branch: env.CI_COMMIT_REF_NAME,
sha: env.CI_COMMIT_SHA,
tag: env.CI_COMMIT_TAG,
name: env.CI_PROJECT_PATH,
commit: env.CI_COMMIT_MSG,
url: env.CI_PROJECT_URL + '/-/jobs/' + env.CI_JOB_ID
}
}
if (env.CIRCLECI) {
return {
build: env.CIRCLE_BUILD_NUM,
branch: env.CIRCLE_BRANCH,
sha: env.CIRCLE_SHA1,
tag: env.CIRCLE_TAG,
name: env.CIRCLE_PROJECT_REPONAME,
commit: env.CIRCLE_MSG,
url: env.CIRCLE_BUILD_URL
}
}
if (env.TRAVIS) {
return {
build: env.TRAVIS_JOB_NUMBER,
branch: env.TRAVIS_BRANCH,
sha: env.TRAVIS_COMMIT,
tag: env.TRAVIS_TAG,
name: env.TRAVIS_REPO_SLUG,
commit: env.TRAVIS_COMMIT_MESSAGE,
url: 'https://travis-this.ci.org/' + env.TRAVIS_REPO_SLUG + '/builds/' + env.TRAVIS_BUILD_ID
}
}
return {};
}
/**
* Initialize configuration
*/
protractorUtil.prototype.setup = function() {
var defaultSettings = {
screenshotPath: './reports/e2e',
clearFoldersBeforeTest: true,
withLogs: true,
screenshotOnExpect: 'failure+success',
htmlOnExpect: 'failure',
dumpOnExpect: 'failure',
verbose: 'info',
screenshotOnSpec: 'failure+success',
htmlOnSpec: 'failure',
dumpOnSpec: 'none',
pauseOn: 'never',
imageToAscii: 'none',
imageToAsciiOpts: {
bg: true
},
dump: null,
htmlReport: true,
writeReportFreq: 'end'
}
this.ci = this.obtainCIVariables(process.env);
this.config = _.merge({}, defaultSettings, this.config);
this.config.reportFile = this.config.screenshotPath + '/reports/' + uuid.v1() + '.js';
if (this.config.verbose === 'debug') {
protractorUtil.logDebug = console.log;
protractorUtil.logInfo = console.info;
}
if (this.config.clearFoldersBeforeTest) {
try {
fse.removeSync(this.config.screenshotPath);
} catch (err) {
console.error(err);
}
}
var self = this;
mkdirp.sync(this.config.screenshotPath + '/screenshots', function(err) {
if (err) {
console.error(err);
} else {
protractorUtil.logDebug(self.config.screenshotPath + '/screenshots' + ' folder created!');
}
});
mkdirp.sync(this.config.screenshotPath + '/htmls', function(err) {
if (err) {
console.error(err);
} else {
protractorUtil.logDebug(self.config.screenshotPath + '/htmls' + ' folder created!');
}
});
mkdirp.sync(this.config.screenshotPath + '/reports', function(err) {
if (err) {
console.error(err);
} else {
protractorUtil.logDebug(self.config.screenshotPath + '/reports' + ' folder created!');
}
});
var pjson = require(__dirname + '/package.json');
protractorUtil.logInfo('Activated Protractor Screenshoter Plugin, ver. ' + pjson.version + ' (c) 2016 - ' + new Date().getFullYear() + ' ' + pjson.author + ' and contributors');
protractorUtil.logDebug('The resolved configuration is:');
protractorUtil.logDebug(this.config);
};
/**
* Sets reporter hooks based on the configurtion
*/
protractorUtil.prototype.onPrepare = function() {
protractorUtil.registerJasmineReporter(this);
if (this.config.screenshotOnExpect != 'none') {
protractorUtil.takeScreenshotOnExpectDone(this);
}
if (this.config.failTestOnErrorLog) {
return protractorUtil.failTestOnErrorLog(this);
}
}
var deferred = q.defer();
protractorUtil.runningOperations = 0;
protractorUtil.resolve = function() {
deferred.resolve.apply(deferred, arguments);
};
protractorUtil.newLongRunningOperationCounter = function() {
protractorUtil.runningOperations++;
// protractorUtil.logDebug('Open operations ', protractorUtil.runningOperations);
return function() {
protractorUtil.runningOperations--;
// protractorUtil.logDebug('Remaining operations ', protractorUtil.runningOperations);
}
};
protractorUtil.prototype.teardown = function() {
// protractorUtil.logDebug('===== teardown screenshoter =====');
var self = this;
function finish() {
protractorUtil.writeReport(self, function(err) {
if (err) {
protractorUtil.logDebug(err);
}
protractorUtil.resolve();
});
}
var attempt = 0;
function waitUntilAllOperationsAreDone() {
attempt++;
// protractorUtil.logDebug('Remaining running operations ', protractorUtil.runningOperations);
if (protractorUtil.runningOperations === 0 || attempt > 10) {
finish();
} else {
setTimeout(waitUntilAllOperationsAreDone, 1000);
}
}
waitUntilAllOperationsAreDone();
return deferred.promise;
};
module.exports = new protractorUtil();