UNPKG

@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
"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