WIP - with lots of notes

This commit is contained in:
Shannon
2020-07-10 08:44:43 +10:00
parent f4e31e1dba
commit 0722adba2f
11 changed files with 286 additions and 167 deletions

View File

@@ -69,6 +69,18 @@
};
}
if (!String.prototype.trimStartSpecial) {
/** trimSpecial extension method for string */
// Removes all non printable chars from beginning of a string
String.prototype.trimStartSpecial = function () {
var index = 0;
while (this.charCodeAt(index) <= 46) {
index++;
}
return this.substr(index);
};
}
if (!String.prototype.startsWith) {
/** startsWith extension method for string */
String.prototype.startsWith = function (str) {

View File

@@ -158,33 +158,33 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
if (hadError) {
scope.$evalAsync(function () {
// TODO: This does not work :( :( :(
// We cannot clear a val-property-msg because another nested child might have server validation errors too.
// I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well
// as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done?
//// TODO: This does not work :( :( :(
//// We cannot clear a val-property-msg because another nested child might have server validation errors too.
//// I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well
//// as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done?
// Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key.
// we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error.
var currUmbProperty = umbPropCtrl;
var parentValidationKey = currUmbProperty.getParentValidationPath();
while (currUmbProperty && parentValidationKey) {
//// Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key.
//// we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error.
//var currUmbProperty = umbPropCtrl;
//var parentValidationKey = currUmbProperty.getParentValidationPath();
//while (currUmbProperty && parentValidationKey) {
if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) {
serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment);
// if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) {
// serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment);
// re-assign and loop
if (currUmbProperty !== umbPropCtrl.parentUmbProperty) {
currUmbProperty = umbPropCtrl.parentUmbProperty;
parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null;
}
else {
break;
}
}
else {
break;
}
}
// // re-assign and loop
// if (currUmbProperty !== umbPropCtrl.parentUmbProperty) {
// currUmbProperty = umbPropCtrl.parentUmbProperty;
// parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null;
// }
// else {
// break;
// }
// }
// else {
// break;
// }
//}
//// we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive
//// of some sort but that would also get messy. This works though since in this case we're always going to be in the property
@@ -205,19 +205,43 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
// serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment);
// }
//}
});
}
}
}
//function checkFormValidation(f) {
// if (!angularHelper.isForm(f)) {
// throw "The object is not an angular Form";
// }
// // if there's no value, the controls can be reset, which clears the error state on formCtrl
// for (let control of formCtrl.$getControls()) {
// control.$setValidity();
// }
//}
function checkValidationStatus() {
if (formCtrl.$invalid) {
//first we need to check if the valPropertyMsg validity is invalid
if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) {
//since we already have an error we'll just return since this means we've already set the
// hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
// TODO: This does not work! we cannot just clear if there are no errors in child controls because child controls
// won't even have been loaded yet so this will just instantly clear them
//// At this stage we might have an error assigned because it was assigned based on validation hierarchy from the server,
//// BUT one or ALL of the child server (and client) validations may be cleared at this point. We will know if we have an
//// explicitly assigned error based on the error message assigned, if it is a non-explicit error (meaning that the error
//// was assigned because it has child errors) then the message will just be: labels.propertyHasErrors
//if (scope.errorMsg === labels.propertyHasErrors && _.every(formCtrl.$getControls(), c => c.$valid)) {
// resetError();
//}
return;
}
//if there are any errors in the current property form that are not valPropertyMsg
@@ -295,7 +319,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
if (scope.currentProperty) { //this can be null if no property was assigned, TODO: I don't believe it can? If it was null we'd get errors above
if (scope.currentProperty) { //this can be null if no property was assigned
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
hasError = !isValid;
@@ -311,13 +335,13 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
}
}
unsubscribe.push(serverValidationManager.subscribe(propertyValidationKey,
unsubscribe.push(serverValidationManager.subscribe(
propertyValidationKey,
currentCulture,
"",
serverValidationManagerCallback,
currentSegment
)
);
));
}
//when the scope is disposed we need to unsubscribe

View File

