cypress-terminal-report
Version:
Better terminal and file output for cypress test logs.
326 lines (325 loc) • 15.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = __importDefault(require("../constants"));
const LogCollectControlBase_1 = __importDefault(require("./LogCollectControlBase"));
const utils_1 = __importDefault(require("../utils"));
/**
* Collects and dispatches all logs from all tests and hooks.
*/
class LogCollectControlExtended extends LogCollectControlBase_1.default {
constructor(collectorState, config) {
super();
this.collectorState = collectorState;
this.config = config;
this.registerCypressBeforeMochaHooksSealEvent();
}
register() {
this.collectorState.setStrict();
this.registerState();
this.registerBeforeAllHooks();
this.registerAfterAllHooks();
this.registerTests();
this.registerLogToFiles();
}
triggerSendTask(buildDataMessage, noQueue, wait) {
if (noQueue) {
this.debugLog('Sending with debounce.');
this.debounceNextMochaSuite(Promise.resolve()
// Need to wait for command log update debounce.
.then(() => new Promise((resolve) => setTimeout(resolve, wait)))
.then(() => utils_1.default.nonQueueTask(constants_1.default.TASK_NAME, buildDataMessage()))
.catch(console.error));
}
else {
// Need to wait for command log update debounce.
cy.wait(wait, { log: false }).then(() => {
cy.task(constants_1.default.TASK_NAME, buildDataMessage(), { log: false });
});
}
}
registerState() {
const self = this;
this.debugLog('Registering state.');
Cypress.on('log:changed', (options) => {
if (options.state === 'failed') {
this.collectorState.updateLogStatus(options.id);
}
});
Cypress.mocha.getRunner().on('test', (test) => {
this.collectorState.startTest(test);
});
Cypress.mocha.getRunner().on('suite', () => {
this.collectorState.startSuite();
});
Cypress.mocha.getRunner().on('suite end', () => {
this.collectorState.endSuite();
});
// Keeps track of before and after all hook indexes.
Cypress.mocha.getRunner().on('hook', function (hook) {
if (!hook._ctr_hook && !hook.fn._ctr_hook) {
// After each hooks get merged with the test.
if (hook.hookName !== 'after each') {
self.collectorState.addNewLogStack();
}
// Before each hooks also get merged with the test.
if (hook.hookName === 'before each') {
self.collectorState.markCurrentStackFromBeforeEach();
}
if (hook.hookName === 'before all') {
self.collectorState.incrementBeforeHookIndex();
}
else if (hook.hookName === 'after all') {
self.collectorState.incrementAfterHookIndex();
}
}
});
}
registerBeforeAllHooks() {
const self = this;
// Logs commands from before all hook if the hook passed.
Cypress.mocha.getRunner().on('hook end', function (hook) {
if (hook.hookName === 'before all' &&
self.collectorState.hasLogsInCurrentStack() &&
!hook._ctr_hook) {
self.debugLog('extended: sending logs of passed before all hook');
self.sendLogsToPrinter(self.collectorState.getCurrentLogStackIndex(), this.currentRunnable, {
state: 'passed',
isHook: true,
title: self.collectorState.getBeforeHookTestTile(),
consoleTitle: self.collectorState.getBeforeHookTestTile(),
});
}
});
// Logs commands from before all hooks that failed.
Cypress.on('before:mocha:hooks:seal', function () {
const testBeforeAllSent = [];
self.prependBeforeAllHookInAllSuites([this.mocha.getRootSuite()], function ctrAfterAllPerSuite() {
var _a, _b, _c;
const suiteHasTestAsChild = (suite, test) => {
if (suite === null || suite === void 0 ? void 0 : suite.tests.includes(test)) {
return true;
}
return (suite === null || suite === void 0 ? void 0 : suite.suites.some((s) => suiteHasTestAsChild(s, test))) || false;
};
if (((_a = this.currentTest) === null || _a === void 0 ? void 0 : _a.hookName) === 'before all' &&
((_b = this.currentTest) === null || _b === void 0 ? void 0 : _b.failedFromHookId) && // This is how we know a hook failed the suite.
suiteHasTestAsChild((_c = this.test) === null || _c === void 0 ? void 0 : _c.parent, this.currentTest) && // Since we have after all in each suite we need this for nested suites case.
!testBeforeAllSent.includes(this.currentTest.id) &&
self.collectorState.hasLogsInCurrentStack()) {
testBeforeAllSent.push(this.currentTest.id);
self.debugLog('extended: sending logs of failed before all hook');
self.sendLogsToPrinter(self.collectorState.getCurrentLogStackIndex(), this.currentTest, {
state: 'failed',
title: self.collectorState.getBeforeHookTestTile(),
isHook: true,
});
}
});
});
}
registerAfterAllHooks() {
const self = this;
// Logs commands from after all hooks that passed.
Cypress.mocha.getRunner().on('hook end', function (hook) {
if (hook.hookName === 'after all' &&
self.collectorState.hasLogsInCurrentStack() &&
!hook._ctr_hook) {
self.debugLog('extended: sending logs of passed after all hook');
self.sendLogsToPrinter(self.collectorState.getCurrentLogStackIndex(), hook, {
state: 'passed',
title: self.collectorState.getAfterHookTestTile(),
consoleTitle: self.collectorState.getAfterHookTestTile(),
isHook: true,
noQueue: true,
});
}
});
// Logs after all hook commands when a command fails in the hook.
Cypress.prependListener('fail', function (error) {
const currentRunnable = this.mocha.getRunner().currentRunnable;
if (currentRunnable.hookName === 'after all' && self.collectorState.hasLogsInCurrentStack()) {
// We only have the full list of commands when the suite ends.
this.mocha.getRunner().prependOnceListener('suite end', () => {
self.debugLog('extended: sending logs of failed after all hook');
self.sendLogsToPrinter(self.collectorState.getCurrentLogStackIndex(), currentRunnable, {
state: 'failed',
title: self.collectorState.getAfterHookTestTile(),
isHook: true,
noQueue: true,
wait: 8, // Need to wait so that cypress log updates happen.
});
});
// Have to wait for debounce on log updates to have correct state information.
// Done state is used as callback and awaited in Cypress.fail.
Cypress.state('done', async (error) => {
await new Promise((resolve) => setTimeout(resolve, 6));
throw error;
});
}
// Check if there are other fail listeners registered.
// If yes, let them handle the error; if no, we throw.
const failListeners = Cypress.listeners('fail');
const hasOtherListeners = failListeners && failListeners.length > 1;
if (!hasOtherListeners) {
Cypress.state('error', error);
throw error;
}
});
}
registerTests() {
const self = this;
const sendLogsToPrinterForATest = (test) => {
// We take over logging the passing test titles since we need to control when it gets printed so
// that our logs come after it is printed.
if (test.state === 'passed') {
this.printPassingMochaTestTitle(test);
this.preventNextMochaPassEmit();
}
this.sendLogsToPrinter(this.collectorState.getCurrentLogStackIndex(), test, { noQueue: true });
};
const testHasAfterEachHooks = (test) => {
let suite = test.parent;
while (suite) {
const _afterEach = suite['_afterEach'];
if (_afterEach.length > 0) {
return true;
}
suite = suite.parent;
}
return false;
};
const isLastAfterEachHookForTest = (test, hook) => {
let suite = test.parent;
while (suite) {
const _afterEach = suite['_afterEach'];
if (_afterEach.length > 0) {
return _afterEach.indexOf(hook) === _afterEach.length - 1;
}
suite = suite.parent;
}
return false;
};
// Logs commands form each separate test when after each hooks are present.
Cypress.mocha.getRunner().on('hook end', function (hook) {
if (hook.hookName === 'after each') {
if (isLastAfterEachHookForTest(self.collectorState.getCurrentTest(), hook)) {
self.debugLog('extended: sending logs for ended test, just after the last after each hook: ' +
self.collectorState.getCurrentTest().title);
sendLogsToPrinterForATest(self.collectorState.getCurrentTest());
}
}
});
// Logs commands form each separate test when there is no after each hook.
Cypress.mocha.getRunner().on('test end', function (test) {
if (!testHasAfterEachHooks(test)) {
self.debugLog('extended: sending logs for ended test, that has not after each hooks: ' +
self.collectorState.getCurrentTest().title);
sendLogsToPrinterForATest(self.collectorState.getCurrentTest());
}
});
// Logs commands if test was manually skipped.
Cypress.mocha.getRunner().on('pending', function (test) {
if (self.collectorState.getCurrentTest() === test) {
// In case of fully skipped tests we might not yet have a log stack.
if (self.collectorState.hasLogsInCurrentStack()) {
self.debugLog('extended: sending logs for skipped test: ' + test.title);
sendLogsToPrinterForATest(test);
}
}
});
}
registerLogToFiles() {
after(function () {
cy.wait(50, { log: false });
cy.task(constants_1.default.TASK_NAME_OUTPUT, null, { log: false });
});
}
debounceNextMochaSuite(promise) {
const runner = Cypress.mocha.getRunner();
// Hack to make mocha wait for our logs to be written to console before
// going to the next suite. This is because 'fail' and 'suite begin' both
// fire synchronously and thus we wouldn't get a window to display the
// logs between the failed hook title and next suite title.
const originalRunSuite = runner['runSuite'];
runner['runSuite'] = function (...args) {
promise
.catch(() => {
/* noop */
})
// We need to wait here as for some reason the next suite title will be displayed to soon.
.then(() => new Promise((resolve) => setTimeout(resolve, 6)))
.then(() => {
originalRunSuite.apply(runner, args);
runner['runSuite'] = originalRunSuite;
});
};
}
registerCypressBeforeMochaHooksSealEvent() {
// Hack to have dynamic after hook per suite.
// The onSpecReady in cypress is called before the hooks are 'condensed', or so
// to say sealed and thus in this phase we can register dynamically hooks.
const oldOnSpecReady = Cypress.onSpecReady;
Cypress.onSpecReady = function () {
Cypress.emit('before:mocha:hooks:seal');
oldOnSpecReady(...arguments);
};
}
prependBeforeAllHookInAllSuites(rootSuites, hookCallback) {
const recursiveSuites = (suites) => {
if (suites) {
suites.forEach((suite) => {
if (suite.isPending()) {
return;
}
suite.afterAll(hookCallback);
// Make sure our hook is first so that other after all hook logs come after
// the failed before all hooks logs.
const hook = suite['_afterAll'].pop();
suite['_afterAll'].unshift(hook);
// Don't count this in the hook index and logs.
hook._ctr_hook = true;
recursiveSuites(suite.suites);
});
}
};
recursiveSuites(rootSuites);
}
printPassingMochaTestTitle(test) {
if (Cypress.config('isTextTerminal')) {
Cypress.emit('mocha', 'pass', {
id: test.id,
order: test.order,
title: test.title,
state: 'passed',
type: 'test',
duration: test.duration,
wallClockStartedAt: test.wallClockStartedAt,
timings: test.timings,
file: null,
invocationDetails: test.invocationDetails,
final: true,
currentRetry: test['currentRetry'](),
retries: test.retries(),
});
}
}
preventNextMochaPassEmit() {
const oldAction = Cypress.action;
Cypress.action = function (actionName, ...args) {
if (actionName === 'runner:pass') {
Cypress.action = oldAction;
return;
}
return oldAction.call(Cypress, actionName, ...args);
};
}
debugLog(message) {
if (this.config.debug) {
console.log(constants_1.default.DEBUG_LOG_PREFIX + message);
}
}
}
exports.default = LogCollectControlExtended;