@serenity-js/core
Version:
The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure
248 lines • 8.65 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorFactory = void 0;
const node_util_1 = require("node:util");
const diff_1 = require("diff");
const objects_1 = require("tiny-types/lib/objects");
const io_1 = require("../io");
const Unanswered_1 = require("../screenplay/questions/Unanswered");
const AnsiDiffFormatter_1 = require("./diff/AnsiDiffFormatter");
/**
* Generates Serenity/JS [`RuntimeError`](https://serenity-js.org/api/core/class/RuntimeError/) objects based
* on the provided [configuration](https://serenity-js.org/api/core/interface/ErrorOptions/).
*
* @group Errors
*/
class ErrorFactory {
formatter;
constructor(formatter = new AnsiDiffFormatter_1.AnsiDiffFormatter()) {
this.formatter = formatter;
}
create(errorType, options) {
const message = [
this.title(options.message),
options.expectation && `\nExpectation: ${options.expectation}`,
options.diff && ('\n' + this.diffFrom(options.diff)),
options.location && (` at ${options.location.path.value}:${options.location.line}:${options.location.column}`),
].
filter(Boolean).
join('\n');
return new errorType(message, options?.cause);
}
title(value) {
return String(value).trim();
}
diffFrom(diff) {
return new Diff(diff.expected, diff.actual)
.lines()
.map(line => line.decorated(this.formatter))
.join('\n');
}
}
exports.ErrorFactory = ErrorFactory;
class DiffLine {
type;
value;
static markers = {
'expected': '- ',
'received': '+ ',
'unchanged': ' ',
};
static empty = () => new DiffLine('unchanged', '');
static unchanged = (line) => new DiffLine('unchanged', String(line));
static expected = (line) => new DiffLine('expected', String(line));
static received = (line) => new DiffLine('received', String(line));
static changed = (change, line) => {
if (change.removed) {
return this.expected(line);
}
if (change.added) {
return this.received(line);
}
return this.unchanged(line);
};
constructor(type, value) {
this.type = type;
this.value = value;
}
prependMarker() {
return this.prepend(this.marker());
}
appendMarker() {
return this.append(this.marker());
}
prepend(text) {
return new DiffLine(this.type, String(text) + this.value);
}
append(text) {
return new DiffLine(this.type, this.value + String(text));
}
decorated(decorator) {
return decorator[this.type](this.value);
}
marker() {
return DiffLine.markers[this.type];
}
}
class DiffValue {
value;
nameAndType;
summary;
changes;
desiredNameFieldLength;
constructor(name, value) {
this.value = value;
this.nameAndType = `${name} ${io_1.ValueInspector.typeOf(value)}`;
this.desiredNameFieldLength = this.nameAndType.length;
this.summary = this.summaryOf(value);
}
withDesiredFieldLength(columns) {
this.desiredNameFieldLength = columns;
return this;
}
hasSummary() {
return this.summary !== undefined;
}
type() {
return io_1.ValueInspector.typeOf(this.value);
}
isComplex() {
return (typeof this.value === 'object' || node_util_1.types.isProxy(this.value))
&& !(this.value instanceof RegExp)
&& !(this.value instanceof Unanswered_1.Unanswered);
}
isArray() {
return Array.isArray(this.value);
}
isComparableAsJson() {
if (!this.value || this.value instanceof Unanswered_1.Unanswered) {
return false;
}
return io_1.ValueInspector.isPlainObject(this.value)
|| this.value['toJSON'];
}
toString() {
const labelWidth = this.desiredNameFieldLength - this.nameAndType.length;
return [
this.nameAndType,
this.summary && ': '.padEnd(labelWidth + 2),
this.summary,
this.changes && this.changes.padStart(labelWidth + 5),
].
filter(Boolean).
join('');
}
summaryOf(value) {
if (value instanceof Date) {
return value.toISOString();
}
const isDefined = value !== undefined && value !== null;
if (isDefined && (io_1.ValueInspector.isPrimitive(value) || value instanceof RegExp)) {
return String(value);
}
return undefined;
}
}
class Diff {
diff;
constructor(expectedValue, actualValue) {
this.diff = this.diffFrom(expectedValue, actualValue);
}
diffFrom(expectedValue, actualValue) {
const { expected, actual } = this.aligned(new DiffValue('Expected', expectedValue), new DiffValue('Received', actualValue));
if (this.shouldRenderActualValueOnly(expected, actual)) {
return this.renderActualValue(expected, actual);
}
if (this.shouldRenderJsonDiff(expected, actual)) {
return this.renderJsonDiff(expected, actual);
}
if (this.shouldRenderArrayDiff(expected, actual)) {
return this.renderArrayDiff(expected, actual);
}
return [
DiffLine.expected(expected),
DiffLine.received(actual),
DiffLine.empty(),
];
}
shouldRenderActualValueOnly(expected, actual) {
return actual.isComplex()
&& !actual.hasSummary()
&& expected.type() !== actual.type();
}
shouldRenderJsonDiff(expected, actual) {
return expected.isComparableAsJson()
&& actual.isComparableAsJson()
&& !expected.hasSummary()
&& !actual.hasSummary();
}
shouldRenderArrayDiff(expected, actual) {
return expected.isArray()
&& actual.isArray();
}
aligned(expected, actual) {
const maxFieldLength = Math.max(expected.desiredNameFieldLength, actual.desiredNameFieldLength);
return {
expected: expected.withDesiredFieldLength(maxFieldLength),
actual: actual.withDesiredFieldLength(maxFieldLength)
};
}
renderActualValue(expected, actual) {
const lines = (0, io_1.inspected)(actual.value)
.split('\n')
.map(DiffLine.unchanged);
return [
DiffLine.expected(expected),
DiffLine.received(actual),
DiffLine.empty(),
...lines,
DiffLine.empty(),
];
}
renderJsonDiff(expected, actual) {
const changes = (0, diff_1.diffJson)(expected.value, actual.value);
const lines = changes.reduce((acc, change) => {
const changedLines = change.value.trimEnd().split('\n');
return acc.concat(changedLines.map(line => DiffLine.changed(change, line).prependMarker()));
}, []);
const { added, removed } = this.countOf(changes);
return [
DiffLine.expected(expected).append(' ').appendMarker().append(`${removed}`),
DiffLine.received(actual).append(' ').appendMarker().append(`${added}`),
DiffLine.empty(),
...lines,
DiffLine.empty(),
];
}
renderArrayDiff(expected, actual) {
const changes = (0, diff_1.diffArrays)(expected.value, actual.value, { comparator: objects_1.equal });
const lines = changes.reduce((acc, change) => {
const items = change.value;
return acc.concat(items.map(item => DiffLine.changed(change, (0, io_1.inspected)(item, { compact: true }))
.prepend(' ')
.prependMarker()));
}, []);
const { added, removed } = this.countOf(changes);
return [
DiffLine.expected(expected).append(' ').appendMarker().append(`${removed}`),
DiffLine.received(actual).append(' ').appendMarker().append(`${added}`),
DiffLine.empty(),
DiffLine.unchanged(' ['),
...lines,
DiffLine.unchanged(' ]'),
DiffLine.empty(),
];
}
countOf(changes) {
return changes.reduce(({ removed, added }, change) => {
return {
removed: removed + (change.removed ? change.count : 0),
added: added + (change.added ? change.count : 0),
};
}, { removed: 0, added: 0 });
}
lines() {
return this.diff;
}
}
//# sourceMappingURL=ErrorFactory.js.map