@@ -55,11 +55,8 @@ function valServer(serverValidationManager) {
}
}
function getPropertyValidationKey() {
// Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works
var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : null;
return propertyValidationPath ? propertyValidationPath : currentProperty.alias;
}
// Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works
var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : currentProperty.alias;
// Need to watch the value model for it to change, previously we had subscribed to
// modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
@@ -81,10 +78,10 @@ function valServer(serverValidationManager) {
if (modelCtrl.$invalid) {
modelCtrl.$setValidity('valServer', true);
console.log("valServer cleared (watch)");
console.log("valServer cleared (watch) " + propertyValidationPath);
//clear the server validation entry
serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment);
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment);
stopWatch();
}
}, true);
@@ -102,14 +99,14 @@ function valServer(serverValidationManager) {
function serverValidationManagerCallback(isValid, propertyErrors, allErrors) {
if (!isValid) {
modelCtrl.$setValidity('valServer', false);
console.log("valServer error");
console.log("valServer error " + propertyValidationPath);
//assign an error msg property to the current validator
modelCtrl.errorMsg = propertyErrors[0].errorMsg;
startWatch();
}
else {
modelCtrl.$setValidity('valServer', true);
console.log("valServer cleared");
console.log("valServer cleared " + propertyValidationPath);
//reset the error message
modelCtrl.errorMsg = "";
stopWatch();
@@ -119,7 +116,7 @@ function valServer(serverValidationManager) {
unsubscribe.push(serverValidationManager.subscribe(
getPropertyValidationKey(),
propertyValidationPath,
currentCulture,
fieldName,
serverValidationManagerCallback,

View File

@@ -7,6 +7,9 @@
* Some angular helper/extension methods
*/
function angularHelper($q) {
var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"];
return {
/**
@@ -100,6 +103,28 @@ function angularHelper($q) {
}
},
isForm: function (obj) {
// a method to check that the collection of object prop names contains the property name expected
function allPropertiesExist(objectPropNames) {
//ensure that every required property name exists on the current object
return _.every(requiredFormProps, function (item) {
return _.contains(objectPropNames, item);
});
}
//get the keys of the property names for the current object
var props = _.keys(obj);
//if the length isn't correct, try the next prop
if (props.length < requiredFormProps.length) {
return false;
}
//ensure that every required property name exists on the current scope property
return allPropertiesExist(props);
},
/**
* @ngdoc function
* @name getCurrentForm
@@ -121,31 +146,10 @@ function angularHelper($q) {
// is to inject the $element object and use: $element.inheritedData('$formController');
var form = null;
var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"];
// a method to check that the collection of object prop names contains the property name expected
function propertyExists(objectPropNames) {
//ensure that every required property name exists on the current scope property
return _.every(requiredFormProps, function (item) {
return _.contains(objectPropNames, item);
});
}
for (var p in scope) {
if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") {
//get the keys of the property names for the current property
var props = _.keys(scope[p]);
//if the length isn't correct, try the next prop
if (props.length < requiredFormProps.length) {
continue;
}
//ensure that every required property name exists on the current scope property
var containProperty = propertyExists(props);
if (containProperty) {
if (this.isForm(scope[p])) {
form = scope[p];
break;
}

View File

@@ -614,6 +614,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
formHelper.handleServerValidation(args.err.data.ModelState);
//add model state errors to notifications
// TODO: Need to ignore complex messages
if (args.showNotifications) {
for (var e in args.err.data.ModelState) {
notificationsService.error("Validation", args.err.data.ModelState[e][0]);

View File

@@ -54,8 +54,13 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
}
}
//reset the server validations
serverValidationManager.reset();
//reset the server validations if required (default is true), otherwise notify existing ones of changes
if (!args.keepServerValidation) {
serverValidationManager.reset();
}
else {
serverValidationManager.notify();
}
return true;
},

View File

@@ -10,9 +10,29 @@
*/
function serverValidationManager($timeout, udiService) {
// TODO: It would be nicer to just SHA1 hash these 'keys' instead of having the keys be compound values
// It would be another lib dependency or we could embed this https://github.com/emn178/js-sha1
// this would remove the need for the auto-generated 'id' values since the hash would be the actual id value.
// The array of callback objects, each object 'key' is:
// - propertyAlias
// - culture
// - fieldName
// - segment
// The object also contains:
// - callback (function)
// - id (unique identifier, auto-generated, used internally for unsubscribing the callback)
var callbacks = [];
// The array of error messages
// The array of error message objects, each object 'key' is:
// - propertyAlias
// - culture
// - fieldName
// - segment
// The object also contains:
// - errorMsg
// - id (unique identifier, auto-generated, used internally for mapping parent/child property validation messages)
// - parentId (used to map parent/child property validation messages)
var items = [];
/** calls the callback specified with the errors specified, used internally */
@@ -41,6 +61,13 @@ function serverValidationManager($timeout, udiService) {
*/
function notify() {
$timeout(function () {
console.log(`VAL-ERROR-COUNT: ${items.length}`);
for (var i = 0; i < items.length; i++) {
var item = items[i];
console.log(`VAL-ERROR [${item.propertyAlias}] [${item.culture}] [${item.fieldName}] [${item.segment}]`)
}
notifyCallbacks();
});
}
@@ -76,7 +103,14 @@ function serverValidationManager($timeout, udiService) {
return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
}
function getPropertyErrorById(id) {
//find all errors for this id
return _.find(items, function (item) {
return (item.id === id);
});
}
function getVariantErrors(culture, segment) {
if (!culture) {
@@ -118,35 +152,47 @@ function serverValidationManager($timeout, udiService) {
}
/**
* Returns a dictionary of id (of the block) and it's corresponding validation ModelState
* Flattens the complex errror result json into an array of the block's id/parent id and it's corresponding validation ModelState
* @param {any} errorMsg
* @param {any} the id of the parentId (if any)
*/
function parseComplexEditorError(errorMsg) {
function parseComplexEditorError(errorMsg, parentId) {
var json = JSON.parse(errorMsg);
var json = Utilities.isArray(errorMsg) ? errorMsg : JSON.parse(errorMsg);
var result = {};
var result = [];
function extractModelState(validation) {
function extractModelState(validation, pid) {
if (validation.$id && validation.ModelState) {
result[validation.$id] = validation.ModelState;
var ms = {
id: validation.$id,
elementUdi: udiService.build("element", validation.$id),
parentId: pid,
modelState: validation.ModelState
};
result.push(ms);
return ms;
}
return null;
}
function iterateErrorBlocks(blocks) {
function iterateErrorBlocks(blocks, pid) {
for (var i = 0; i < blocks.length; i++) {
var validation = blocks[i];
extractModelState(validation);
var ms = extractModelState(validation, pid);
if (!ms) {
continue;
}
var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState");
for (const [key, value] of Object.entries(nested)) {
if (Array.isArray(value)) {
iterateErrorBlocks(value); // recurse
iterateErrorBlocks(value, ms.id); // recurse
}
}
}
}
iterateErrorBlocks(json);
iterateErrorBlocks(json, parentId);
return result;
}
@@ -258,7 +304,7 @@ function serverValidationManager($timeout, udiService) {
* @description
* Adds an error message for the content property
*/
function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) {
function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment, parentId) {
if (!propertyAlias) {
return;
@@ -277,14 +323,19 @@ function serverValidationManager($timeout, udiService) {
errorMsg = "";
}
// if the error message is json it's a complex editor validation response that we need to parse
if (errorMsg.startsWith("[")) {
var id = String.CreateGuid();
var idsToErrors = parseComplexEditorError(errorMsg);
for (const [key, value] of Object.entries(idsToErrors)) {
const elementUdi = udiService.build("element", key);
addErrorsForModelState(value, elementUdi);
}
// remove all non printable chars and whitespace from the string
if (Utilities.isString(errorMsg)) {
errorMsg = errorMsg.trimStartSpecial().trim();
}
// if the error message is json it's a complex editor validation response that we need to parse
if ((Utilities.isString(errorMsg) && errorMsg.startsWith("[")) || Utilities.isArray(errorMsg)) {
var idsToErrors = parseComplexEditorError(errorMsg, id);
console.log("idsToErrors = " + JSON.stringify(idsToErrors));
idsToErrors.forEach(x => addErrorsForModelState(x.modelState, x.elementUdi, x.parentId));
// We need to clear the error message else it will show up as a giant json block against the property
errorMsg = "";
@@ -293,6 +344,8 @@ function serverValidationManager($timeout, udiService) {
//only add the item if it doesn't exist
if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) {
items.push({
id: id,
parentId: parentId,
propertyAlias: propertyAlias,
culture: culture,
segment: segment,
@@ -368,10 +421,16 @@ function serverValidationManager($timeout, udiService) {
* @methodOf umbraco.services.serverValidationManager
* @param {any} modelState
* @param {any} elementUdi optional parameter specifying a nested element's UDI for which this property belongs (for complex editors)
* @param {any} parentId optional parameter specifying the parentId validation item object (for complex editors)
* @description
* This wires up all of the server validation model state so that valServer and valServerField directives work
*/
function addErrorsForModelState(modelState, elementUdi) {
function addErrorsForModelState(modelState, elementUdi, parentId) {
if (!Utilities.isObject(modelState)) {
throw "modelState is not an object";
}
for (const [key, value] of Object.entries(modelState)) {
//This is where things get interesting....
@@ -437,7 +496,7 @@ function serverValidationManager($timeout, udiService) {
}
// add a generic error for the property
addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment);
addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment, parentId);
}
else {
@@ -541,6 +600,8 @@ function serverValidationManager($timeout, udiService) {
return;
}
console.log(`serverValidationMgr subscribed [${propertyAlias}] [${culture}] [${fieldName}] [${segment}]`);
var id = String.CreateGuid();
//normalize culture to "invariant"
@@ -700,23 +761,15 @@ function serverValidationManager($timeout, udiService) {
* Gets the error message for the content property
*/
getPropertyError: function (propertyAlias, culture, fieldName, segment) {
//normalize culture to "invariant"
if (!culture) {
culture = "invariant";
var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName);
if (errors.length > 0) { // should only ever contain one
return errors[0];
}
//normalize segment to null
if (!segment) {
segment = null;
}
var err = _.find(items, function (item) {
//return true if the property alias matches and if an empty field name is specified or the field name matches
return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
return err;
return undefined;
},
getPropertyErrorById: getPropertyErrorById,
/**
* @ngdoc function
* @name getFieldError

View File

@@ -56,7 +56,8 @@ angular.module("umbraco")
vm.submitAndClose = function () {
if (vm.model && vm.model.submit) {
if (formHelper.submitForm({ scope: $scope })) {
// always keep server validations since this will be a nested editor and server validations are global
if (formHelper.submitForm({ scope: $scope, formCtrl: vm.blockForm, keepServerValidation: true })) {
vm.model.submit(vm.model);
}
}

View File

@@ -1,8 +1,11 @@
<div class="umb-block-editor" ng-controller="Umbraco.Editors.BlockEditorController as vm">
<ng-form name="blockForm" val-form-manager>
<ng-form name="vm.blockForm" val-form-manager>
<umb-editor-view umb-tabs ng-if="!page.loading">
<pre style="background-color:aquamarine">{{vm.blockForm.$valid}}</pre>
<umb-editor-header
name="vm.model.title"
name-locked="true"

View File

@@ -5,6 +5,7 @@
<pre>{{ vm.getValidationPath() }}</pre>
<!-- TODO: Should we disable watching complex values if we know this is a complex editor? -->
<val-property-msg></val-property-msg>
<div class="umb-el-wrap">

View File

@@ -4,7 +4,8 @@
beforeEach(module('umbraco.services'));
beforeEach(inject(function ($injector) {
serverValidationManager = $injector.get('serverValidationManager');
serverValidationManager = $injector.get('serverValidationManager');
serverValidationManager.clear();
}));
describe('managing field validation errors', function () {
@@ -316,6 +317,51 @@
describe('managing complex editor validation errors', function () {
// this root element doesn't have it's own attached errors, instead it has model state just
// showing that it has errors within it's nested properties. that ModelState is automatically
// added on the server side.
var nonRootLevelComplexValidationMsg = `[
{
"$elementTypeAlias": "addressBook",
"$id": "34E3A26C-103D-4A05-AB9D-7E14032309C3",
"addresses":
[
{
"$elementTypeAlias": "addressInfo",
"$id": "FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1",
"ModelState":
{
"_Properties.city.invariant.null.country": [
"City is not in Australia"
],
"_Properties.city.invariant.null.capital": [
"Not a capital city"
]
}
},
{
"$elementTypeAlias": "addressInfo",
"$id": "7170A4DD-2441-4B1B-A8D3-437D75C4CBC9",
"ModelState":
{
"_Properties.city.invariant.null.country": [
"City is not in Australia"
],
"_Properties.city.invariant.null.capital": [
"Not a capital city"
]
}
}
],
"ModelState":
{
"_Properties.addresses.invariant.null": [
""
]
}
}
]`;
it('create dictionary of id to ModelState', function () {
//arrange
@@ -365,80 +411,52 @@
]`;
//act
var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg);
var ms = serverValidationManager.parseComplexEditorError(complexValidationMsg);
//assert
var keys = Object.keys(ids);
expect(keys.length).toEqual(3);
expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3");
expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1");
expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9");
expect(ms.length).toEqual(3);
expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3");
expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1");
expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9");
});
it('create dictionary of id to ModelState with inherited errors', function () {
// arrange
// this root element doesn't have it's own attached errors, instead it has model state just
// showing that it has errors within it's nested properties. that ModelState is automatically
// added on the server side.
var complexValidationMsg = `[
{
"$elementTypeAlias": "addressBook",
"$id": "34E3A26C-103D-4A05-AB9D-7E14032309C3",
"addresses":
[
{
"$elementTypeAlias": "addressInfo",
"$id": "FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1",
"ModelState":
{
"_Properties.city.invariant.null.country": [
"City is not in Australia"
],
"_Properties.city.invariant.null.capital": [
"Not a capital city"
]
}
},
{
"$elementTypeAlias": "addressInfo",
"$id": "7170A4DD-2441-4B1B-A8D3-437D75C4CBC9",
"ModelState":
{
"_Properties.city.invariant.null.country": [
"City is not in Australia"
],
"_Properties.city.invariant.null.capital": [
"Not a capital city"
]
}
}
],
"ModelState":
{
"_Properties.addresses.invariant.null": [
""
]
}
}
]`;
//act
var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg);
var ms = serverValidationManager.parseComplexEditorError(nonRootLevelComplexValidationMsg);
//assert
var keys = Object.keys(ids);
expect(keys.length).toEqual(3);
expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3");
var item0ModelState = ids["34E3A26C-103D-4A05-AB9D-7E14032309C3"];
expect(ms.length).toEqual(3);
expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3");
var item0ModelState = ms[0].modelState;
expect(Object.keys(item0ModelState).length).toEqual(1);
expect(item0ModelState["_Properties.addresses.invariant.null"].length).toEqual(1);
expect(item0ModelState["_Properties.addresses.invariant.null"][0]).toEqual("");
expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1");
expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9");
expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1");
expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9");
});
it('add errors for ModelState with inherited errors', function () {
//act
let modelState = {
"_Properties.blockFeatures.invariant.null": [
nonRootLevelComplexValidationMsg
]
};
serverValidationManager.addErrorsForModelState(modelState);
//assert
console.log(JSON.stringify(serverValidationManager.items));
let propError = serverValidationManager.getPropertyError("umb://element/FBEAEE8F4BC943EE8B81FCA8978850F1/city");
expect(propError).toBeDefined();
console.log(JSON.stringify(propError));
expect(propError.parentId).toBeDefined();
let parentError = serverValidationManager.getPropertyErrorById(propError.parentId);
expect(parentError).toBeDefined();
expect(parentError.propertyAlias).toEqual("umb://element/34E3A26C103D4A05AB9D7E14032309C3/addresses");
});