Skip to content

Commit

Permalink
Lifecycle event hooks support for v2 (#3817)
Browse files Browse the repository at this point in the history
* Feat/to integration (#15)

* implement lifecycle event hooks

---------

Co-authored-by: Binayak Ghosh <[email protected]>
(cherry picked from commit 5b3df0c)

* remove suport for parallelism from cucumber

(cherry picked from commit 45b4e24)

* add tests

(cherry picked from commit 687ab5c)

* refactor

(cherry picked from commit cf315db)

* refactor usingCucumber param

Co-authored-by: Harshit Agrawal <[email protected]>
(cherry picked from commit dc35eaf)

* modified tests

(cherry picked from commit f5c482e)

* SessionCapabilities handling in parallel mode

(cherry picked from commit 4d2e57d)

* bug fix: require cucumber only when needed

* session capabilities fix for parallel in cucumber

(cherry picked from commit a787b37b37307d8e30ce2539721697c611235bb8)

* code refactoring

(cherry picked from commit 315a2f1)

* logs session capability logs

(cherry picked from commit 9842ff7)

* capabilities session issue fix

(cherry picked from commit be6a6ec)

* capabilities issue fix

(cherry picked from commit 323c3940b9a57a6c750bc1ffc168de1cca711c1a)

* handling the case when auto_start_session will be false

(cherry picked from commit 0d5a9a0)

* code refactoring for capabilities issue

---------

Co-authored-by: Ravi Sawlani <[email protected]>
Co-authored-by: Binayak Ghosh <[email protected]>
  • Loading branch information
3 people authored Sep 21, 2023
1 parent 2b8a604 commit 31902ee
Show file tree
Hide file tree
Showing 29 changed files with 1,053 additions and 103 deletions.
5 changes: 5 additions & 0 deletions lib/api/client-commands/saveScreenshot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const ClientCommand = require('./_base-command.js');
const {Logger, writeToFile} = require('../../utils');
const {COMMON_EVENTS: {ScreenshotCreated}, NightwatchEventHub} = require('../../runner/eventHub.js');

/**
* Take a screenshot of the current page and saves it as the given filename.
Expand All @@ -24,6 +25,10 @@ class SaveScreenshot extends ClientCommand {
if (client.transport.isResultSuccess(result) && result.value) {
writeToFile(fileName, result.value, 'base64').then(() => {
callback.call(this, result);

NightwatchEventHub.emit(ScreenshotCreated, {
path: fileName
});
}).catch(err => {
Logger.error(`Couldn't save screenshot to "${fileName}":`);
Logger.error(err);
Expand Down
7 changes: 7 additions & 0 deletions lib/http/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const Formatter = require('./formatter.js');
const HttpResponse = require('./response.js');
const Utils = require('./../utils');
const {Logger, isString} = Utils;
const {DEFAULT_RUNNER_EVENTS: {LogCreated}, NightwatchEventHub} = require('../runner/eventHub.js');

// To handle Node v17 issue. Refer https://github.com/nodejs/node/issues/40702 for details.
if (dns.setDefaultResultOrder && (typeof dns.setDefaultResultOrder === 'function')) {
Expand Down Expand Up @@ -271,6 +272,12 @@ class HttpRequest extends EventEmitter {
return;
}

if (NightwatchEventHub.runner !== 'cucumber') {
NightwatchEventHub.emit(LogCreated, {
httpOutput: Logger.collectCommandOutput()
});
}

this.emit('complete', result);
}

Expand Down
20 changes: 19 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Utils = require('./utils');
const Settings = require('./settings/settings.js');
const ElementGlobal = require('./api/_loaders/element-global.js');
const NightwatchClient = require('./core/client.js');
const {NightwatchEventHub} = require('./runner/eventHub');

const {Logger} = Utils;

Expand Down Expand Up @@ -129,8 +130,25 @@ module.exports.createClient = function({
.then(() => {
return client.createSession({argv});
})
.then(_ => {
.then((data) => {
if (this.settings.test_runner?.type === 'cucumber' && NightwatchEventHub.isAvailable) {
const NightwatchFormatter = require('./runner/test-runners/cucumber/nightwatch-format');

NightwatchFormatter.setCapabilities(data);
}

return client.api;
})
.catch((error) => {
if (this.settings.test_runner?.type === 'cucumber' && NightwatchEventHub.isAvailable) {
const NightwatchFormatter = require('./runner/test-runners/cucumber/nightwatch-format');

NightwatchFormatter.setCapabilities({
error: error
});
}

throw error;
});
}
};
Expand Down
8 changes: 8 additions & 0 deletions lib/reporter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ class Reporter extends SimplifiedReporter {
this.testResults.setCurrentSection(testcase);
}

markHookRun(hookName) {
this.testResults.markHookRun(hookName);
}

unmarkHookRun(hookName) {
this.testResults.unmarkHookRun(hookName);
}

setAxeResults(results) {
this.currentTest.results.a11y = results;
}
Expand Down
52 changes: 52 additions & 0 deletions lib/reporter/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module.exports = class Results {
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;
Expand Down Expand Up @@ -57,6 +59,41 @@ module.exports = class Results {
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;
}
Expand Down Expand Up @@ -408,6 +445,10 @@ module.exports = class Results {
const result = this.getTestSection(this.currentSectionName);
result.commands.push(command);

if (this.isHookRunning) {
this.testSectionHook.commands.push(command);
}

return this;
}

Expand Down Expand Up @@ -599,4 +640,15 @@ module.exports = class 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
}
};
}
};
59 changes: 57 additions & 2 deletions lib/runner/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const Runner = require('../runner.js');
const ProcessListener = require('../process-listener.js');
const analyticsCollector = require('../../utils/analytics.js');
const {Logger, singleSourceFile, isSafari, isLocalhost} = Utils;
const NightwatchEvent = require('../eventHub.js');
const {NightwatchEventHub, DEFAULT_RUNNER_EVENTS} = NightwatchEvent;
const {GlobalHook} = DEFAULT_RUNNER_EVENTS;
const {RealIosDeviceIdError, iosRealDeviceUDID, isRealIos, isMobile, killSimulator} = require('../../utils/mobile.js');

class CliRunner {
Expand Down Expand Up @@ -255,6 +258,38 @@ class CliRunner {
isBstack: this.test_settings.desiredCapabilities['bstack:options'] !== undefined,
test_runner: this.test_settings.test_runner ? this.test_settings.test_runner.type : null
});

this.setupEventHub();
}

isRegisterEventHandlersCallbackExistsInGlobal() {
const {globals} = this.test_settings;
const {plugins = []} = this.globals;

return Utils.isFunction(globals.registerEventHandlers) || plugins.some(plugin => plugin.globals && Utils.isFunction(plugin.globals.registerEventHandlers));
}

setupEventHub() {
if (this.isRegisterEventHandlersCallbackExistsInGlobal() && !NightwatchEventHub.isAvailable) {
NightwatchEventHub.runner = this.testRunner.type;
NightwatchEventHub.isAvailable = true;

const {globals, output_folder} = this.test_settings;
NightwatchEventHub.output_folder = output_folder;
const {plugins} = this.globals;

if (Utils.isFunction(globals.registerEventHandlers)) {
globals.registerEventHandlers(NightwatchEventHub);
}

if (plugins.length > 0) {
plugins.forEach((plugin) => {
if (plugin.globals && Utils.isFunction(plugin.globals.registerEventHandlers)){
plugin.globals.registerEventHandlers(NightwatchEventHub);
}
});
}
}
}

loadTypescriptTranspiler() {
Expand Down Expand Up @@ -354,10 +389,30 @@ class CliRunner {
}

runGlobalHook(key, args = [], isParallelHook = false) {
let promise;
const {globals} = this.test_settings;

if (isParallelHook && Concurrency.isChildProcess() || !isParallelHook && !Concurrency.isChildProcess()) {
return this.globals.runGlobalHook(key, args);
}
const start_time = new Date();

if (Utils.isFunction(globals[key])) {
NightwatchEventHub.emit(GlobalHook[key].started, {
start_time: start_time
});
}

promise = this.globals.runGlobalHook(key, args);

return promise.finally(() => {
if (Utils.isFunction(globals[key])) {
NightwatchEventHub.emit(GlobalHook[key].finished, {
start_time: start_time,
end_time: new Date()
});
}
});
}

return Promise.resolve();
}

Expand Down
140 changes: 140 additions & 0 deletions lib/runner/eventHub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
const {Logger, createFolder} = require('../utils');

class NightwatchEventHub extends EventEmitter {
emit(eventName, data) {
if (this.isAvailable) {
try {
super.emit(eventName, data);
} catch (err) {
Logger.error(err);
}
}

if (eventName === 'TestFinished' && this.output_folder && this.runner === 'cucumber') {
let {output_folder} = this;
output_folder = path.join(output_folder, 'cucumber');
const filename = path.join(output_folder, 'cucumber-report.json');

this.writeReportFile(filename, JSON.stringify(data.report, null, 2), true, output_folder)
.then(_ => {
Logger.info(Logger.colors.stack_trace(`Wrote JSON report file to: ${path.resolve(filename)}`));
});
}
}

get isAvailable() {
return this.eventFnExist;
}

set isAvailable(eventFnExist) {
this.eventFnExist = eventFnExist;
}

set runner(type) {
this.runnerType = type;
}

get runner() {
return this.runnerType;
}

writeReportFile(filename, rendered, shouldCreateFolder, output_folder) {
return (shouldCreateFolder ? createFolder(output_folder) : Promise.resolve())
.then(() => {
return new Promise((resolve, reject) => {
fs.writeFile(filename, rendered, function(err) {
if (err) {
return reject(err);
}

resolve();
});
});
});
}
}

const instance = new NightwatchEventHub();

module.exports = {
NightwatchEventHub: instance,
COMMON_EVENTS: {
ScreenshotCreated: 'ScreenshotCreated'
},
DEFAULT_RUNNER_EVENTS: {
GlobalHook: {
before: {
started: 'GlobalBeforeStarted',
finished: 'GlobalBeforeFinished'
},

beforeChildProcess: {
started: 'GlobalBeforeChildProcessStarted',
finished: 'GlobalBeforeChildProcessFinished'
},

beforeEach: {
started: 'GlobalBeforeEachStarted',
finished: 'GlobalBeforeEachFinished'
},

afterEach: {
started: 'GlobalAfterEachStarted',
finished: 'GlobalAfterEachFinished'
},

afterChildProcess: {
started: 'GlobalAfterChildProcessStarted',
finished: 'GlobalAfterChildProcessFinished'
},

after: {
started: 'GlobalAfterStarted',
finished: 'GlobalAfterFinished'
}
},

TestSuiteHook: {
started: 'TestSuiteStarted',
finished: 'TestSuiteFinished',

before: {
started: 'BeforeStarted',
finished: 'BeforeFinished'
},

beforeEach: {
started: 'BeforeEachStarted',
finished: 'BeforeEachFinished'
},

test: {
started: 'TestRunStarted',
finished: 'TestRunFinished'
},

afterEach: {
started: 'AfterEachStarted',
finished: 'AfterEachFinished'
},

after: {
started: 'AfterStarted',
finished: 'AfterFinished'
}
},

LogCreated: 'LogCreated'
},
CUCUMBER_RUNNER_EVENTS: {
TestStarted: 'TestStarted',
TestFinished: 'TestFinished',
TestCaseStarted: 'TestCaseStarted',
TestCaseFinished: 'TestCaseFinished',
TestStepStarted: 'TestStepStarted',
TestStepFinished: 'TestStepFinished'
}
};
1 change: 1 addition & 0 deletions lib/runner/folder-walk.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Walker {
this.settings = lodashClone(settings, true);
this.argv = argv;
this.usingMocha = this.settings.test_runner && this.settings.test_runner.type === 'mocha';
this.usingCucumber = this.settings.test_runner?.type === 'cucumber';

this.registerMatchers();

Expand Down
Loading

0 comments on commit 31902ee

Please sign in to comment.