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

250 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SerenityReporterForJasmine = void 0; const core_1 = require("@serenity-js/core"); const events_1 = require("@serenity-js/core/events"); const io_1 = require("@serenity-js/core/io"); const model_1 = require("@serenity-js/core/model"); /** * [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 - The Serenity instance * @param requirementsHierarchy - The requirements hierarchy for tagging */ constructor(serenity, requirementsHierachy) { this.serenity = serenity; this.requirementsHierachy = requirementsHierachy; } jasmineStarted(info) { this.emit(new events_1.TestRunStarts(this.serenity.currentTime())); } suiteStarted(result) { this.describes.push(result); this.emit(new events_1.TestSuiteStarts(this.testSuiteDetailsOf(result), this.serenity.currentTime())); } suiteDone(result) { this.describes = this.describes.filter(suite => suite.id !== result.id); this.emit(new events_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 events_1.SceneStarts(this.currentSceneId, scenarioDetails, this.serenity.currentTime()), ...this.requirementsHierachy.requirementTagsFor(scenarioDetails.location.path, model_1.Tags.stripFrom(this.currentFeatureNameFor(result))) .map(tag => new events_1.SceneTagged(this.currentSceneId, tag, this.serenity.currentTime())), new events_1.TestRunnerDetected(this.currentSceneId, new model_1.Name('Jasmine'), this.serenity.currentTime()), ...scenarioTags.map(tag => new events_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 location = this.locationOf(result); const activityDetails = new model_1.ActivityDetails(new model_1.Name('Expectation'), new io_1.FileSystemLocation(io_1.Path.from(location.path), location.line, location.column)); const activityId = this.serenity.assignNewActivityId(activityDetails); this.emit(new events_1.TaskStarts(sceneId, activityId, activityDetails, this.serenity.currentTime()), new events_1.TaskFinished(sceneId, activityId, activityDetails, this.failureOutcomeFrom(failedExpectation), this.serenity.currentTime())); }); } const { scenarioDetails } = this.scenarioDetailsOf(result), outcome = this.outcomeFrom(result); this.emit(new events_1.SceneFinishes(this.currentSceneId, outcome, this.serenity.currentTime())); return this.serenity.waitForNextCue() .then(() => { this.emit(new events_1.SceneFinished(this.currentSceneId, scenarioDetails, outcome, this.serenity.currentTime())); }, error => { const errorOutcome = new model_1.ExecutionFailedWithError(error); this.emit(new events_1.SceneFinished(this.currentSceneId, scenarioDetails, errorOutcome.isWorseThan(outcome) ? errorOutcome : outcome, this.serenity.currentTime())); throw error; }); } jasmineDone(suiteInfo) { this.emit(new events_1.TestRunFinishes(this.serenity.currentTime())); return this.serenity.waitForNextCue() .then(() => { this.emit(new events_1.TestRunFinished(new model_1.ExecutionSuccessful(), this.serenity.currentTime())); }) .catch(error => { this.emit(new events_1.TestRunFinished(new model_1.ExecutionFailedWithError(error), this.serenity.currentTime())); throw error; }); } /** * @private * @param {DomainEvent[]} events */ emit(...events) { events.forEach(event => this.serenity.announce(event)); } /** * Extracts location information from a spec or suite result. * Supports both Jasmine 5.x and 6.x (location object from monkey-patching). * * @private * @param result - The spec or suite result * @returns Location object with path, line, and column */ locationOf(result) { // Jasmine 5.x and 6.x with monkey-patching provides location object if (result.location) { return result.location; } // Fallback: use filename property if available (Jasmine 6.x without monkey-patching) if (result.filename) { return { path: result.filename, line: 0, column: 0, }; } // Fallback for edge cases return { path: 'unknown', line: 0, column: 0, }; } /** * @private * @param {SpecResult} spec * @returns {ScenarioDetails} */ scenarioDetailsOf(spec) { const name = this.currentScenarioNameFor(spec.description); const featureName = this.currentFeatureNameFor(spec); const location = this.locationOf(spec); return { scenarioDetails: new model_1.ScenarioDetails(new model_1.Name(model_1.Tags.stripFrom(name)), new model_1.Category(model_1.Tags.stripFrom(featureName)), new io_1.FileSystemLocation(io_1.Path.from(location.path), location.line, location.column)), scenarioTags: model_1.Tags.from(`${featureName} ${name}`) }; } /** * @private * @param {SuiteResult} result * @returns {TestSuiteDetails} */ testSuiteDetailsOf(result) { const location = this.locationOf(result); return new model_1.TestSuiteDetails(new model_1.Name(result.description), new io_1.FileSystemLocation(io_1.Path.from(location.path), location.line, location.column), new model_1.CorrelationId(result.id)); } /** * @private * @returns {string} */ currentFeatureNameFor(spec) { const location = this.locationOf(spec); const path = new io_1.Path(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 model_1.ImplementationPending(new core_1.ImplementationPendingError(result.pendingReason || '')); case 'excluded': return new model_1.ExecutionSkipped(); case 'passed': default: return new model_1.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 model_1.ExecutionFailedWithAssertionError(error); } if (error instanceof core_1.TestCompromisedError) { return new model_1.ExecutionCompromised(error); } if (failure.matcherName) { // the presence of a non-empty matcherName property indicates a Jasmine-specific assertion error return new model_1.ExecutionFailedWithAssertionError(this.serenity.createError(core_1.AssertionError, { message: failure.message, diff: { expected: failure.expected, actual: failure.actual, }, cause: error, })); } return new model_1.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