diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js index 5100f7e758..a14e722179 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js @@ -316,6 +316,138 @@ hiddenFields[i].remove(); } } + + /** + * Returns an initializeEditDialog-compatible handler that validates min < max in real time. + * Works for both wrapperClass selectors (class on a wrapper div) and granite:class selectors + * (class on the coral field itself) by looking for a coral-numberinput or coral-datepicker + * inside the selector before falling back to the selector element itself. + * + * @param {String} minSelector Dialog-prefixed CSS selector for the min field or its wrapper + * @param {String} maxSelector Dialog-prefixed CSS selector for the max field or its wrapper + * @param {String} minMsg Error message shown on the min field when min > max + * @param {String} maxMsg Error message shown on the max field when min > max + * @param {Function} [compareFn] Optional (a, b) => boolean. Defaults to numeric comparison. + */ + static handleMinMaxValidation(minSelector, maxSelector, minMsg, maxMsg, compareFn) { + return function(dialog) { + function getCoralField(selector) { + var container = dialog.find(selector); + if (!container.length) return null; + var inner = container.find("coral-numberinput, coral-datepicker"); + return (inner.length ? inner : container)[0]; + } + var minField = getCoralField(minSelector); + var maxField = getCoralField(maxSelector); + if (!minField || !maxField) return; + var compare = compareFn || INT_COMPARE; + function validate() { + var minVal = minField.value, maxVal = maxField.value; + var invalid = !!(minVal && maxVal && compare(minVal, maxVal)); + var minErrMsg = Granite.I18n.getMessage(minMsg); + var maxErrMsg = Granite.I18n.getMessage(maxMsg); + if (invalid) { + minField.invalid = true; + maxField.invalid = true; + minField.errorMessage = minErrMsg; + maxField.errorMessage = maxErrMsg; + } else { + // Only clear invalid if this listener set it — avoids clobbering + // required/pattern errors that another validator placed on the field. + if (minField.errorMessage === minErrMsg) { + minField.invalid = false; + minField.errorMessage = ""; + } + if (maxField.errorMessage === maxErrMsg) { + maxField.invalid = false; + maxField.errorMessage = ""; + } + } + } + validate(); + minField.addEventListener("change", validate); + maxField.addEventListener("change", validate); + }; + } + + /** + * Registers a foundation.validation.validator that blocks dialog submission when min > max. + * Must be called once at module scope (not inside initializeEditDialog) to avoid stacking + * duplicate validators on every dialog open. + * + * @param {String} minFieldSelector Bare CSS selector targeting the coral field for min + * @param {String} maxFieldSelector Bare CSS selector targeting the coral field for max + * @param {String} minMsg Error message for the min field + * @param {String} maxMsg Error message for the max field + * @param {Function} [compareFn] Optional (a, b) => boolean. Defaults to numeric comparison. + */ + static registerMinMaxValidator(minFieldSelector, maxFieldSelector, minMsg, maxMsg, compareFn) { + var compare = compareFn || INT_COMPARE; + $(window).adaptTo("foundation-registry").register("foundation.validation.validator", { + selector: minFieldSelector + ", " + maxFieldSelector, + validate: function(el) { + var dialog = $(el).closest("coral-dialog"); + var minField = dialog.find(minFieldSelector)[0]; + var maxField = dialog.find(maxFieldSelector)[0]; + if (!minField || !maxField) return; + var minVal = minField.value, maxVal = maxField.value; + if (minVal && maxVal && compare(minVal, maxVal)) { + if (el === minField) { + return Granite.I18n.getMessage(minMsg); + } + return Granite.I18n.getMessage(maxMsg); + } + } + }); + } } + // ─── Shared min/max validation for container components ────────────────── + // panelcontainer__minOccur / __maxOccur is shared across accordion, wizard, + // tabsontop, verticaltabs, and fragment — none of which have their own + // editDialog.js — so registration is centralised here. + var INT_COMPARE = function(a, b) { return parseInt(a, 10) > parseInt(b, 10); }; + + var MIN_MAX_PAIRS = [ + { + minSelector: ".cmp-adaptiveform-panelcontainer__minOccur coral-numberinput", + maxSelector: ".cmp-adaptiveform-panelcontainer__maxOccur coral-numberinput", + minMsg: "Minimum occurrence cannot be greater than maximum occurrence", + maxMsg: "Maximum occurrence cannot be less than minimum occurrence", + compareFn: INT_COMPARE + } + ]; + + var Utils = window.CQ.FormsCoreComponents.Utils.v1; + + // Register all foundation submit-time validators once at page load. + MIN_MAX_PAIRS.forEach(function(pair) { + Utils.registerMinMaxValidator( + pair.minSelector, + pair.maxSelector, + pair.minMsg, + pair.maxMsg, + pair.compareFn + ); + }); + + // Auto-wire real-time change listeners whenever any dialog opens. + channel.on("foundation-contentloaded", function(e) { + if (!$(e.target).find(".cq-dialog-content").length) return; + Coral.commons.ready(e.target, function() { + var dialog = $(e.target); + MIN_MAX_PAIRS.forEach(function(pair) { + if (dialog.find(pair.minSelector).length && dialog.find(pair.maxSelector).length) { + Utils.handleMinMaxValidation( + pair.minSelector, + pair.maxSelector, + pair.minMsg, + pair.maxMsg, + pair.compareFn + )(dialog); + } + }); + }); + }); + })(jQuery, jQuery(document), Coral); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js index 9e040385d9..d4c4598169 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js @@ -26,6 +26,10 @@ DATEPICKER_DEFAULTDATE = EDIT_DIALOG + " .cmp-adaptiveform-datepicker__defaultdate", DATEPICKER_MINDATE = EDIT_DIALOG + " .cmp-adaptiveform-datepicker__mindate", DATEPICKER_MAXDATE = EDIT_DIALOG + " .cmp-adaptiveform-datepicker__maxdate", + DATEPICKER_MIN_FIELD = ".cmp-adaptiveform-datepicker__mindate coral-datepicker", + DATEPICKER_MAX_FIELD = ".cmp-adaptiveform-datepicker__maxdate coral-datepicker", + DATEPICKER_MIN_MSG = "Minimum date cannot be after maximum date", + DATEPICKER_MAX_MSG = "Maximum date cannot be before minimum date", Utils = window.CQ.FormsCoreComponents.Utils.v1; @@ -69,5 +73,28 @@ maxDateTooltip.innerHTML = fieldDescription; } - Utils.initializeEditDialog(EDIT_DIALOG)(handleDisplayPatternDropDown,handleDisplayFormat,handleEditPatternDropDown,handleEditFormat,handleLang,handleDatePlaceholders); + var DATE_COMPARE = function(a, b) { + var da = new Date(a), db = new Date(b); + return !isNaN(da) && !isNaN(db) && da > db; + }; + + Utils.registerMinMaxValidator( + DATEPICKER_MIN_FIELD, DATEPICKER_MAX_FIELD, + DATEPICKER_MIN_MSG, DATEPICKER_MAX_MSG, + DATE_COMPARE + ); + + Utils.initializeEditDialog(EDIT_DIALOG)( + handleDisplayPatternDropDown, + handleDisplayFormat, + handleEditPatternDropDown, + handleEditFormat, + handleLang, + handleDatePlaceholders, + Utils.handleMinMaxValidation( + DATEPICKER_MIN_FIELD, DATEPICKER_MAX_FIELD, + DATEPICKER_MIN_MSG, DATEPICKER_MAX_MSG, + DATE_COMPARE + ) + ); })(jQuery); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml index daabf7d72e..70413bd31d 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml @@ -77,6 +77,7 @@ type="datetime" emptyText="YYYY-MM-DD HH:mm" name="./minimumDateTime" + wrapperClass="cmp-adaptiveform-datetime__minimumDateTime" valueFormat="YYYY-MM-DD[T]HH:mm:ss.000-00:00"/> db; + }; + + Utils.registerMinMaxValidator( + DATETIME_MIN_FIELD, DATETIME_MAX_FIELD, + DATETIME_MIN_MSG, DATETIME_MAX_MSG, + DATE_COMPARE + ); + + Utils.initializeEditDialog(EDIT_DIALOG)( + Utils.handleMinMaxValidation( + DATETIME_MIN_FIELD, DATETIME_MAX_FIELD, + DATETIME_MIN_MSG, DATETIME_MAX_MSG, + DATE_COMPARE + ) + ); })(jQuery); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml index 37fc33697b..84bb4314a4 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml @@ -101,6 +101,7 @@ Number(b); }; + + Utils.registerMinMaxValidator( + NUMERICINPUT_MIN_FIELD, NUMERICINPUT_MAX_FIELD, + NUMERICINPUT_MIN_MSG, NUMERICINPUT_MAX_MSG, + NUMBER_COMPARE + ); + + Utils.initializeEditDialog(EDIT_DIALOG)( + handleDisplayPatternDropDown, + handleDisplayFormat, + handleLang, + handleTypeDropdown, + Utils.handleMinMaxValidation( + NUMERICINPUT_MIN_FIELD, NUMERICINPUT_MAX_FIELD, + NUMERICINPUT_MIN_MSG, NUMERICINPUT_MAX_MSG, + NUMBER_COMPARE + ) + ); })(jQuery); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js index 25312b21b8..cc5ed34239 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js @@ -20,6 +20,10 @@ TEXTINPUT_ALLOWRICHTEXT = EDIT_DIALOG + " .cmp-adaptiveform-textinput__allowrichtext", TEXTINPUT_MAXLENGTH = EDIT_DIALOG + " .cmp-adaptiveform-textinput__maxlength", TEXTINPUT_MINLENGTH = EDIT_DIALOG + " .cmp-adaptiveform-textinput__minlength", + TEXTINPUT_MIN_FIELD = ".cmp-adaptiveform-textinput__minlength coral-numberinput", + TEXTINPUT_MIN_MSG = "Minimum length cannot be greater than maximum length", + TEXTINPUT_MAX_MSG = "Maximum length cannot be less than minimum length", + TEXTINPUT_MAX_FIELD = ".cmp-adaptiveform-textinput__maxlength coral-numberinput", BASE_PLACEHOLDER = EDIT_DIALOG + " .cmp-adaptiveform-base__placeholder", TEXTINPUT_VALUE = EDIT_DIALOG + " .cmp-adaptiveform-textinput__value", TEXTINPUT_RICHTEXTVALUE = EDIT_DIALOG + " .cmp-adaptiveform-textinput__richtextvalue", @@ -76,6 +80,21 @@ patternComponent.addEventListener("change", updateDisplayValueExpression); } - Utils.initializeEditDialog(EDIT_DIALOG)(handleValidationPatternDropDown,handleValidationFormat,handleDisplayPatternDropDown,handleDisplayFormat,handleDisplayValueExpression); + Utils.registerMinMaxValidator( + TEXTINPUT_MIN_FIELD, TEXTINPUT_MAX_FIELD, + TEXTINPUT_MIN_MSG, TEXTINPUT_MAX_MSG + ); + + Utils.initializeEditDialog(EDIT_DIALOG)( + handleValidationPatternDropDown, + handleValidationFormat, + handleDisplayPatternDropDown, + handleDisplayFormat, + handleDisplayValueExpression, + Utils.handleMinMaxValidation( + TEXTINPUT_MIN_FIELD, TEXTINPUT_MAX_FIELD, + TEXTINPUT_MIN_MSG, TEXTINPUT_MAX_MSG + ) + ); })(jQuery); diff --git a/ui.tests/test-module/specs/datepicker/datepicker.minmax.authoring.cy.js b/ui.tests/test-module/specs/datepicker/datepicker.minmax.authoring.cy.js new file mode 100644 index 0000000000..70238b5e97 --- /dev/null +++ b/ui.tests/test-module/specs/datepicker/datepicker.minmax.authoring.cy.js @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright 2022 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +const afConstants = require("../../libs/commons/formsConstants"); +const sitesSelectors = require("../../libs/commons/sitesSelectors"); + +describe('Page - Authoring', function () { + const dropDatePickerInContainer = function () { + const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*", + responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; + cy.selectLayer("Edit"); + cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Date Picker", afConstants.components.forms.resourceType.datepicker); + cy.get('body').click(0, 0); + }; + + const setDatePickerValue = function (selector, isoDate) { + cy.get(selector).then(($el) => { + $el[0].value = isoDate; + $el[0].dispatchEvent(new Event('change', { bubbles: true })); + }); + }; + + context('Open Forms Editor', function () { + const pagePath = "/content/forms/af/core-components-it/blank", + datePickerEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/datepicker", + datePickerEditPathSelector = "[data-path='" + datePickerEditPath + "']", + datePickerDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.datepicker.split("/").pop(), + editDialogConfigurationSelector = "[data-action='CONFIGURE']", + minField = '.cmp-adaptiveform-datepicker__mindate coral-datepicker', + maxField = '.cmp-adaptiveform-datepicker__maxdate coral-datepicker'; + + beforeEach(function () { + cy.openAuthoring(pagePath); + }); + + it('shows inline error when minimum date is set after maximum date', function () { + dropDatePickerInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + datePickerEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + cy.get('.cmp-adaptiveform-datepicker__editdialog').contains('Validation').click(); + + // Set max = 2024-01-10, then min = 2024-01-20 (invalid: min after max) + setDatePickerValue(maxField, '2024-01-10'); + setDatePickerValue(minField, '2024-01-20'); + + // Both fields should be marked invalid + cy.get(minField).should('have.attr', 'invalid'); + cy.get(maxField).should('have.attr', 'invalid'); + + // Fix by setting min before max + setDatePickerValue(minField, '2024-01-05'); + + // Both fields should no longer be invalid + cy.get(minField).should('not.have.attr', 'invalid'); + cy.get(maxField).should('not.have.attr', 'invalid'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(datePickerDrop); + }); + + it('blocks dialog save when minimum date is after maximum date', function () { + dropDatePickerInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + datePickerEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + cy.get('.cmp-adaptiveform-datepicker__editdialog').contains('Validation').click(); + + // Set an invalid state: min after max + setDatePickerValue(maxField, '2024-01-10'); + setDatePickerValue(minField, '2024-01-20'); + + // Attempt to save — dialog should remain open + cy.get('.cq-dialog-submit').click(); + cy.get('coral-dialog[open]').should('exist'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(datePickerDrop); + }); + }); +}); diff --git a/ui.tests/test-module/specs/datetime/datetime.minmax.authoring.cy.js b/ui.tests/test-module/specs/datetime/datetime.minmax.authoring.cy.js new file mode 100644 index 0000000000..4a50b8f326 --- /dev/null +++ b/ui.tests/test-module/specs/datetime/datetime.minmax.authoring.cy.js @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright 2025 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +const afConstants = require("../../libs/commons/formsConstants"); +const sitesSelectors = require("../../libs/commons/sitesSelectors"); + +describe('Page - Authoring', function () { + const dropDateTimeInContainer = function () { + const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*", + responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; + cy.selectLayer("Edit"); + cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Date and Time", afConstants.components.forms.resourceType.datetime); + cy.get('body').click(0, 0); + }; + + const setDatePickerValue = function (selector, isoDate) { + cy.get(selector).then(($el) => { + $el[0].value = isoDate; + $el[0].dispatchEvent(new Event('change', { bubbles: true })); + }); + }; + + context('Open Forms Editor', function () { + const pagePath = "/content/forms/af/core-components-it/blank", + dateTimeEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/datetime", + dateTimeEditPathSelector = "[data-path='" + dateTimeEditPath + "']", + dateTimeDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.datetime.split("/").pop(), + editDialogConfigurationSelector = "[data-action='CONFIGURE']", + minField = '.cmp-adaptiveform-datetime__minimumDateTime coral-datepicker', + maxField = '.cmp-adaptiveform-datetime__maximumDateTime coral-datepicker'; + + beforeEach(function () { + cy.openAuthoring(pagePath); + }); + + it('shows inline error when minimum date-time is set after maximum date-time', function () { + dropDateTimeInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + dateTimeEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + + // min/max fields are on the Basic tab (open by default) + // Set max = 2024-01-10, then min = 2024-01-20 (invalid: min after max) + setDatePickerValue(maxField, '2024-01-10'); + setDatePickerValue(minField, '2024-01-20'); + + // Both fields should be marked invalid + cy.get(minField).should('have.attr', 'invalid'); + cy.get(maxField).should('have.attr', 'invalid'); + + // Fix by setting min before max + setDatePickerValue(minField, '2024-01-05'); + + // Both fields should no longer be invalid + cy.get(minField).should('not.have.attr', 'invalid'); + cy.get(maxField).should('not.have.attr', 'invalid'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(dateTimeDrop); + }); + + it('blocks dialog save when minimum date-time is after maximum date-time', function () { + dropDateTimeInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + dateTimeEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + + // Set an invalid state: min after max + setDatePickerValue(maxField, '2024-01-10'); + setDatePickerValue(minField, '2024-01-20'); + + // Attempt to save — dialog should remain open + cy.get('.cq-dialog-submit').click(); + cy.get('coral-dialog[open]').should('exist'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(dateTimeDrop); + }); + }); +}); diff --git a/ui.tests/test-module/specs/fileinput/fileinput.minmax.authoring.cy.js b/ui.tests/test-module/specs/fileinput/fileinput.minmax.authoring.cy.js new file mode 100644 index 0000000000..31ddcb3054 --- /dev/null +++ b/ui.tests/test-module/specs/fileinput/fileinput.minmax.authoring.cy.js @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright 2022 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +const afConstants = require("../../libs/commons/formsConstants"); +const sitesSelectors = require("../../libs/commons/sitesSelectors"); + +describe('Page - Authoring', function () { + const dropFileInputInContainer = function () { + const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*", + responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; + cy.selectLayer("Edit"); + cy.insertComponent(responsiveGridDropZoneSelector, "File Attachment", afConstants.components.forms.resourceType.formfileinput); + cy.get('body').click(0, 0); + }; + + const setNumberInputValue = function (selector, value) { + cy.get(selector).then(($el) => { + $el[0].value = value; + $el[0].dispatchEvent(new Event('change', { bubbles: true })); + }); + }; + + context('Open Forms Editor', function () { + const pagePath = "/content/forms/af/core-components-it/blank", + fileInputEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/fileinput", + fileInputEditPathSelector = "[data-path='" + fileInputEditPath + "']", + fileInputDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.formfileinput.split("/").pop(), + editDialogConfigurationSelector = "[data-action='CONFIGURE']", + minField = '.cmp-adaptiveform-fileinput__minimumFiles coral-numberinput', + maxField = '.cmp-adaptiveform-fileinput__maximumFiles coral-numberinput'; + + beforeEach(function () { + cy.openAuthoring(pagePath); + }); + + it('shows inline error when minimum files is set greater than maximum files', function () { + dropFileInputInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + fileInputEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + + // Enable multi-selection (Basic tab) to reveal min/max files fields on the Validation tab + cy.get("[name='./multiSelection'][type='checkbox']").should('exist').check({ force: true }); + // Navigate to Validation tab where min/max fields live + cy.get('.cmp-adaptiveform-fileinput__editdialog').contains('Validation').click(); + cy.get(minField).should('be.visible'); + + // Set maximum = 5, then minimum = 10 (invalid: min > max) + setNumberInputValue(maxField, 5); + setNumberInputValue(minField, 10); + + // Both fields should be marked invalid + cy.get(minField).should('have.attr', 'invalid'); + cy.get(maxField).should('have.attr', 'invalid'); + + // Fix by lowering minimum below maximum + setNumberInputValue(minField, 3); + + // Both fields should no longer be invalid + cy.get(minField).should('not.have.attr', 'invalid'); + cy.get(maxField).should('not.have.attr', 'invalid'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(fileInputDrop); + }); + + it('blocks dialog save when minimum files is greater than maximum files', function () { + dropFileInputInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + fileInputEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + + // Enable multi-selection (Basic tab) to reveal min/max files fields on the Validation tab + cy.get("[name='./multiSelection'][type='checkbox']").should('exist').check({ force: true }); + // Navigate to Validation tab where min/max fields live + cy.get('.cmp-adaptiveform-fileinput__editdialog').contains('Validation').click(); + cy.get(minField).should('be.visible'); + + // Set an invalid state: minimum > maximum + setNumberInputValue(maxField, 5); + setNumberInputValue(minField, 10); + + // Attempt to save — dialog should remain open + cy.get('.cq-dialog-submit').click(); + cy.get('coral-dialog[open]').should('exist'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(fileInputDrop); + }); + }); +}); diff --git a/ui.tests/test-module/specs/numberinput/numberinput.minmax.authoring.cy.js b/ui.tests/test-module/specs/numberinput/numberinput.minmax.authoring.cy.js new file mode 100644 index 0000000000..3a5a28c9b7 --- /dev/null +++ b/ui.tests/test-module/specs/numberinput/numberinput.minmax.authoring.cy.js @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2022 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +const afConstants = require("../../libs/commons/formsConstants"); +const sitesSelectors = require("../../libs/commons/sitesSelectors"); + +describe('Page - Authoring', function () { + const dropNumberInputInContainer = function () { + const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*", + responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; + cy.selectLayer("Edit"); + cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Number Input", afConstants.components.forms.resourceType.formnumberinput); + cy.get('body').click(0, 0); + } + + context('Open Forms Editor', function () { + const pagePath = "/content/forms/af/core-components-it/blank", + numberInputEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/numberinput", + numberInputEditPathSelector = "[data-path='" + numberInputEditPath + "']", + numberInputDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.formnumberinput.split("/").pop(), + numberInputBlockBemSelector = '.cmp-adaptiveform-numberinput', + editDialogConfigurationSelector = "[data-action='CONFIGURE']"; + + beforeEach(function () { + cy.openAuthoring(pagePath); + }); + + it('shows inline error when minimum is set greater than maximum', function () { + dropNumberInputInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + numberInputEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + cy.get(numberInputBlockBemSelector + '__editdialog').contains('Validation').click(); + + // Set maximum = 5, then minimum = 10 (invalid: min > max) + cy.get(numberInputBlockBemSelector + '__maximum').find('input').clear().type('5'); + cy.focused().blur(); + cy.get(numberInputBlockBemSelector + '__minimum').find('input').clear().type('10'); + cy.focused().blur(); + + // Both fields should be marked invalid + cy.get(numberInputBlockBemSelector + '__minimum').should('have.attr', 'invalid'); + cy.get(numberInputBlockBemSelector + '__maximum').should('have.attr', 'invalid'); + + // Fix by lowering minimum below maximum + cy.get(numberInputBlockBemSelector + '__minimum').find('input').clear().type('3'); + cy.focused().blur(); + + // Both fields should no longer be invalid + cy.get(numberInputBlockBemSelector + '__minimum').should('not.have.attr', 'invalid'); + cy.get(numberInputBlockBemSelector + '__maximum').should('not.have.attr', 'invalid'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(numberInputDrop); + }); + + it('blocks dialog save when minimum is greater than maximum', function () { + dropNumberInputInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + numberInputEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + cy.get(numberInputBlockBemSelector + '__editdialog').contains('Validation').click(); + + // Set an invalid state: minimum > maximum + cy.get(numberInputBlockBemSelector + '__maximum').find('input').clear().type('5'); + cy.focused().blur(); + cy.get(numberInputBlockBemSelector + '__minimum').find('input').clear().type('10'); + cy.focused().blur(); + + // Attempt to save — dialog should remain open + cy.get('.cq-dialog-submit').click(); + cy.get('coral-dialog[open]').should('exist'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(numberInputDrop); + }); + }); +}); diff --git a/ui.tests/test-module/specs/panelcontainer/panelcontainer.minmax.authoring.cy.js b/ui.tests/test-module/specs/panelcontainer/panelcontainer.minmax.authoring.cy.js new file mode 100644 index 0000000000..ef8106fed2 --- /dev/null +++ b/ui.tests/test-module/specs/panelcontainer/panelcontainer.minmax.authoring.cy.js @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright 2022 Adobe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +const afConstants = require("../../libs/commons/formsConstants"); +const sitesSelectors = require("../../libs/commons/sitesSelectors"); + +describe('Page - Authoring', function () { + const dropPanelContainerInContainer = function () { + const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*", + responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; + cy.selectLayer("Edit"); + cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Panel", afConstants.components.forms.resourceType.panelcontainer); + cy.get('body').click(0, 0); + }; + + context('Open Forms Editor', function () { + const pagePath = "/content/forms/af/core-components-it/blank", + panelContainerEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/panelcontainer", + panelContainerEditPathSelector = "[data-path='" + panelContainerEditPath + "']", + panelContainerDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.panelcontainer.split("/").pop(), + editDialogConfigurationSelector = "[data-action='CONFIGURE']", + minField = '.cmp-adaptiveform-panelcontainer__minOccur coral-numberinput', + maxField = '.cmp-adaptiveform-panelcontainer__maxOccur coral-numberinput'; + + beforeEach(function () { + cy.openAuthoring(pagePath); + }); + + it('shows inline error when minimum occurrences is set greater than maximum occurrences', function () { + dropPanelContainerInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + panelContainerEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + cy.get('.cmp-adaptiveform-panelcontainer__editdialog').contains('Repeat Panel').click(); + + // Enable repeatability to unlock min/max occurrence fields + cy.get('.cmp-adaptiveform-panelcontainer__repeatable coral-switch').click(); + cy.get(maxField).should('not.have.attr', 'disabled'); + + // Set maximum = 5, then minimum = 10 (invalid: min > max) + cy.get(maxField).find('input').clear().type('5'); + cy.focused().blur(); + cy.get(minField).find('input').clear().type('10'); + cy.focused().blur(); + + // Both fields should be marked invalid + cy.get(minField).should('have.attr', 'invalid'); + cy.get(maxField).should('have.attr', 'invalid'); + + // Fix by lowering minimum below maximum + cy.get(minField).find('input').clear().type('3'); + cy.focused().blur(); + + // Both fields should no longer be invalid + cy.get(minField).should('not.have.attr', 'invalid'); + cy.get(maxField).should('not.have.attr', 'invalid'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(panelContainerDrop); + }); + + it('blocks dialog save when minimum occurrences is greater than maximum occurrences', function () { + dropPanelContainerInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + panelContainerEditPathSelector); + cy.invokeEditableAction(editDialogConfigurationSelector); + cy.get('.cmp-adaptiveform-panelcontainer__editdialog').contains('Repeat Panel').click(); + + // Enable repeatability to unlock min/max occurrence fields + cy.get('.cmp-adaptiveform-panelcontainer__repeatable coral-switch').click(); + cy.get(maxField).should('not.have.attr', 'disabled'); + + // Set an invalid state: minimum > maximum + cy.get(maxField).find('input').clear().type('5'); + cy.focused().blur(); + cy.get(minField).find('input').clear().type('10'); + cy.focused().blur(); + + // Attempt to save — dialog should remain open + cy.get('.cq-dialog-submit').click(); + cy.get('coral-dialog[open]').should('exist'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(panelContainerDrop); + }); + }); +}); diff --git a/ui.tests/test-module/specs/textinput/textinput.minmax.authoring.cy.js b/ui.tests/test-module/specs/textinput/textinput.minmax.authoring.cy.js new file mode 100644 index 0000000000..04ffd0efaa --- /dev/null +++ b/ui.tests/test-module/specs/textinput/textinput.minmax.authoring.cy.js @@ -0,0 +1,87 @@ +/* + * Copyright 2022 Adobe Systems Incorporated + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const sitesSelectors = require('../../libs/commons/sitesSelectors'), + afConstants = require('../../libs/commons/formsConstants'); + +describe('Page - Authoring', function () { + const dropTextInputInContainer = function () { + const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*", + responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; + cy.selectLayer("Edit"); + cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Text Box", afConstants.components.forms.resourceType.formtextinput); + cy.get('body').click(0, 0); + } + + context('Open Forms Editor', function () { + const pagePath = "/content/forms/af/core-components-it/blank", + textInputEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/textinput", + textInputEditPathSelector = "[data-path='" + textInputEditPath + "']", + textInputDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.formtextinput.split("/").pop(); + + beforeEach(function () { + cy.openAuthoring(pagePath); + }); + + it('shows inline error when minLength is set greater than maxLength', function () { + dropTextInputInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textInputEditPathSelector); + cy.invokeEditableAction("[data-action='CONFIGURE']"); + cy.get('.cmp-adaptiveform-textinput__editdialog').contains('Validation').click({force: true}); + + // Set maxLength = 5, then minLength = 10 (invalid: min > max) + cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').find('input').clear().type('5'); + cy.focused().blur(); + cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').find('input').clear().type('10'); + cy.focused().blur(); + + // Both fields should be marked invalid + cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').should('have.attr', 'invalid'); + cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').should('have.attr', 'invalid'); + + // Fix by lowering minLength below maxLength + cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').find('input').clear().type('3'); + cy.focused().blur(); + + // Both fields should no longer be invalid + cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').should('not.have.attr', 'invalid'); + cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').should('not.have.attr', 'invalid'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(textInputDrop); + }); + + it('blocks dialog save when minLength is greater than maxLength', function () { + dropTextInputInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textInputEditPathSelector); + cy.invokeEditableAction("[data-action='CONFIGURE']"); + cy.get('.cmp-adaptiveform-textinput__editdialog').contains('Validation').click({force: true}); + + // Set an invalid state: minLength > maxLength + cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').find('input').clear().type('5'); + cy.focused().blur(); + cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').find('input').clear().type('10'); + cy.focused().blur(); + + // Attempt to save — dialog should remain open + cy.get('.cq-dialog-submit').click(); + cy.get('coral-dialog[open]').should('exist'); + + cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.deleteComponentByPath(textInputDrop); + }); + }); +});