@serenity-js/webdriverio
Version:
Adapter that integrates @serenity-js/web with the latest stable version of WebdriverIO, enabling Serenity/JS reporting and using the Screenplay Pattern to write web and mobile test scenarios
394 lines • 15.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebdriverIONotifier = void 0;
const core_1 = require("@serenity-js/core");
const index_js_1 = require("@serenity-js/core/lib/events/index.js");
const index_js_2 = require("@serenity-js/core/lib/model/index.js");
const tiny_types_1 = require("tiny-types");
// interface Argument {
// rows?: {
// cells: string[];
// }[];
// }
//
// export interface testStats {
// type: 'test:start' | 'test:pass' | 'test:fail' | 'test:retry' | 'test:pending' | 'test:end' | 'test:skip';
// title: string;
// parent: string;
// fullTitle: string;
// pending: boolean;
// file?: string;
// duration?: number;
// cid: string;
// specs: string[];
// uid: string;
// pendingReason?: string;
// error?: Error;
// errors?: Error[];
// retries?: number;
// argument?: string | Argument;
// }
//
/**
* @package
*/
class WebdriverIONotifier {
config;
capabilities;
reporter;
successThreshold;
cid;
specs;
failures;
stage;
/**
* We don't have access to the "context" object produced by Mocha,
* and can't assume that other test runners have a similar concept.
* Since, at the time of writing, none of the WebdriverIO rely on this parameter
* using a dummy seems to be sufficient.
* @private
*/
static dummmyContext = {};
events = new EventLog();
suites = [];
constructor(config, capabilities, reporter, successThreshold, cid, specs, failures = 0, stage) {
this.config = config;
this.capabilities = capabilities;
this.reporter = reporter;
this.successThreshold = successThreshold;
this.cid = cid;
this.specs = specs;
this.failures = failures;
this.stage = stage;
}
assignedTo(stage) {
this.stage = stage;
return this;
}
notifyOf(event) {
return (0, tiny_types_1.match)(event)
.when(index_js_1.TestSuiteStarts, WebdriverIONotifier.prototype.onTestSuiteStarts.bind(this))
.when(index_js_1.TestSuiteFinished, WebdriverIONotifier.prototype.onTestSuiteFinished.bind(this))
.when(index_js_1.SceneStarts, WebdriverIONotifier.prototype.onSceneStarts.bind(this))
.when(index_js_1.SceneFinished, WebdriverIONotifier.prototype.onSceneFinished.bind(this))
.when(index_js_1.TestRunFinishes, WebdriverIONotifier.prototype.onTestRunFinishes.bind(this))
.else(() => void 0);
}
onTestRunFinishes() {
return this.invokeHooks('after', this.config.after, [this.failures, this.capabilities, this.specs]);
}
failureCount() {
return this.failures;
}
onTestSuiteStarts(started) {
this.events.record(started.details.correlationId, started);
const suite = this.suiteStartEventFrom(started);
this.reporter.emit('suite:start', suite);
this.suites.push(started.details);
return this.invokeHooks('beforeSuite', this.config.beforeSuite, [suite]); // todo: correct types
}
onTestSuiteFinished(finished) {
this.suites.pop();
const started = this.events.getByCorrelationId(finished.details.correlationId);
const suite = this.suiteEndEventFrom(started, finished);
this.reporter.emit('suite:end', suite);
return this.invokeHooks('afterSuite', this.config.afterSuite, [suite]); // todo: correct types
}
suiteStartEventFrom(started) {
return {
type: 'suite:start',
uid: started.details.correlationId.value,
cid: this.cid,
title: started.details.name.value,
fullTitle: this.suiteNamesConcatenatedWith(started.details.name.value),
parent: this.parentSuiteName(),
file: started.details.location.path.value,
specs: this.specs,
pending: false,
};
}
suiteNamesConcatenatedWith(name) {
return this.suites.map(suite => suite.name.value).concat(name).join(' ');
}
suiteEndEventFrom(started, finished) {
return {
...this.suiteStartEventFrom(started),
type: 'suite:end',
duration: finished.timestamp.diff(started.timestamp).inMilliseconds()
};
}
onSceneStarts(started) {
const test = this.testStartEventFrom(started);
this.events.record(started.sceneId, started);
this.reporter.emit(test.type, test);
return this.invokeHooks('beforeTest', this.config.beforeTest, [this.testFrom(started), WebdriverIONotifier.dummmyContext]);
}
onSceneFinished(finished) {
if (finished.outcome.isWorseThan(this.successThreshold)) {
this.failures++;
}
const started = this.events.getByCorrelationId(finished.sceneId);
let testEnd;
if (this.willBeRetried(finished.outcome)) {
testEnd = this.testEndEventFrom(started, finished);
const type = 'test:retry';
this.reporter.emit(type, {
...testEnd,
type,
error: this.errorFrom(finished.outcome),
});
}
else {
const testResultEvent = this.testResultEventFrom(started, finished);
this.reporter.emit(testResultEvent.type, testResultEvent);
testEnd = this.testEndEventFrom(started, finished);
this.reporter.emit(testEnd.type, testEnd);
}
return this.invokeHooks('afterTest', this.config.afterTest, [this.testFrom(started), WebdriverIONotifier.dummmyContext, this.testResultFrom(started, finished)]);
}
willBeRetried(outcome) {
return outcome instanceof index_js_2.ExecutionIgnored;
}
testShortTitleFrom(started) {
return started.details.name.value
.replace(new RegExp(`^.*?(${this.parentSuiteName()})`), '')
.trim();
}
testFrom(started) {
const title = this.testShortTitleFrom(started);
return {
ctx: WebdriverIONotifier.dummmyContext,
file: started.details.location.path.value,
fullName: this.suiteNamesConcatenatedWith(title),
fullTitle: this.suiteNamesConcatenatedWith(title),
parent: this.parentSuiteName(),
pending: false,
title,
type: 'test' // I _think_ it's either 'test' or 'hook' - https://github.com/mochajs/mocha/blob/0ea732c1169c678ef116c3eb452cc94758fff150/lib/test.js#L31
};
}
testStartEventFrom(started) {
const title = this.testShortTitleFrom(started);
return {
type: 'test:start',
title,
fullTitle: this.suiteNamesConcatenatedWith(title),
parent: this.parentSuiteName(),
file: started.details.location.path.value,
pending: false,
cid: this.cid,
uid: started.sceneId.value,
specs: this.specs,
};
}
parentSuiteName() {
return this.suites.at(-1)?.name.value || '';
}
/**
* test status is 'passed' | 'pending' | 'skipped' | 'failed' | 'broken' | 'canceled'
* Since this is not documented, we're mimicking other WebdriverIO reporters, for example:
* https://github.com/webdriverio/webdriverio/blob/7415f3126e15a733b51721492e4995ceafae6046/packages/wdio-allure-reporter/src/constants.ts#L3-L9
*
* @param started
* @param finished
* @private
*/
testResultFrom(started, finished) {
const duration = finished.timestamp.diff(started.timestamp).inMilliseconds();
const defaultRetries = { attempts: 0, limit: 0 };
const passedOrFailed = (outcome) => this.whenSuccessful(outcome, true, false);
return (0, tiny_types_1.match)(finished.outcome)
.when(index_js_2.ExecutionCompromised, (outcome) => {
const error = this.errorFrom(outcome);
return {
duration,
error,
exception: error.message,
passed: passedOrFailed(outcome),
status: 'broken',
retries: defaultRetries
};
})
.when(index_js_2.ExecutionFailedWithError, (outcome) => {
const error = this.errorFrom(outcome);
return {
duration,
error,
exception: error.message,
passed: passedOrFailed(outcome),
status: 'broken',
retries: defaultRetries
};
})
.when(index_js_2.ExecutionFailedWithAssertionError, (outcome) => {
const error = this.errorFrom(outcome);
return {
duration,
error,
exception: error.message,
passed: passedOrFailed(outcome),
status: 'failed',
retries: defaultRetries
};
})
.when(index_js_2.ImplementationPending, (outcome) => {
const error = this.errorFrom(outcome);
return {
duration,
error,
exception: error.message,
passed: passedOrFailed(outcome),
status: 'pending',
retries: defaultRetries
};
})
.when(index_js_2.ExecutionIgnored, (outcome) => {
const error = this.errorFrom(outcome);
return {
duration,
error,
exception: error.message,
passed: passedOrFailed(outcome),
status: 'canceled', // fixme: mark as canceled for now for the lack of a better alternative;
retries: defaultRetries // consider capturing info about retries from Mocha and putting it on the ExecutionIgnored event so we can pass it on
};
})
.when(index_js_2.ExecutionSkipped, (outcome) => ({
duration,
exception: '',
passed: passedOrFailed(outcome),
status: 'skipped',
retries: defaultRetries
}))
.else(() => ({
duration,
exception: '',
passed: true,
status: 'passed',
retries: defaultRetries
}));
}
testEndEventFrom(started, finished) {
const duration = finished.timestamp.diff(started.timestamp).inMilliseconds();
return {
...this.testStartEventFrom(started),
type: 'test:end',
duration
};
}
whenSuccessful(outcome, resultWhenSuccessful, resultWhenNotSuccessful) {
return !outcome.isWorseThan(this.successThreshold) && (outcome instanceof index_js_2.ProblemIndication)
? resultWhenSuccessful
: resultWhenNotSuccessful;
}
testResultEventFrom(started, finished) {
const test = this.testEndEventFrom(started, finished);
const unlessSuccessful = (outcome, type) => this.whenSuccessful(outcome, 'test:pass', type);
return (0, tiny_types_1.match)(finished.outcome)
.when(index_js_2.ExecutionCompromised, (outcome) => ({
...test,
type: unlessSuccessful(outcome, 'test:fail'),
error: this.errorFrom(outcome),
}))
.when(index_js_2.ExecutionFailedWithError, (outcome) => ({
...test,
type: unlessSuccessful(outcome, 'test:fail'),
error: this.errorFrom(outcome),
}))
.when(index_js_2.ExecutionFailedWithAssertionError, (outcome) => ({
...test,
type: unlessSuccessful(outcome, 'test:fail'),
error: this.errorFrom(outcome),
}))
.when(index_js_2.ImplementationPending, (outcome) => ({
...test,
type: unlessSuccessful(outcome, 'test:pending'),
error: this.errorFrom(outcome),
pending: true,
pendingReason: outcome.error.message
}))
.when(index_js_2.ExecutionIgnored, (outcome) => ({
...test,
// In WebdriverIO, skipped == pending == ignored
// https://github.com/webdriverio/webdriverio/blob/a1830046f367be7737af2c00561796c3ae5dd85b/packages/wdio-reporter/src/index.ts#L162
type: unlessSuccessful(outcome, 'test:pending'),
error: this.errorFrom(outcome),
pending: true,
pendingReason: outcome.error.message
}))
.when(index_js_2.ExecutionSkipped, (outcome) => ({
...test,
// In WebdriverIO, skipped == pending == ignored
// https://github.com/webdriverio/webdriverio/blob/a1830046f367be7737af2c00561796c3ae5dd85b/packages/wdio-reporter/src/index.ts#L162
type: unlessSuccessful(outcome, 'test:pending'),
pending: true,
}))
.else(() => ({
...test,
type: 'test:pass',
}));
}
errorFrom(outcome) {
const error = outcome.error;
// https://github.com/webdriverio/webdriverio/blob/7ec2c60a7623de431d60bb3605957e6e4bdf057b/packages/wdio-mocha-framework/src/index.ts#L233
return {
name: error.name,
message: error.message,
stack: error.stack,
type: error.type || error.name,
expected: error.expected,
actual: error.actual
};
}
/**
* @see https://github.com/webdriverio/webdriverio/blob/main/packages/wdio-utils/src/shim.ts
* @param hookName
* @param hookFunctions
* @param args
* @private
*/
invokeHooks(hookName, hookFunctions, args) {
const hooks = Array.isArray(hookFunctions)
? hookFunctions
: [hookFunctions];
const asyncOperationId = index_js_2.CorrelationId.create();
this.stage.announce(new index_js_1.AsyncOperationAttempted(new index_js_2.Name(`WebdriverIONotifier#invokeHooks`), new index_js_2.Description(`Invoking ${hookName} hook`), asyncOperationId, this.stage.currentTime()));
return Promise.all(hooks.map((hook) => new Promise((resolve) => {
let result;
try {
result = hook(...args);
}
catch (error) {
return resolve(error);
}
/**
* if a promise is returned make sure we don't have a catch handler
* so in case of a rejection it won't cause the hook to fail
*/
if (result && typeof result.then === 'function') {
return result.then(resolve, (error) => {
resolve(error);
});
}
resolve(result);
}))).
then(results => {
this.stage.announce(new index_js_1.AsyncOperationCompleted(asyncOperationId, this.stage.currentTime()));
return results;
});
}
}
exports.WebdriverIONotifier = WebdriverIONotifier;
class EventLog {
events = new Map();
record(correlationId, event) {
this.events.set(correlationId.value, event);
}
getByCorrelationId(correlationId) {
if (!this.events.has(correlationId.value)) {
throw new core_1.LogicError(`Event with correlation id ${correlationId} has never been recorded`);
}
return this.events.get(correlationId.value);
}
}
//# sourceMappingURL=WebdriverIONotifier.js.map