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

210 lines 10 kB
import { AssertionError, ErrorSerialiser, ImplementationPendingError, TestCompromisedError } from '@serenity-js/core'; import { SceneFinished, SceneFinishes, SceneStarts, SceneTagged, TaskFinished, TaskStarts, TestRunFinished, TestRunFinishes, TestRunnerDetected, TestRunStarts, TestSuiteFinished, TestSuiteStarts, } from '@serenity-js/core/lib/events/index.js'; import { FileSystemLocation, Path } from '@serenity-js/core/lib/io/index.js'; import { ActivityDetails, Category, CorrelationId, ExecutionCompromised, ExecutionFailedWithAssertionError, ExecutionFailedWithError, ExecutionSkipped, ExecutionSuccessful, ImplementationPending, Name, ScenarioDetails, Tags, TestSuiteDetails, } from '@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. */ export 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 TestRunStarts(this.serenity.currentTime())); } suiteStarted(result) { this.describes.push(result); this.emit(new TestSuiteStarts(this.testSuiteDetailsOf(result), this.serenity.currentTime())); } suiteDone(result) { this.describes = this.describes.filter(suite => suite.id !== result.id); this.emit(new 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 SceneStarts(this.currentSceneId, scenarioDetails, this.serenity.currentTime()), ...this.requirementsHierachy.requirementTagsFor(scenarioDetails.location.path, Tags.stripFrom(this.currentFeatureNameFor(result))) .map(tag => new SceneTagged(this.currentSceneId, tag, this.serenity.currentTime())), new TestRunnerDetected(this.currentSceneId, new Name('Jasmine'), this.serenity.currentTime()), ...scenarioTags.map(tag => new 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 ActivityDetails(new Name('Expectation'), new FileSystemLocation(Path.from(result.location.path), result.location.line, result.location.column)); const activityId = this.serenity.assignNewActivityId(activityDetails); this.emit(new TaskStarts(sceneId, activityId, activityDetails, this.serenity.currentTime()), new TaskFinished(sceneId, activityId, activityDetails, this.failureOutcomeFrom(failedExpectation), this.serenity.currentTime())); }); } const { scenarioDetails } = this.scenarioDetailsOf(result), outcome = this.outcomeFrom(result); this.emit(new SceneFinishes(this.currentSceneId, this.serenity.currentTime())); return this.serenity.waitForNextCue() .then(() => { this.emit(new SceneFinished(this.currentSceneId, scenarioDetails, outcome, this.serenity.currentTime())); }, error => { this.emit(new SceneFinished(this.currentSceneId, scenarioDetails, new ExecutionFailedWithError(error), this.serenity.currentTime())); throw error; }); } jasmineDone(suiteInfo) { this.emit(new TestRunFinishes(this.serenity.currentTime())); return this.serenity.waitForNextCue() .then(() => { this.emit(new TestRunFinished(new ExecutionSuccessful(), this.serenity.currentTime())); }) .catch(error => { this.emit(new TestRunFinished(new 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 ScenarioDetails(new Name(Tags.stripFrom(name)), new Category(Tags.stripFrom(featureName)), FileSystemLocation.fromJSON(spec.location)), scenarioTags: Tags.from(`${featureName} ${name}`) }; } /** * @private * @param {SuiteResult} result * @returns {TestSuiteDetails} */ testSuiteDetailsOf(result) { return new TestSuiteDetails(new Name(result.description), FileSystemLocation.fromJSON(result.location), new CorrelationId(result.id)); } /** * @private * @returns {string} */ currentFeatureNameFor(spec) { const path = new 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 ImplementationPending(new ImplementationPendingError(result.pendingReason || '')); case 'excluded': return new ExecutionSkipped(); case 'passed': // eslint-disable-line unicorn/no-useless-switch-case default: return new ExecutionSuccessful(); } } /** * @private * @param {Expectation} failure * @returns {ProblemIndication} */ failureOutcomeFrom(failure) { const error = this.errorFrom(failure); if (error instanceof AssertionError) { // sadly, Jasmine error propagation mechanism is rather basic // and unable to serialise the expected/actual properties of the AssertionError object return new ExecutionFailedWithAssertionError(error); } if (error instanceof TestCompromisedError) { return new ExecutionCompromised(error); } if (failure.matcherName) { // the presence of a non-empty matcherName property indicates a Jasmine-specific assertion error return new ExecutionFailedWithAssertionError(this.serenity.createError(AssertionError, { message: failure.message, diff: { expected: failure.expected, actual: failure.actual, }, cause: error, })); } return new ExecutionFailedWithError(error); } errorFrom(failure) { if (this.containsCorrectlySerialisedError(failure)) { return ErrorSerialiser.deserialiseFromStackTrace(failure.stack); } if (this.containsIncorrectlySerialisedErrorWithErrorPropertiesInStack(failure)) { return ErrorSerialiser.deserialiseFromStackTrace(this.repairedStackTraceOf(failure)); } if (this.containsIncorrectlySerialisedError(failure)) { return 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'); } } //# sourceMappingURL=SerenityReporterForJasmine.js.map