UNPKG

@serenity-js/jasmine

Version:

Serenity/JS test runner adapter for Jasmine, enabling the use of the Screenplay Pattern in Jasmine-based test suites and leveraging Serenity/JS reporting capabilities

214 lines 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SerenityReporterForJasmine = 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/io/index.js"); const index_js_3 = require("@serenity-js/core/lib/model/index.js"); /** * [Jasmine reporter](https://jasmine.github.io/tutorials/custom_reporter) that translates Jasmine-specific test events * to Serenity/JS events. */ class SerenityReporterForJasmine { serenity; requirementsHierachy; static errorMessagePattern = /^([^\s:]*Error):\s(.*)$/m; describes = []; currentSceneId = undefined; /** * @param {Serenity} serenity */ constructor(serenity, requirementsHierachy) { this.serenity = serenity; this.requirementsHierachy = requirementsHierachy; } jasmineStarted(info) { this.emit(new index_js_1.TestRunStarts(this.serenity.currentTime())); } suiteStarted(result) { this.describes.push(result); this.emit(new index_js_1.TestSuiteStarts(this.testSuiteDetailsOf(result), this.serenity.currentTime())); } suiteDone(result) { this.describes = this.describes.filter(suite => suite.id !== result.id); this.emit(new index_js_1.TestSuiteFinished(this.testSuiteDetailsOf(result), this.outcomeFrom(result), this.serenity.currentTime())); } specStarted(result) { this.currentSceneId = this.serenity.assignNewSceneId(); const { scenarioDetails, scenarioTags } = this.scenarioDetailsOf(result); this.emit(new index_js_1.SceneStarts(this.currentSceneId, scenarioDetails, this.serenity.currentTime()), ...this.requirementsHierachy.requirementTagsFor(scenarioDetails.location.path, index_js_3.Tags.stripFrom(this.currentFeatureNameFor(result))) .map(tag => new index_js_1.SceneTagged(this.currentSceneId, tag, this.serenity.currentTime())), new index_js_1.TestRunnerDetected(this.currentSceneId, new index_js_3.Name('Jasmine'), this.serenity.currentTime()), ...scenarioTags.map(tag => new index_js_1.SceneTagged(this.currentSceneId, tag, this.serenity.currentTime()))); } specDone(result) { /** * Serenity doesn't allow for more than one failure per activity, but Jasmine does. * If there are multiple failures we wrap them up in fake activities so that they're all reported correctly. */ if (result.failedExpectations.length > 1) { result.failedExpectations.forEach(failedExpectation => { const sceneId = this.serenity.currentSceneId(); const activityDetails = new index_js_3.ActivityDetails(new index_js_3.Name('Expectation'), new index_js_2.FileSystemLocation(index_js_2.Path.from(result.location.path), result.location.line, result.location.column)); const activityId = this.serenity.assignNewActivityId(activityDetails); this.emit(new index_js_1.TaskStarts(sceneId, activityId, activityDetails, this.serenity.currentTime()), new index_js_1.TaskFinished(sceneId, activityId, activityDetails, this.failureOutcomeFrom(failedExpectation), this.serenity.currentTime())); }); } const { scenarioDetails } = this.scenarioDetailsOf(result), outcome = this.outcomeFrom(result); this.emit(new index_js_1.SceneFinishes(this.currentSceneId, this.serenity.currentTime())); return this.serenity.waitForNextCue() .then(() => { this.emit(new index_js_1.SceneFinished(this.currentSceneId, scenarioDetails, outcome, this.serenity.currentTime())); }, error => { this.emit(new index_js_1.SceneFinished(this.currentSceneId, scenarioDetails, new index_js_3.ExecutionFailedWithError(error), this.serenity.currentTime())); throw error; }); } jasmineDone(suiteInfo) { this.emit(new index_js_1.TestRunFinishes(this.serenity.currentTime())); return this.serenity.waitForNextCue() .then(() => { this.emit(new index_js_1.TestRunFinished(new index_js_3.ExecutionSuccessful(), this.serenity.currentTime())); }) .catch(error => { this.emit(new index_js_1.TestRunFinished(new index_js_3.ExecutionFailedWithError(error), this.serenity.currentTime())); throw error; }); } /** * @private * @param {DomainEvent[]} events */ emit(...events) { events.forEach(event => this.serenity.announce(event)); } /** * @private * @param {SpecResult} spec * @returns {ScenarioDetails} */ scenarioDetailsOf(spec) { const name = this.currentScenarioNameFor(spec.description); const featureName = this.currentFeatureNameFor(spec); return { scenarioDetails: new index_js_3.ScenarioDetails(new index_js_3.Name(index_js_3.Tags.stripFrom(name)), new index_js_3.Category(index_js_3.Tags.stripFrom(featureName)), index_js_2.FileSystemLocation.fromJSON(spec.location)), scenarioTags: index_js_3.Tags.from(`${featureName} ${name}`) }; } /** * @private * @param {SuiteResult} result * @returns {TestSuiteDetails} */ testSuiteDetailsOf(result) { return new index_js_3.TestSuiteDetails(new index_js_3.Name(result.description), index_js_2.FileSystemLocation.fromJSON(result.location), new index_js_3.CorrelationId(result.id)); } /** * @private * @returns {string} */ currentFeatureNameFor(spec) { const path = new index_js_2.Path(spec.location.path); return this.describes[0] ? this.describes[0].description : this.serenity.cwd().relative(path).value; } /** * @private * @param {string} itBlockDescription * @returns {string} */ currentScenarioNameFor(itBlockDescription) { const [topSuite_, ...rest] = this.describes; return rest.reverse() .reduce((name, current) => `${current.description} ${name}`, itBlockDescription); } /** * @private * @param {SpecResult | SuiteResult} result * @returns {Outcome} */ outcomeFrom(result) { switch (result.status) { case 'failed': return this.failureOutcomeFrom(result.failedExpectations[0]); case 'pending': return new index_js_3.ImplementationPending(new core_1.ImplementationPendingError(result.pendingReason || '')); case 'excluded': return new index_js_3.ExecutionSkipped(); case 'passed': // eslint-disable-line unicorn/no-useless-switch-case default: return new index_js_3.ExecutionSuccessful(); } } /** * @private * @param {Expectation} failure * @returns {ProblemIndication} */ failureOutcomeFrom(failure) { const error = this.errorFrom(failure); if (error instanceof core_1.AssertionError) { // sadly, Jasmine error propagation mechanism is rather basic // and unable to serialise the expected/actual properties of the AssertionError object return new index_js_3.ExecutionFailedWithAssertionError(error); } if (error instanceof core_1.TestCompromisedError) { return new index_js_3.ExecutionCompromised(error); } if (failure.matcherName) { // the presence of a non-empty matcherName property indicates a Jasmine-specific assertion error return new index_js_3.ExecutionFailedWithAssertionError(this.serenity.createError(core_1.AssertionError, { message: failure.message, diff: { expected: failure.expected, actual: failure.actual, }, cause: error, })); } return new index_js_3.ExecutionFailedWithError(error); } errorFrom(failure) { if (this.containsCorrectlySerialisedError(failure)) { return core_1.ErrorSerialiser.deserialiseFromStackTrace(failure.stack); } if (this.containsIncorrectlySerialisedErrorWithErrorPropertiesInStack(failure)) { return core_1.ErrorSerialiser.deserialiseFromStackTrace(this.repairedStackTraceOf(failure)); } if (this.containsIncorrectlySerialisedError(failure)) { return core_1.ErrorSerialiser.deserialiseFromStackTrace(this.repairedStackTraceOf(failure)); } return new Error(failure.message); } containsCorrectlySerialisedError(failure) { return !!failure.stack && SerenityReporterForJasmine.errorMessagePattern.test(failure.stack.split('\n')[0]); } containsIncorrectlySerialisedErrorWithErrorPropertiesInStack(failure) { return !!failure.stack && failure.stack.startsWith('error properties: ') && SerenityReporterForJasmine.errorMessagePattern.test(failure.message); } containsIncorrectlySerialisedError(failure) { return !!failure.stack && SerenityReporterForJasmine.errorMessagePattern.test(failure.message); } /** * It seems like Jasmine mixes up serialisation and display logic, * which means that its "failure.stack" is not really an Error stacktrace, * but rather something along the lines of: * "error properties: AssertionError: undefined" * where the error message is lost, and there's an "error properties:" prefix present. * * Probably caused by this: * https://github.com/jasmine/jasmine/blob/b4cbe9850fbe192eaffeae450669f96e79a574ed/src/core/ExceptionFormatter.js#L93 * * @param {Expectation} failure */ repairedStackTraceOf(failure) { const lastLine = failure.message.split('\n').pop(); const frames = failure.stack.split('\n').filter(line => !line.startsWith('error properties:')); return [ failure.message, ...frames.slice(frames.indexOf(lastLine) + 1), ].join('\n'); } } exports.SerenityReporterForJasmine = SerenityReporterForJasmine; //# sourceMappingURL=SerenityReporterForJasmine.js.map