Skip to content

Commit

Permalink
fix(validation): Improve annotations and showValidation error handling
Browse files Browse the repository at this point in the history
- Addressed issue in `EML211EditorView.showValidation` to correctly display nested validation errors.
- Enhanced `EML211.validate` to handle annotation validation more robustly, preventing null errors.
- Added unit tests to cover the changes in `EML211EditorView.showValidation`.

Fixes #2606
  • Loading branch information
vchendrix committed Feb 5, 2025
1 parent 1423787 commit a5bfefa
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 65 deletions.
4 changes: 2 additions & 2 deletions src/js/models/metadata/eml211/EML211.js
Original file line number Diff line number Diff line change
Expand Up @@ -1709,9 +1709,9 @@ define([
}
}

// Validate the EMLAnnotation models
// Validate the EMLAnnotation models, checking for the canonical dataset
const annotations = this.get("annotations");
const annotationErrors = annotations.validate();
const annotationErrors = annotations ? annotations.validate() : [];

if (annotationErrors?.length) {
errors.annotations = annotationErrors.filter(
Expand Down
153 changes: 91 additions & 62 deletions src/js/views/metadata/EML211EditorView.js
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,10 @@ define([
}
},

/** Show any errors that occured when trying to save changes */
/**
* Show any errors that occurred when trying to save changes
*
*/
showValidation() {
// First clear all the error messaging
this.$(".notification.error").empty();
Expand All @@ -1228,75 +1231,101 @@ define([

const errors = this.model.validationError;

errors?.forEach((errorMsg, category) => {
const categoryEls = this.$(`[data-category='${category}']`);
const dataItemRow = categoryEls.parents(".data-package-item");
if (errors && typeof errors === "object") {
Object.entries(errors).forEach(([category, errorMsg]) => {
if (typeof errorMsg === "string") {
// Handle string error messages
this.showError(category, errorMsg);
} else if (typeof errorMsg === "object") {
// Handle object error messages by iterating over leaf nodes
this.showLeafErrors(category, errorMsg);
}
});

// If this field is in a DataItemView, then delegate to that view
if (dataItemRow.length && dataItemRow.data("view")) {
dataItemRow.data("view").showValidation(category, errorMsg);
return;
}
const elsWithViews = categoryEls.filter(
(el) =>
$(el).data("view") &&
$(el).data("view").showValidation &&
!$(el).data("view").isNew,
);
if (Object.keys(errors).length) {
// Create a list of errors to display in the error message shown to the user
const errorList = `<ul>${this.getErrorListItem(errors)}</ul>`;

if (elsWithViews.length) {
elsWithViews.forEach((el) => {
$(el).data("view").showValidation();
});
} else if (categoryEls.length) {
// Show the error message
categoryEls
.filter(".notification")
.addClass("error")
.text(errorMsg);

// Add the error message to inputs
categoryEls.filter("textarea, input").addClass("error");
MetacatUI.appView.showAlert(
`Fix the errors flagged below before submitting: ${errorList}`,
"alert-error",
this.$el,
null,
{
remove: true,
},
);
}
}
},

// Get the link in the table of contents navigation
let navigationLink = this.$(
`.side-nav-item[data-category='${category}']`,
);
/**
* Log an error message for a specific category
*
* @param category - The category of the error
* @param errorMsg - The error message to display
*/
showError(category, errorMsg) {
const categoryEls = this.$(`[data-category='${category}']`);
const dataItemRow = categoryEls.parents(".data-package-item");

if (!navigationLink.length) {
const section = categoryEls.parents("[data-section]");
navigationLink = this.$(
`.side-nav-item.${$(section).attr("data-section")}`,
);
}
// If this field is in a DataItemView, then delegate to that view
if (dataItemRow.length && dataItemRow.data("view")) {
dataItemRow.data("view").showValidation(category, errorMsg);
return;
}
const elsWithViews = categoryEls.filter(
(el) =>
$(el).data("view") &&
$(el).data("view").showValidation &&
!$(el).data("view").isNew,
);

// Show the error icon in the table of contents
navigationLink
.addClass("error")
.find(".icon")
.addClass("error")
.show();

this.model.off(`change:${category}`, this.model.checkValidity);
this.model.once(`change:${category}`, this.model.checkValidity);
}, this);

if (errors) {
// Create a list of errors to display in the error message shown to
// the user
const errorList = `<ul>${this.getErrorListItem(errors)}</ul>`;

MetacatUI.appView.showAlert(
`Fix the errors flagged below before submitting: ${errorList}`,
"alert-error",
this.$el,
null,
{
remove: true,
},
if (elsWithViews.length) {
elsWithViews.forEach((el) => {
$(el).data("view").showValidation();
});
} else if (categoryEls.length) {
// Show the error message
categoryEls.filter(".notification").addClass("error").text(errorMsg);

// Add the error message to inputs
categoryEls.filter("textarea, input").addClass("error");
}

// Get the link in the table of contents navigation
let navigationLink = this.$(
`.side-nav-item[data-category='${category}']`,
);

if (!navigationLink.length) {
const section = categoryEls.parents("[data-section]");
navigationLink = this.$(
`.side-nav-item.${$(section).attr("data-section")}`,
);
}

// Show the error icon in the table of contents
navigationLink.addClass("error").find(".icon").addClass("error").show();

this.model.off(`change:${category}`, this.model.checkValidity);
this.model.once(`change:${category}`, this.model.checkValidity);
},

/**
* Recursively log the leaf errors in the error object
*
* @param category - The category of the error
* @param errorObj - The object containing the error messages
*/
showLeafErrors(category, errorObj) {
Object.entries(errorObj).forEach(([subCategory, subErrorMsg]) => {
if (typeof subErrorMsg === "string") {
this.showError(`${category}`, subErrorMsg);
} else if (typeof subErrorMsg === "object") {
this.showLeafErrors(`${subCategory}`, subErrorMsg);
}
});
},

/** @inheritdoc */
Expand Down
3 changes: 2 additions & 1 deletion test/config/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
"./js/specs/unit/models/ontologies/BioontologyOntology.spec.js",
"./js/specs/unit/models/accordion/Accordion.spec.js",
"./js/specs/unit/models/accordion/AccordionItem.spec.js",
"./js/specs/unit/views/DataItemView.spec.js"
"./js/specs/unit/views/DataItemView.spec.js",
"./js/specs/unit/views/metadata/EML211EditorView.spec.js"
],
"integration": [
"./js/specs/integration/collections/SolrResults.spec.js",
Expand Down
94 changes: 94 additions & 0 deletions test/js/specs/unit/views/metadata/EML211EditorView.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
define(["jquery", "backbone", "views/metadata/EML211EditorView"], function (
$,
Backbone,
EML211EditorView,
) {
describe("EML211EditorView", function () {
let view, model, sandbox;

beforeEach(function () {
sandbox = sinon.createSandbox();
console.log("beforeEach called");
// Create a mock model with validation errors
model = new Backbone.Model();
model.validationError = {
title: "Error in title",
abstract: "Error in abstract",
methods: {
methodSteps: "Error in step 1",
},
};

// Instantiate the view with the mock model
view = new EML211EditorView({ model: model });

// Spy on the methods that interact with the DOM
sandbox.spy(view, "showError");
sandbox.spy(view, "showLeafErrors");
});

afterEach(function () {
sandbox.restore();
});

it("should log validation errors correctly", function () {
console.log("Test: should log validation errors correctly");
// Call the showValidation method
view.showValidation();

// Assert that showError and showLeafErrors were called with the expected arguments
sinon.assert.calledWith(view.showError, "methods", "Error in step 1");
sinon.assert.calledWith(view.showLeafErrors, "methods", {
methodSteps: "Error in step 1",
});
sinon.assert.calledWith(view.showError, "title", "Error in title");
sinon.assert.calledWith(view.showError, "abstract", "Error in abstract");
});

it("should handle string error messages correctly in showError", function () {
console.log(
"Test: should handle string error messages correctly in showError",
);
// Mock the category elements
view.$ = sandbox.stub().returns({
addClass: sandbox.stub().returnsThis(),
text: sandbox.stub(),
filter: sandbox.stub().returnsThis(),
parents: sandbox.stub().returnsThis(),
data: sandbox.stub().returnsThis(),
find: sandbox.stub().returnsThis(),
show: sandbox.stub(),
});

// Call the showError method
view.showError("methodSteps.step1", "Error in step 1");

// Assert that the appropriate DOM manipulation methods were called
sinon.assert.calledWith(view.$, "[data-category='methodSteps.step1']");
});

it("should handle nested error objects correctly in showLeafErrors", function () {
console.log(
"Test: should handle nested error objects correctly in showLeafErrors",
);

// Call the showLeafErrors method
view.showLeafErrors("methodSteps.step2", {
subStep1: "Error in sub-step 1",
subStep2: "Error in sub-step 2",
});

// Assert that showError was called with the expected arguments
sinon.assert.calledWith(
view.showError,
"methodSteps.step2",
"Error in sub-step 1",
);
sinon.assert.calledWith(
view.showError,
"methodSteps.step2",
"Error in sub-step 2",
);
});
});
});

0 comments on commit a5bfefa

Please sign in to comment.