nightwatch
Version:
Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.
666 lines (536 loc) • 17.6 kB
JavaScript
const lodashMerge = require('lodash/merge');
const uuid = require('uuid');
const Utils = require('../utils');
const {Logger} = Utils;
module.exports = class Results {
static get TEST_FAIL() {
return 'fail';
}
static get TEST_PASS() {
return 'pass';
}
static get TEST_SKIP() {
return 'skip';
}
constructor(tests = [], opts, settings, skippedTests = [], allScreenedTests = []) {
this.skippedByUser = skippedTests;
this.skippedAtRuntime = tests.slice(0);
this.testcases = {};
this.testSections = {};
this.testSectionHook = {};
this.isHookRunning = false;
this.suiteName = opts.suiteName;
this.moduleKey = opts.moduleKey;
this.modulePath = opts.modulePath;
this.groupName = opts.groupName;
this.reportPrefix = opts.reportPrefix;
this.testEnv = settings.testEnv;
this.isMobile = opts.isMobile;
this.globalStartTime = new Date().getTime();
this.startTimestamp = this.globalStartTime;
this.endTimestamp = this.globalStartTime;
this.currentTestName = '';
this.currentSectionName = '';
this.__currentTest = null;
this.__initialResult = {
errors: 0,
failed: 0,
passed: 0,
assertions: [],
commands: [],
tests: 0
};
// Adding sessionInfo to reporter
const {capabilities = {}, desiredCapabilities = {}} = settings;
this.sessionCapabilities = capabilities || desiredCapabilities;
this.sessionId = '';
this.projectName = capabilities.projectName || desiredCapabilities.projectName || '';
this.buildName = capabilities.buildName || desiredCapabilities.buildName || '';
const {webdriver = {}} = settings;
this.host = webdriver.host || '';
this.name = opts.suiteName || '';
this.tags = opts.tags || [];
this.__retryTest = false;
this.__uuid = uuid.v4();
this.initCount(allScreenedTests);
}
markHookRun(hookName) {
this.isHookRunning = true;
this.currentHookName = hookName;
this.testSectionHook = this.createTestCaseResults({testName: `${this.currentSectionName}__${hookName}`});
// reset test hooks output
Logger.collectTestHooksOutput();
}
unmarkHookRun() {
this.isHookRunning = false;
if (!this.currentHookName) {
throw new Error('Hook run not started yet');
}
const currentSection = this.getTestSection(this.currentSectionName);
const hookdata = this.testSectionHook;
const startTime = hookdata.startTimestamp;
const endTime = new Date().getTime();
const elapsedTime = endTime - startTime;
hookdata.endTimestamp = endTime;
hookdata.time = (elapsedTime / 1000).toPrecision(4);
hookdata.timeMs = elapsedTime;
hookdata.httpOutput = Logger.collectTestHooksOutput();
if (hookdata.errors > 0 || hookdata.failed > 0) {
hookdata.status = Results.TEST_FAIL;
}
currentSection[this.currentHookName] = hookdata;
}
get initialResult() {
return this.__initialResult;
}
get currentTestResult() {
const testName = this.currentTest.name;
return this.getTestResult(testName, {returnFullResult: true});
}
get uuid() {
return this.__uuid;
}
/**
* @param {TestCase} value
*/
set currentTest(value) {
this.__currentTest = value;
}
getTestResult(testName, {returnFullResult = false} = {}) {
if (!testName || !this.testcases[testName]) {
return this.initialResult;
}
const currentTest = this.testcases[testName];
if (returnFullResult) {
currentTest.steps = this.skippedAtRuntime;
currentTest.stackTrace = this.stackTrace;
currentTest.testcases = Object.keys(this.testcases).reduce((prev, key) => {
prev[key] = Object.keys(this.testcases[key]).reduce((prevVal, prop) => {
if (prop !== 'testcases') {
prevVal[prop] = this.testcases[key][prop];
}
if (prop === 'assertions') {
prevVal.tests = this.testcases[key].assertions;
}
return prevVal;
}, {});
return prev;
}, {});
}
return currentTest;
}
getTestSection(testName) {
if (!testName || !this.testSections[testName]) {
return this.initialResult;
}
return this.testSections[testName];
}
/**
* @return {TestCase}
*/
getCurrentTest() {
return this.__currentTest;
}
get currentTest() {
if (!this.__currentTest) {
return null;
}
const name = this.__currentTest.testName;
return {
name,
module: this.moduleKey,
group: this.groupName,
results: this.getTestResult(name, {returnFullResult: true}),
timestamp: this.timestamp
};
}
get currentSection() {
return this.getTestSection(this.currentSectionName);
}
hasFailures() {
return this.currentTestResult.errors > 0 || this.currentTestResult.failed > 0;
}
////////////////////////////////////////////////////////////
// Counters
////////////////////////////////////////////////////////////
get passedCount() {
return this.__passedCount;
}
get failedCount() {
return this.__failedCount;
}
get errorsCount() {
return this.__errorsCount;
}
get skippedCount() {
return this.__skippedCount;
}
get timestamp() {
return this.__timestamp;
}
get testsCount() {
return this.__testsCount;
}
get lastError() {
return this.__lastError;
}
get stackTrace() {
return (this.__lastError && this.__lastError instanceof Error) ? this.__lastError.stack : '';
}
get currentTestElapsedTime() {
return this.currentTestResult.timeMs;
}
get currentTestCasePassed() {
return this.currentTestResult.errors === 0 && this.currentTestResult.failed === 0;
}
/**
* Exports the complete results for the entire testsuite
*
* @return {object}
*/
get export() {
let lastError = null;
const {moduleKey, reportPrefix} = this;
const suiteResults = {
moduleKey,
hasFailures: !this.testsPassed(),
results: {
uuid: this.uuid,
reportPrefix,
assertionsCount: this.initialResult.assertions.length
}
};
Object.keys(this.testcases).forEach(key => {
const testcase = this.testcases[key];
if (testcase.lastError) {
//lastError = testcase.lastError;
}
suiteResults.results.assertionsCount += testcase.assertions.length;
});
if (!lastError && this.lastError) {
lastError = this.lastError;
}
suiteResults.results.lastError = lastError;
suiteResults.results.skippedAtRuntime = this.skippedAtRuntime;
suiteResults.results.skippedByUser = this.skippedByUser;
suiteResults.results.skipped = [...this.skippedAtRuntime, ...this.skippedByUser];
suiteResults.results.time = this.time;
suiteResults.results.timeMs = this.timeMs;
suiteResults.results.completed = this.testcases;
suiteResults.results.completedSections = this.testSections;
suiteResults.results.errmessages = this.errmessages;
suiteResults.results.testsCount = this.testsCount;
suiteResults.results.skippedCount = this.skippedCount;
suiteResults.results.failedCount = this.failedCount;
suiteResults.results.errorsCount = this.errorsCount;
suiteResults.results.passedCount = this.passedCount;
suiteResults.results.group = this.groupName;
suiteResults.results.modulePath = this.modulePath;
suiteResults.results.startTimestamp = new Date(this.startTimestamp).toUTCString();
suiteResults.results.endTimestamp = new Date(this.endTimestamp).toUTCString();
suiteResults.results.sessionCapabilities = this.sessionCapabilities;
suiteResults.results.sessionId = this.sessionId;
suiteResults.results.projectName = this.projectName;
suiteResults.results.buildName = this.buildName;
suiteResults.results.testEnv = this.testEnv;
suiteResults.results.isMobile = this.isMobile;
suiteResults.results.status = this.getTestStatus();
suiteResults.results.seleniumLog = this.seleniumLog;
suiteResults.results.host = this.host;
suiteResults.results.name = this.name;
suiteResults.results.tags = this.tags;
// Backwards compat
suiteResults.results.tests = this.testsCount;
suiteResults.results.failures = this.failedCount;
suiteResults.results.errors = this.errorsCount;
suiteResults.results.group = this.groupName;
return suiteResults;
}
initCurrentTest(testcase) {
this.currentTest = testcase;
return this;
}
resetCurrentTestName() {
// FIXME: in v2, this needs to be this.__currentTest.testName, but it isn't desirable now because will make
// client.currentTest.name empty in the after() hook and potentially introduce breaking changes
//this.__currentTest.testName = '';
this.currentTestName = '';
}
resetCurrentSectionName() {
this.currentSectionName = '';
}
getCurrentTestName() {
// FIXME: see resetCurrentTestName()
const {testName} = this.getCurrentTest() || {};
return this.currentTestName;
}
set retryTest(value) {
this.__retryTest = value;
}
get retryTest() {
return this.__retryTest;
}
/**
* @param {TestCase} testcase
* @return {Object}
*/
createTestCaseResults(testcase) {
const result = {
time: 0,
assertions: [],
commands: [],
passed: 0,
errors: 0,
failed: 0,
retries: testcase.retriesCount,
skipped: 0,
tests: 0,
status: Results.TEST_PASS,
startTimestamp: new Date().getTime(),
httpOutput: []
};
if (this.retryTest && this.testSections[testcase.testName]) {
const retryTestData = this.testSections[testcase.testName];
result['retryTestData'] = [retryTestData];
if (retryTestData['retryTestData']) {
result['retryTestData'].push(...retryTestData['retryTestData']);
delete retryTestData['retryTestData'];
}
this.retryTest = false;
}
return result;
}
resetLastError() {
this.__lastError = null;
}
setLastError(err, {incrementTotal, addToErrArray = false} = {}) {
const testName = this.getCurrentTestName();
this.__lastError = err;
if (testName && this.testcases[testName]) {
this.testcases[testName].lastError = err;
if (addToErrArray) {
this.testcases[testName].errorsPerTest = this.testcases[testName].errorsPerTest || [];
this.testcases[testName].errorsPerTest.push(err.message);
}
}
const detailedLogging = err.detailedLogging || Utils.isUndefined(err.detailedLogging);
if ((!testName || addToErrArray) && incrementTotal && detailedLogging) {
const errorMessage = Logger.getErrorContent(err, this.modulePath);
this.errmessages.push(errorMessage);
}
return this;
}
/**
* @param {Boolean} incrementTotal
* @return {Results}
*/
incrementErrorCount(incrementTotal = true) {
if (incrementTotal) {
this.__errorsCount++;
}
const result = this.getTestResult(this.currentTestName);
result.errors++;
return this;
}
/**
* @param {Boolean} incrementTotal
* @return {Results}
*/
incrementFailedCount(incrementTotal = true) {
if (incrementTotal) {
this.__failedCount++;
}
const result = this.getTestResult(this.currentTestName);
result.failed++;
return this;
}
incrementPassedCount() {
this.__passedCount++;
const result = this.getTestResult(this.currentTestName);
result.passed++;
return this;
}
subtractPassedCount(count = 0) {
this.__passedCount = this.__passedCount - count;
return this;
}
/**
* @param {Object} assertion
* @return {module.Results}
*/
logAssertion(assertion) {
const result = this.getTestResult(this.currentTestName);
result.assertions.push(assertion);
// backwards compatibility
result.tests++;
return this;
}
logCommand(command) {
const result = this.getTestSection(this.currentSectionName);
result.commands.push(command);
if (this.isHookRunning) {
this.testSectionHook.commands.push(command);
}
return this;
}
initCount(allScreenedTests) {
this.__passedCount = 0;
this.__failedCount = 0;
this.__errorsCount = 0;
this.__skippedCount = allScreenedTests.length;
this.__testsCount = 0;
this.__timestamp = new Date().toUTCString();
this.errmessages = [];
this.time = 0;
}
setElapsedTime() {
const currentTest = this.getCurrentTest();
const startTime = currentTest ? currentTest.startTime : this.globalStartTime;
const endTime = new Date().getTime();
const elapsedTime = endTime - startTime;
this.endTimestamp = endTime;
this.time += elapsedTime;
if (currentTest) {
this.currentTestResult.time = (elapsedTime / 1000).toPrecision(4);
this.currentTestResult.timeMs = elapsedTime;
this.currentTestResult.startTimestamp = new Date(startTime).toUTCString();
this.currentTestResult.endTimestamp = new Date(endTime).toUTCString();
}
return this;
}
setTestSectionElapsedTime() {
const currentSection = this.getTestSection(this.currentSectionName);
const startTime = currentSection ? currentSection.startTimestamp : this.globalStartTime;
const endTime = new Date().getTime();
const elapsedTime = endTime - startTime;
this.endTimestamp = endTime;
this.time += elapsedTime;
if (currentSection) {
currentSection.time = (elapsedTime / 1000).toPrecision(4);
currentSection.timeMs = elapsedTime;
currentSection.startTimestamp = startTime;
currentSection.endTimestamp = endTime;
}
return this;
}
setTestStatus() {
const currentTest = this.getCurrentTest();
const currentSection = this.getTestSection(this.currentSectionName);
if (!currentTest) {
return;
}
if (this.hasFailures()) {
this.currentTestResult.status = Results.TEST_FAIL;
currentSection.status = Results.TEST_FAIL;
} else {
this.currentTestResult.status = Results.TEST_PASS;
currentSection.status = Results.TEST_PASS;
}
}
collectTestSectionOutput() {
const currentSection = this.getTestSection(this.currentSectionName);
if (!currentSection) {
return;
}
currentSection.httpOutput = Logger.collectTestSectionOutput();
}
setTotalElapsedTime() {
this.timeMs = this.time;
this.time = (this.time / 1000).toPrecision(4);
return this;
}
/**
* Sets the currently running testcase
*
* @param {TestCase} testcase
* @return {Results}
*/
setCurrentTest(testcase) {
this.currentTest = testcase;
const testName = testcase.testName;
this.currentTestName = testName;
this.testcases[testName] = this.createTestCaseResults(testcase);
const index = this.skippedAtRuntime.indexOf(testName);
if (index > -1) {
this.skippedAtRuntime.splice(index, 1);
this.__skippedCount -= 1;
}
this.__testsCount++;
return this;
}
setCurrentSection(testcase) {
this.currentSectionName = testcase.testName;
this.testSections[testcase.testName] = this.createTestCaseResults(testcase);
}
setSeleniumLogFile(outputFilePath) {
this.seleniumLog = outputFilePath;
}
logScreenshotFile(screenshotFile) {
const assertions = this.currentTestResult.assertions;
const lastAssertion = assertions[assertions.length - 1];
if (lastAssertion) {
lastAssertion.screenshots = lastAssertion.screenshots || [];
lastAssertion.screenshots.push(screenshotFile);
}
}
testsPassed() {
return this.failedCount === 0 && this.errorsCount === 0;
}
getTestStatus() {
if (this.failedCount > 0 || this.errorsCount > 0) {
return Results.TEST_FAIL;
}
if (this.passedCount > 0) {
return Results.TEST_PASS;
}
return Results.TEST_SKIP;
}
/**
* Appends data in current testSections data.
*
* @param {Object} data
*/
appendTestResult(data) {
lodashMerge(this.testSections, data);
}
/**
* Combines all the individual test suite reports into a global one
*
* @param {Array} suiteResultsArr
* @param {Object} initialReport
* @return {Object}
*/
static createGlobalReport(suiteResultsArr, initialReport) {
return suiteResultsArr.reduce((prev, item) => {
const results = item.results;
if (results.lastError) {
prev.lastError = results.lastError;
}
// Accumulating stats
prev.passed += results.passedCount;
prev.failed += results.failedCount;
prev.errors += results.errorsCount;
prev.skipped += results.skippedCount;
prev.assertions += results.assertionsCount;
// Accumulating error messages
if (Array.isArray(results.errmessages) && results.errmessages.length > 0) {
prev.errmessages = prev.errmessages.concat(results.errmessages);
}
results.httpOutput = item.httpOutput || [];
results.rawHttpOutput = item.rawHttpOutput || [];
prev.modules[item.moduleKey] = results;
prev.modulesWithEnv[item.results.testEnv] = prev.modulesWithEnv[item.results.testEnv] || {};
prev.modulesWithEnv[item.results.testEnv][item.moduleKey] = results;
return prev;
}, initialReport);
}
get eventDataToEmit() {
const {testEnv, sessionCapabilities, sessionId, tags, modulePath, name, host} = this;
return {
envelope: this.testSections,
metadata: {
testEnv, sessionCapabilities, sessionId, tags, modulePath, name, host
}
};
}
};