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