diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index bde15e70e4..f26bd1e25e 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -155,6 +155,10 @@ curl -s -u admin:admin \ | `cy.previewForm(pagePath)` | Loads form at `?wcmmode=disabled`, waits for AF runtime init, returns `formContainer` | | `cy.previewFormWithPanel(pagePath)` | Same as above but waits for panel-specific initialization | | `cy.fetchFeatureToggles()` | `GET /etc.clientlibs/toggles.json`, returns `{ enabled: ["FT_FORMS-XXXXX", ...] }` | +| `cy.selectLayer(layer)` | Keeps an already-selected layer; otherwise waits for the selected layer and overlay repositioning | +| `cy.insertComponent(dropZone, searchText, resourceType)` | Opens the Insert dialog with bounded retries, searches its visible component region, and waits for insertion and overlay refresh | + +Authoring specs should reuse these commands instead of implementing their own layer-switch or insertion sequence. Insert-dialog selectors use `guideSelectors.insertComponentDialog.dialog` (`.InsertComponentDialog-components`) and replacement selectors use the Forms hook `.cmp-replace-dialog-search-components`; visibility is scoped to those content regions without depending on a `coral-dialog` element or an outer dialog wrapper. ### Key `cy.af` helpers (defined in `libs/support/functions.js`) diff --git a/ui.tests/test-module/libs/support/commands.js b/ui.tests/test-module/libs/support/commands.js index e6abd79ef7..20e5519a26 100644 --- a/ui.tests/test-module/libs/support/commands.js +++ b/ui.tests/test-module/libs/support/commands.js @@ -297,13 +297,23 @@ Cypress.Commands.add("openPage", (pagePath, options = {}) => { cy.visit(path, options); }); -// cypress command to select layer in authoring +/** + * Selects an authoring layer and waits for its overlays to be repositioned. + * @param {string} layer Authoring layer name. + */ Cypress.Commands.add("selectLayer", (layer) => { - // please note: when switching from style to other layer, we refresh guide, so these events need to be checked here - cy.get(siteSelectors.selectLayer.trigger).click(); - cy.get(siteSelectors.selectLayer.popover.self + ' [data-layer="' + layer + '"]').should('be.visible'); - cy.get(siteSelectors.selectLayer.popover.self + ' [data-layer="' + layer + '"]').click({force: true}); - cy.get(siteSelectors.selectLayer.current + '[data-layer="' + layer + '"].is-selected'); + const selectedLayer = siteSelectors.selectLayer.current + '[data-layer="' + layer + '"].is-selected'; + return cy.get(siteSelectors.selectLayer.current).then(($layers) => { + if ($layers.filter('[data-layer="' + layer + '"].is-selected').length) { + return cy.get(selectedLayer).should('be.visible'); + } + cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isLayerOverlaysRepositioned"); + cy.get(siteSelectors.selectLayer.trigger).click(); + cy.get(siteSelectors.selectLayer.popover.self + ' [data-layer="' + layer + '"]') + .should('be.visible').click({force: true}); + cy.get(selectedLayer).should('be.visible'); + return cy.get("@isLayerOverlaysRepositioned").its('done').should('equal', true); + }); }); // cypress command to open editable toolbar @@ -656,24 +666,47 @@ Cypress.Commands.add("deleteComponentByTitle", (title) => { }); -// cypress command to insert component +/** + * Inserts a component through the visible dialog and waits for the editor refresh. + * @param {string} selector Drop-zone overlay selector. + * @param {string} componentString Component search text. + * @param {string} componentType Component resource type. + */ Cypress.Commands.add("insertComponent", (selector, componentString, componentType) => { - //Open toolbar of root panel - const insertComponentDialog_Selector = '.InsertComponentDialog-components [value="' + componentType + '"]', - insertComponentDialog_searchField = ".InsertComponentDialog-components input[type='search']"; - cy.openEditableToolbar(selector); - cy.get(guideSelectors.editableToolbar.actions.insert).should('be.visible').click(); + const insertComponentDialog = guideSelectors.insertComponentDialog.dialog + ':visible', + insertComponentDialog_Selector = insertComponentDialog + ' [value="' + componentType + '"]', + insertComponentDialog_searchField = insertComponentDialog + " input[type='search']"; + // Overlay refreshes can discard an Insert click; retry opening, not interactions with a hidden dialog. recurse( - // the commands to repeat, and they yield the input element - () => cy.get(insertComponentDialog_searchField).clear().type(componentString), - // the predicate takes the output of the above commands - // and returns a boolean. If it returns true, the recursion stops + () => { + cy.get('body').then(($body) => { + if (!$body.find(insertComponentDialog).length) { + cy.openEditableToolbar(selector); + cy.get(guideSelectors.editableToolbar.actions.insert).should('be.visible').click(); + } + }); + return cy.get('body'); + }, + ($body) => $body.find(insertComponentDialog).length === 1, + {limit: 5, delay: 1000, timeout: 30000, log: false} + ); + cy.get(insertComponentDialog).should('have.length', 1).and('be.visible'); + recurse( + () => { + cy.get(insertComponentDialog_searchField).should('be.visible').clear(); + cy.get(insertComponentDialog_searchField).should('be.visible').type(componentString); + return cy.get(insertComponentDialog_searchField); + }, ($input) => $input.val() === componentString, - ) + ); cy.get(insertComponentDialog_searchField).type('{enter}'); - cy.get(insertComponentDialog_Selector).should('be.visible');// basically should assertions does implicit retry in cypress - // refer https://docs.cypress.io/guides/references/error-messages.html#cy-failed-because-the-element-you-are-chaining-off-of-has-become-detached-or-removed-from-the-dom - cy.get(insertComponentDialog_Selector).click({force: true}); // sometimes AEM popover is visible, hence adding force here + cy.get(insertComponentDialog_Selector).should('be.visible'); + cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_EDITABLES_UPDATED).as("isInsertEditableUpdated"); + cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isInsertOverlaysRepositioned"); + cy.get(insertComponentDialog_Selector).click(); + cy.get(insertComponentDialog).should('not.exist'); + cy.get("@isInsertEditableUpdated").its('done').should('equal', true); + return cy.get("@isInsertOverlaysRepositioned").its('done').should('equal', true); }); /** diff --git a/ui.tests/test-module/specs/replaceaction/replacelayout-sites.authoring.cy.js b/ui.tests/test-module/specs/replaceaction/replacelayout-sites.authoring.cy.js index e4a941aec5..d0fb644268 100644 --- a/ui.tests/test-module/specs/replaceaction/replacelayout-sites.authoring.cy.js +++ b/ui.tests/test-module/specs/replaceaction/replacelayout-sites.authoring.cy.js @@ -15,7 +15,10 @@ */ +/* global Cypress */ + const sitesSelectors = require('../../libs/commons/sitesSelectors'), + sitesConstants = require('../../libs/commons/sitesConstants'), afConstants = require('../../libs/commons/formsConstants'); /** @@ -26,6 +29,19 @@ describe('Replace functionality - sites', function () { const pagePath = "/content/forms/sites/core-components-it/blank", pageDropZoneSuffix = "/jcr:content/root/responsivegrid/container"; + const replaceDialog = '.cmp-replace-dialog-search-components:visible'; + + const selectReplacement = (resourceTypeSelector, componentPath) => { + cy.initializeEventHandlerOnChannel(sitesConstants.EVENT_NAME_EDITABLES_UPDATED).as('replacementEditableUpdated'); + cy.initializeEventHandlerOnChannel(sitesConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as('replacementOverlaysRepositioned'); + cy.intercept('POST', '**' + componentPath).as('replaceComponent'); + cy.get(replaceDialog + ' ' + resourceTypeSelector).should('be.visible').click(); + cy.wait('@replaceComponent').its('response.statusCode').should('be.oneOf', [200, 201]); + cy.get(replaceDialog).should('not.exist'); + cy.get('@replacementEditableUpdated').its('done').should('equal', true); + cy.get('@replacementOverlaysRepositioned').its('done').should('equal', true); + }; + const dropComponentInSites = function (componentName, resourceType) { const dataPath = "/content/forms/sites/core-components-it/blank/jcr:content/root/responsivegrid/container/container/*", responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']"; @@ -46,14 +62,13 @@ describe('Replace functionality - sites', function () { cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + editPathSelector); cy.invokeEditableAction("[data-action='replace']"); // this line is causing frame busting which is causing cypress to fail - cy.get('.cmp-replace-dialog-search-components').should('exist'); + cy.get(replaceDialog).should('be.visible'); // Check If Dialog Options Are Visible cy.get(textInput) .should("not.exist"); cy.get(submitButton) .should("exist"); - cy.get(title) - .click(); + selectReplacement(title, pagePath + pageDropZoneSuffix + "/container/button"); cy.get('[title="'+titleName+'"]') .should('exist'); @@ -85,8 +100,7 @@ describe('Replace functionality - sites', function () { .should("exist"); cy.get(wizard) .should("exist"); - cy.get(accordion) - .click(); + selectReplacement(accordion, pagePath + pageDropZoneSuffix + "/container/panelcontainer"); cy.get(accordionDefaultPanel) .should("not.exist"); @@ -107,38 +121,56 @@ describe('Replace functionality - sites', function () { testButtonReplaceBehaviour(buttonEditPathSelector); }); - it('test behaviour of replace button', function () { + it('test behaviour of replace panel with accordion', function () { testPanelReplaceBehaviourWithAccordion(panelEditPathSelector); }); - }); - - const checkTestGroupPolicy = () => { - cy.openAuthoring("/conf/core-components-examples/settings/wcm/templates/content-page/structure"); - cy.get('[data-text="Layout Container"]').eq(0).click() - .then(() => { - cy.get('.cq-editable-action').eq(3).click().then(() => { - cy.get('[value="group:replace test group"]').eq(0).click().then(() => { - cy.get('[title="Done"]').scrollIntoView().click(); - }) - }); + it('recovers when the first insert action is lost during an editor refresh', function () { + cy.intercept('POST', '**/content/forms/sites/core-components-it/blank/**', (request) => { + request.on('response', (response) => response.setDelay(500)); }); - } - - const uncheckTestGroupPolicy = () => { - cy.openSiteAuthoring("/conf/core-components-examples/settings/wcm/templates/content-page/structure"); - cy.get('[data-text="Layout Container"]').eq(0).click() - .then(() => { - cy.get('.cq-editable-action').eq(3).click().then(() => { - cy.get('[value="group:replace test group"]').eq(0).click().then(() => { - cy.get('[title="Done"]').scrollIntoView().click(); - }) - }); + const lostInsertAction = cy.stub().as('lostInsertAction'); + cy.window().then((win) => { + const discardFirstInsert = (event) => { + if (event.target.closest(sitesSelectors.editableToolbar.actions.insert)) { + event.preventDefault(); + event.stopImmediatePropagation(); + win.document.removeEventListener('click', discardFirstInsert, true); + lostInsertAction(); + } + }; + win.document.addEventListener('click', discardFirstInsert, true); }); - } + testButtonReplaceBehaviour(buttonEditPathSelector); + cy.get('@lostInsertAction').should('have.been.calledOnce'); + }); + + }); context('Test replace action within different groups', function () { const templatePath = "/conf/core-components-examples/settings/wcm/templates/content-page/structure"; + const policyDialog = '.cq-dialog:visible:has([value="group:replace test group"])', + policyCheckbox = policyDialog + ' [value="group:replace test group"]'; + let originalTestGroupAllowed; + + const updateTestGroupPolicy = (allow) => { + cy.openEditableToolbar(sitesSelectors.overlays.overlay.self + '[data-path="' + templatePath + '/jcr:content/root/responsivegrid"]'); + cy.invokeEditableAction(sitesSelectors.editableToolbar.actions.policy); + cy.get(policyCheckbox).first().scrollIntoView().should('be.visible').invoke('prop', 'checked') + .should('be.a', 'boolean').then((checked) => { + if (originalTestGroupAllowed === undefined) { + originalTestGroupAllowed = checked; + } + if (checked !== allow) { + cy.get(policyCheckbox).first().click(); + } + }); + cy.get(policyCheckbox).first().should('have.prop', 'checked', allow); + cy.intercept('POST', '**/conf/core-components-examples/settings/wcm/policies/**').as('saveReplacePolicy'); + cy.get(policyDialog + ' [title="Done"]').scrollIntoView().should('be.visible').click(); + cy.wait('@saveReplacePolicy').its('response.statusCode').should('be.oneOf', [200, 201]); + cy.get(policyDialog).should('not.exist'); + }; const pagePath = "/content/forms/sites/core-components-it/blank", replaceCompTestGroup = "/apps/forms-core-components-it/form/image", @@ -153,23 +185,31 @@ describe('Replace functionality - sites', function () { replaceCompTestGroupDrop = pagePath + containerSuffix + "/container/image"; beforeEach(function () { - checkTestGroupPolicy(); + originalTestGroupAllowed = undefined; + cy.openAuthoring(templatePath); + updateTestGroupPolicy(true); }); afterEach(function () { - uncheckTestGroupPolicy(); + if (originalTestGroupAllowed !== undefined) { + cy.openSiteAuthoring(templatePath); + updateTestGroupPolicy(originalTestGroupAllowed); + } }); it('test behaviour of replace within different groups same component type', function () { cy.openSiteAuthoring(pagePath); cy.selectLayer("Edit"); + cy.cleanTest(editPath); cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Image", afConstants.components.forms.resourceType.formimage); cy.get('body').click( 0,0); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + editPathSelector); cy.invokeEditableAction("[data-action='replace']"); - cy.get(image).click(); + selectReplacement(image, editPath); + cy.request((Cypress.env('crx.contextPath') || '') + editPath + '.0.json') + .its('body.sling:resourceType').should('equal', replaceCompTestGroup.replace('/apps/', '')); cy.deleteComponentByPath(replaceCompTestGroupDrop); }); @@ -180,13 +220,16 @@ describe('Replace functionality - sites', function () { cy.openSiteAuthoring(pagePath); cy.selectLayer("Edit"); + cy.cleanTest(buttonEditPath); cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Button", afConstants.components.forms.resourceType.formbutton); cy.get('body').click( 0,0); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); cy.invokeEditableAction("[data-action='replace']"); - cy.get(image).click(); + selectReplacement(image, buttonEditPath); + cy.request((Cypress.env('crx.contextPath') || '') + buttonEditPath + '.0.json') + .its('body.sling:resourceType').should('equal', replaceCompTestGroup.replace('/apps/', '')); cy.deleteComponentByPath(buttonEditPath); }); }); diff --git a/ui.tests/test-module/specs/telephoneinput/telephoneinput.authoring.cy.js b/ui.tests/test-module/specs/telephoneinput/telephoneinput.authoring.cy.js index d6a5553032..ca84c383b6 100644 --- a/ui.tests/test-module/specs/telephoneinput/telephoneinput.authoring.cy.js +++ b/ui.tests/test-module/specs/telephoneinput/telephoneinput.authoring.cy.js @@ -15,6 +15,8 @@ */ +/* global Cypress */ + const sitesSelectors = require('../../libs/commons/sitesSelectors'), afConstants = require('../../libs/commons/formsConstants'); @@ -103,13 +105,26 @@ describe('Page - Authoring', function () { telephoneInputEditPathSelector = "[data-path='" + telephoneInputEditPath + "']"; const customKey = 'customKey', customValue = 'customValue', - // Shared, persistent design policy this test mutates — reset it each run so a leftover custom - // format from a prior run/retry can't leave the dialog in a dirty state (see reset command). + // The policy is shared across specs and retries. telephoneInputPolicyPath = '/conf/core-components-examples/settings/wcm/policies/forms-components-examples/components/form/telephoneinput/default'; + let policyPrepared = false; beforeEach(function () { - cy.openAuthoring(templateDataPath + ".html"); + policyPrepared = false; + cy.openAuthoring(templateDataPath); + cy.resetTelephoneInputDesignPolicy(telephoneInputPolicyPath).then(() => { + policyPrepared = true; + }); + }); + + afterEach(function () { + if (!policyPrepared) { + return; + } cy.resetTelephoneInputDesignPolicy(telephoneInputPolicyPath); + const contextPath = Cypress.env('crx.contextPath') || ''; + cy.request(contextPath + telephoneInputPolicyPath + '.json') + .its('body').should('not.have.property', 'allowedCustomFormats'); }); it('Adding removing patterns from design policy', function () { @@ -121,9 +136,18 @@ describe('Page - Authoring', function () { cy.get('[name="./allowedCustomFormats/item0/customFormatKey"]').should('exist').then(() => { cy.get('[name="./allowedCustomFormats/item0/customFormatKey"]').focus().type(customKey); cy.get('[name="./allowedCustomFormats/item0/customFormatValue"]').focus().type(customValue); - cy.get('[title="Done"]').click(); + cy.intercept('POST', '**' + telephoneInputPolicyPath + '*').as('saveTelephonePolicy'); + cy.get('[title="Done"]:visible').click(); + cy.wait('@saveTelephonePolicy').its('response.statusCode').should('be.oneOf', [200, 201]); + cy.get(bemDesignDialog + ':visible').should('not.exist'); + const contextPath = Cypress.env('crx.contextPath') || ''; + cy.request(contextPath + telephoneInputPolicyPath + '.infinity.json') + .its('body.allowedCustomFormats.item0') + .should('include', {customFormatKey: customKey, customFormatValue: customValue}); }).then(() => { cy.openSiteAuthoring(authoringPagePath); + cy.selectLayer('Edit'); + cy.cleanTest(telephoneInputDrop); dropTelephoneInputInContainer(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + telephoneInputEditPathSelector); cy.invokeEditableAction("[data-action='CONFIGURE']"); @@ -132,6 +156,7 @@ describe('Page - Authoring', function () { cy.get('.cmp-adaptiveform-telephoneinput__validationpattern select').select(customKey, {force: true}).then(() => { cy.get('.cmp-adaptiveform-telephoneinput__validationformat').should('have.value', customValue); cy.get('.cq-dialog-cancel').click(); + cy.get(bemEditDialog + ':visible').should('not.exist'); cy.deleteComponentByPath(telephoneInputDrop); }) });