From 5fd5c61d10d2ad479c009c158830b8f228726a63 Mon Sep 17 00:00:00 2001 From: Rishi Mehta Date: Tue, 6 Oct 2026 14:43:48 +0530 Subject: [PATCH 01/10] fix(tests): stabilize release/650 authoring Cypress tests Synchronize editor transitions, isolate fixtures, use classic Coral controls, and preserve telephone policy state. Add controlled authoring race regressions and optional remote request authentication. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ui.tests/test-module/README.md | 40 +++ ui.tests/test-module/libs/support/commands.js | 272 +++++++++++++++--- .../authoring/authoring.synchronization.cy.js | 106 +++++++ .../checkboxgroup.authoring.cy.js | 77 ++--- .../contentfragment.authoring.cy.js | 34 ++- .../specs/dropdown/dropdown.authoring.cy.js | 22 +- .../telephoneinput.authoring.cy.js | 61 +++- .../specs/wizard/wizard.authoring.cy.js | 44 +-- 8 files changed, 527 insertions(+), 129 deletions(-) create mode 100644 ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js diff --git a/ui.tests/test-module/README.md b/ui.tests/test-module/README.md index 3e79987a20..a553e9e5fb 100644 --- a/ui.tests/test-module/README.md +++ b/ui.tests/test-module/README.md @@ -30,3 +30,43 @@ npm install ``` After execution, reports and logs are available in `target/reports` folder + +## Remote authoring and stability checks + +Override `baseUrl` rather than changing the committed localhost configuration: + +```sh +npx --no-install cypress run --browser chrome --headless \ + --spec 'specs/authoring/authoring.synchronization.cy.js,specs/wizard/wizard.authoring.cy.js,specs/checkboxgroup/checkboxgroup.authoring.cy.js,specs/dropdown/dropdown.authoring.cy.js,specs/contentfragment/contentfragment.authoring.cy.js,specs/telephoneinput/telephoneinput.authoring.cy.js' \ + --config 'baseUrl=https://author.example,retries=0,video=false' \ + --env 'crx.loginViaRequest=true' +``` + +`crx.loginViaRequest` is optional and defaults to the existing UI login flow. +Enable it when an external author endpoint requires a session before navigation +or its UI login redirects escape the Cypress frame. It uses AEM's form-login +endpoint and the existing `crx.username`, `crx.password`, and `crx.contextPath` +settings. Supply credentials using Cypress environment configuration; do not +commit them. For an ngrok browser-warning endpoint, a non-browser `userAgent` +override can be supplied together with request login. + +The synchronization spec injects a delayed overlay, lost Insert and layer-switcher +clicks, and delayed insertion/configure responses. Component insertion must finish the +editor refresh before a caller configures or deletes the new component. +Use `cleanTest` after entering the Edit layer, `cancelConfigureDialog` for +configure-dialog cancellation, and `submitConfigureDialog` when a save must +finish before the next toolbar action. Select classic Coral options through +`selectCoralOption`, rather than assuming a native `select` or modern Coral +CSS classes. + +Use `createRule` to wait for the rule editor's statement builder, rather than +assuming its first Create click is handled while the iframe initializes. + +Use `cleanTestFixture` in teardown to remove only the named test-owned component +through authenticated requests, even if the editor or configure dialog failed. +Tutorial preferences are saved through awaited requests; request-login authoring +does not depend on the landing-page redirect. + +Repeat stability runs with retries disabled, including per-test retry overrides. +Verify that test-owned components are removed and the telephone policy matches +its original snapshot after both passing and failing runs. diff --git a/ui.tests/test-module/libs/support/commands.js b/ui.tests/test-module/libs/support/commands.js index 44156f23e6..7fa389a224 100644 --- a/ui.tests/test-module/libs/support/commands.js +++ b/ui.tests/test-module/libs/support/commands.js @@ -62,6 +62,22 @@ Cypress.on('uncaught:exception', (err, runnable) => { Cypress.Commands.add("login", (pagePath, failurehandler = () => {}) => { const username = Cypress.env('crx.username') ? Cypress.env('crx.username') : "admin"; const password = Cypress.env('crx.password') ? Cypress.env('crx.password') : "admin"; + if (Cypress.env('crx.loginViaRequest') === true) { + const contextPath = Cypress.env('crx.contextPath') || ''; + return cy.request({ + method: 'POST', + url: contextPath + '/libs/granite/core/content/login.html/j_security_check', + form: true, + body: {j_username: username, j_password: password, j_validate: 'true', resource: contextPath + '/index.html'}, + followRedirect: false, + log: false + }).then(({status}) => { + expect(status, 'AEM form authentication').to.be.oneOf([200, 302]); + return cy.getCookie('login-token', {log: false}).then(cookie => { + expect(Boolean(cookie), 'AEM session established').to.equal(true); + }); + }); + } cy.get('#username').type(username); cy.get('#password').type(password); let retryCount = 0; @@ -131,44 +147,66 @@ Cypress.Commands.add("enableOrDisableTutorials", (enable) => { }); cy.get("@home").then(home => { const url = contextPath + home + '/preferences'; - //cy.request('POST', url, preferences) // not using cy.request, since application level cookies needs to be passed - cy.window().then(win => { - win.$.post(url, preferences) + return cy.request({ + method: 'POST', + url, + form: true, + headers: {'CSRF-Token': preferences[":cq_csrf_token"]}, + body: preferences, + log: false }); }); }); /** - * Resets a telephoneinput design policy to its baseline by removing the custom-format additions a test - * makes (`allowedCustomFormats` items and the `allowedFormat3` toggle). The design policy is shared, - * persistent repo state; without this, a test that adds a custom format leaves it behind and any later - * run/retry starts from a dirty policy — the multifield/Coral dialog then misbehaves and - * `allowedCustomFormats/item0/customFormatKey` never renders. Call in beforeEach for a deterministic start. + * Restores the mutable telephone policy fields from a snapshot, including existing custom formats. * @param policyPath JCR path of the telephoneinput policy node (…/policies/…/telephoneinput/) */ -Cypress.Commands.add("resetTelephoneInputDesignPolicy", (policyPath) => { +Cypress.Commands.add("restoreTelephoneInputDesignPolicy", (policyPath, policy) => { const contextPath = Cypress.env('crx.contextPath') ? Cypress.env('crx.contextPath') : ""; const url = contextPath + policyPath; - // POST via the app's jQuery inside the authenticated page (not cy.request) so the session cookie is - // carried — same reason enableOrDisableTutorials does; the sling `@Delete` removes the property/node. cy.request(contextPath + '/libs/granite/csrf/token.json').its('body.token').then((token) => { - cy.window().then((win) => new Cypress.Promise((resolve) => { - win.$.post(url, { + const options = {method: 'POST', url, form: true, headers: {'CSRF-Token': token}, log: false}; + cy.request({...options, body: { 'allowedCustomFormats@Delete': '', 'allowedFormat3@Delete': '', ':cq_csrf_token': token - }).always(resolve); - })); + }}); + const body = {':cq_csrf_token': token}; + if (policy.allowedFormat3 !== undefined) { + body.allowedFormat3 = policy.allowedFormat3; + } + if (policy.allowedCustomFormats) { + body['allowedCustomFormats/jcr:primaryType'] = policy.allowedCustomFormats['jcr:primaryType']; + } + Object.entries(policy.allowedCustomFormats || {}).forEach(([item, values]) => { + if (typeof values === 'object' && values !== null) { + Object.entries(values).forEach(([name, value]) => { + if (name === 'jcr:primaryType' || !name.startsWith('jcr:')) { + body['allowedCustomFormats/' + item + '/' + name] = value; + } + }); + } + }); + cy.request({...options, body}); + }); + return cy.request(url + '.infinity.json').then(({body}) => { + expect(body.allowedFormat3, 'restored policy pattern').to.equal(policy.allowedFormat3); + expect(body.allowedCustomFormats, 'restored custom patterns').to.deep.equal(policy.allowedCustomFormats); }); }); // Cypress command to open AFv2 Cypress.Commands.add("openAFv2TemplateEditor", () => { const baseUrl = Cypress.env('crx.contextPath') ? Cypress.env('crx.contextPath') : ""; - cy.visit(baseUrl, {'failOnStatusCode': false}); - cy.login(baseUrl, () => { - cy.openAFv2TemplateEditor(); - }); + if (Cypress.env('crx.loginViaRequest') === true) { + cy.login(baseUrl); + } else { + cy.visit(baseUrl, {'failOnStatusCode': false}); + cy.login(baseUrl, () => { + cy.openAFv2TemplateEditor(); + }); + } cy.openTemplateEditor("/conf/core-components-examples/settings/wcm/templates/af-blank-v2/structure.html"); }); @@ -276,11 +314,15 @@ Cypress.Commands.add("openTemplateEditor", (templatePath) => { // Cypress command to open authoring page Cypress.Commands.add("openAuthoring", (pagePath) => { const baseUrl = Cypress.env('crx.contextPath') ? Cypress.env('crx.contextPath') : ""; - // getting status 403 intermittently, just ignore it - cy.visit(baseUrl, {'failOnStatusCode': false}); - cy.login(baseUrl, () => { - cy.openAuthoring(pagePath); - }); + if (Cypress.env('crx.loginViaRequest') === true) { + cy.login(baseUrl); + } else { + // getting status 403 intermittently, just ignore it + cy.visit(baseUrl, {'failOnStatusCode': false}); + cy.login(baseUrl, () => { + cy.openAuthoring(pagePath); + }); + } cy.openSiteAuthoring(pagePath); }); @@ -289,9 +331,12 @@ Cypress.Commands.add("openPage", (pagePath, options = {}) => { const contextPath = Cypress.env('crx.contextPath') ? Cypress.env('crx.contextPath') : ""; let path = ((contextPath && !pagePath.startsWith(contextPath)) ? `${contextPath}${pagePath.startsWith('/') ? '' : '/'}${pagePath}` : pagePath); if (!options.noLogin) { - // getting status 403 intermittently, just ignore it - const baseUrl = contextPath; - cy.visit(baseUrl, {'failOnStatusCode': false}); + if (Cypress.env('crx.loginViaRequest') === true) { + cy.login(contextPath); + } else { + // getting status 403 intermittently, just ignore it + const baseUrl = contextPath; + cy.visit(baseUrl, {'failOnStatusCode': false}); cy.getCookie('login-token').then(cookie => { if(!cookie) { cy.login(baseUrl, () => { @@ -299,6 +344,7 @@ Cypress.Commands.add("openPage", (pagePath, options = {}) => { }); } }) + } } const defaultOptions = { retryOnStatusCodeFailure: true, @@ -310,11 +356,29 @@ Cypress.Commands.add("openPage", (pagePath, options = {}) => { // cypress command to select layer in authoring 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'; + const layerOption = siteSelectors.selectLayer.popover.self + ' [data-layer="' + layer + '"]'; + return cy.get(siteSelectors.selectLayer.current).then($layers => { + if ($layers.filter('[data-layer="' + layer + '"].is-selected').length) { + return cy.get(selectedLayer).should('be.visible'); + } + recurse( + () => { + cy.get('body').then($body => { + if (!$body.find(siteSelectors.selectLayer.popover.self).is(':visible')) { + cy.get(siteSelectors.selectLayer.trigger).should('be.visible').click(); + } + }); + return cy.get('body'); + }, + $body => $body.find(layerOption).is(':visible'), + {limit: 5, delay: 500, timeout: 10000, log: false} + ); + cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isLayerOverlaysRepositioned"); + cy.get(layerOption).click({force: true}); + cy.get(selectedLayer).should('be.visible'); + return cy.get("@isLayerOverlaysRepositioned").its('done').should('equal', true); + }); }); // cypress command to open editable toolbar @@ -352,6 +416,57 @@ Cypress.Commands.add("invokeEditableAction", (actionSelector) => { cy.get(actionSelector).should('be.visible').click({force: true}); }); +Cypress.Commands.add("cancelConfigureDialog", () => { + return recurse( + () => { + cy.get('body').then($body => { + if ($body.find('.cq-dialog-cancel:visible').length) { + cy.get('.cq-dialog-cancel:visible').should('have.length', 1).click(); + } + }); + return cy.get('body'); + }, + $body => !$body.find('.cq-dialog-cancel:visible').length, + {limit: 5, delay: 500, timeout: 10000, log: false} + ); +}); + +Cypress.Commands.add("createRule", () => { + const statement = guideSelectors.ruleEditor.choiceModels.STATEMENT + ' .child-choice-name'; + return recurse( + () => { + cy.getRuleEditorIframe().should('not.have.class', 'af-freezeBody').then($body => { + if (!$body.find(statement).is(':visible')) { + cy.wrap($body).find(guideSelectors.ruleEditor.action.createRuleButton) + .should('be.visible').and('not.be.disabled').click(); + } + }); + return cy.getRuleEditorIframe(); + }, + $body => $body.find(statement).is(':visible'), + {limit: 5, delay: 500, timeout: 10000, log: false} + ); +}); + +Cypress.Commands.add("selectCoralOption", (selector, value) => { + cy.get(selector).should('have.length', 1).scrollIntoView().should('be.visible'); + cy.get(selector).find('button').first().should('be.visible').click(); + cy.get(selector).find('coral-selectlist-item[value="' + value + '"]') + .should('have.length', 1).and('be.visible').click(); + return cy.get(selector).should('have.value', value); +}); + +Cypress.Commands.add("fillTextField", (selector, value) => { + return recurse( + () => { + cy.get(selector).scrollIntoView().should('be.visible').and('not.be.disabled').clear(); + return cy.get(selector).type(value); + }, + $input => $input.val() === value, + {limit: 5, delay: 500, timeout: 10000, log: false} + ); +}); + // cypress command to submit a component's configure dialog and wait for the editor to settle. // A config-dialog submit fires an asynchronous editable re-render that repositions the overlays and // hides the shared #EditableToolbar. Any openEditableToolbar issued before that re-render settles @@ -363,6 +478,7 @@ Cypress.Commands.add("submitConfigureDialog", (submitSelector = ".cq-dialog-subm cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_EDITABLES_UPDATED).as("isConfigureEditableUpdated"); cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isConfigureOverlaysRepositioned"); cy.get(submitSelector).click({force: true}); + cy.get('.cq-dialog-submit:visible').should('not.exist'); cy.get("@isConfigureEditableUpdated").its('done').should('equal', true); // wait until re-render done cy.get("@isConfigureOverlaysRepositioned").its('done').should('equal', true); // wait until overlays settled }); @@ -585,14 +701,45 @@ Cypress.Commands.add("cleanTest", (editPath) => { // once it actually finished, so callers chaining off cleanTest could race ahead of the // real deletion. Returning the cypress command chain directly makes cleanTest actually // wait for deletion to complete. - return cy.get("body").then($body => { - const selector12 = "[data-path='" + editPath + "']"; - if ($body.find(selector12).length > 0) { + const contextPath = Cypress.env('crx.contextPath') || ''; + cy.selectLayer("Edit"); + return cy.request({url: contextPath + editPath + '.json', failOnStatusCode: false}).then(response => { + expect(response.status, 'fixture lookup').to.be.oneOf([200, 404]); + if (response.status === 200) { + cy.get(siteSelectors.overlays.overlay.component + "[data-path='" + editPath + "']").should('exist'); return cy.deleteComponentByPath(editPath); } }); }) +Cypress.Commands.add("cleanTestFixture", (componentPath) => { + const contextPath = Cypress.env('crx.contextPath') || ''; + const url = contextPath + componentPath; + return cy.request({url: url + '.json', failOnStatusCode: false, log: false}).then(response => { + expect(response.status, 'fixture cleanup lookup').to.be.oneOf([200, 404]); + if (response.status === 404) { + return; + } + expect(response.body['sling:resourceType'], 'test fixture resource type').to.be.a('string'); + expect(response.body['sling:resourceType'].split('/').pop(), 'owned fixture name') + .to.equal(componentPath.split('/').pop()); + cy.request({url: contextPath + '/libs/granite/csrf/token.json', log: false}).its('body.token').then(token => { + cy.request({ + method: 'POST', url, form: true, headers: {'CSRF-Token': token}, + body: {':operation': 'delete', ':cq_csrf_token': token}, log: false + }); + }); + return recurse( + () => cy.request({url: url + '.json', failOnStatusCode: false, log: false}), + result => { + expect(result.status, 'removed fixture lookup').to.be.oneOf([200, 404]); + return result.status === 404; + }, + {limit: 10, delay: 500, timeout: 20000, log: false} + ); + }); +}); + Cypress.Commands.add("cleanTitleTest", (editPath) => { // clean the test before the next run, if any. Same fix as cleanTest above: chain the // deletions on the returned cypress command instead of resolving a wrapping Promise early. @@ -630,7 +777,7 @@ Cypress.Commands.add("deleteComponentByPath", (componentPath) => { // failure screenshot showing the edit dialog still open when the delete-confirm dialog was // expected). Wait for any leftover open dialog to actually close first. cy.get('body').should($body => { - expect($body.find('coral-dialog.is-open').length, 'no leftover open dialog before delete').to.equal(0); + expect($body.find('coral-dialog.is-open:visible').length, 'no leftover open dialog before delete').to.equal(0); }); // open editable toolbar cy.openEditableToolbar(siteSelectors.overlays.overlay.component + componentPathSelector); @@ -644,6 +791,15 @@ Cypress.Commands.add("deleteComponentByPath", (componentPath) => { // wait for event to complete to signify deletion is complete cy.get("@isEditableUpdateEventComplete").its('done').should('equal', true); // wait here until done cy.get("@isOverlayRepositionEventComplete").its('done').should('equal', true); // wait here until done + const contextPath = Cypress.env('crx.contextPath') || ''; + return recurse( + () => cy.request({url: contextPath + componentPath + '.json', failOnStatusCode: false}), + response => { + expect(response.status, 'deleted fixture lookup').to.be.oneOf([200, 404]); + return response.status === 404; + }, + {limit: 10, delay: 500, timeout: 20000, log: false} + ); }); // cypress command to delete component by title @@ -657,7 +813,7 @@ Cypress.Commands.add("deleteComponentByTitle", (title) => { cy.initializeEventHandlerOnChannel(overlayRepositionEvent).as("isOverlayRepositionEventComplete"); // Same leftover-open-dialog race as deleteComponentByPath above; wait for it to actually close. cy.get('body').should($body => { - expect($body.find('coral-dialog.is-open').length, 'no leftover open dialog before delete').to.equal(0); + expect($body.find('coral-dialog.is-open:visible').length, 'no leftover open dialog before delete').to.equal(0); }); // open editable toolbar cy.openEditableToolbar(siteSelectors.overlays.overlay.component + componentPathSelector); @@ -674,22 +830,50 @@ Cypress.Commands.add("deleteComponentByTitle", (title) => { // cypress command to insert component Cypress.Commands.add("insertComponent", (selector, componentString, componentType) => { - //Open toolbar of root panel - const insertComponentDialog_Selector = '.InsertComponentDialog-components [value="' + componentType + '"]', - insertComponentDialog_searchField = ".InsertComponentDialog-components .coral3-Search-input"; - cy.openEditableToolbar(selector); - cy.get(guideSelectors.editableToolbar.actions.insert).should('be.visible').click(); + const dialog = '.InsertComponentDialog:visible', + insertComponentDialog_Selector = dialog + ' [value="' + componentType + '"]', + insertComponentDialog_searchField = dialog + " .coral3-Search-input, " + dialog + " input[type='search']"; + recurse( + () => { + cy.get('body').then($body => { + if (!$body.find(dialog).length) { + cy.openEditableToolbar(selector); + cy.get(guideSelectors.editableToolbar.actions.insert).should('be.visible').click(); + } + }); + return cy.get('body'); + }, + $body => $body.find(dialog).length === 1, + {limit: 5, delay: 1000, timeout: 30000, log: false} + ); recurse( // the commands to repeat, and they yield the input element - () => cy.get(insertComponentDialog_searchField).clear().type(componentString), + () => { + cy.get(insertComponentDialog_searchField).should('be.visible').clear(); + return cy.get(insertComponentDialog_searchField).type(componentString); + }, // the predicate takes the output of the above commands // and returns a boolean. If it returns true, the recursion stops ($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 + // Granite rebuilds the options asynchronously when the search is applied. + cy.get(dialog + ' coral-selectlist-item').should($items => { + const visibleItems = $items.filter(':visible'); + expect(visibleItems.length, 'filtered insert options').to.be.greaterThan(0); + visibleItems.each((index, item) => { + expect(item.textContent.trim().toLowerCase(), 'insert search applied') + .to.include(componentString.toLowerCase()); + }); + }); + cy.get(insertComponentDialog_Selector).should('have.length', 1).and('be.visible'); + cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_EDITABLES_UPDATED).as("isInsertEditableUpdated"); + cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isInsertOverlaysRepositioned"); // 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(dialog).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/authoring/authoring.synchronization.cy.js b/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js new file mode 100644 index 0000000000..72d907d696 --- /dev/null +++ b/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright 2026 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. + ******************************************************************************/ + +/* global cy, Cypress, expect */ +const sitesSelectors = require('../../libs/commons/sitesSelectors'); +const constants = require('../../libs/commons/formsConstants'); + +describe('Authoring synchronization', {retries: 0}, function () { + const page = '/content/forms/af/core-components-it/blank'; + const container = page + constants.FORM_EDITOR_FORM_CONTAINER_SUFFIX; + const component = container + '/telephoneinput'; + const overlay = sitesSelectors.overlays.overlay.component + '[data-path=\'' + component + '\']'; + const insert = () => cy.insertComponent( + sitesSelectors.overlays.overlay.component + '[data-path=\'' + container + '/*\']', + 'Adaptive Form Telephone input', + constants.components.forms.resourceType.formtelephoneinput + ); + + beforeEach(function () { + cy.openAuthoring(page); + cy.cleanTest(component); + }); + + afterEach(function () { + cy.cleanTestFixture(component); + }); + + it('cleans persisted fixtures while their overlay is still rendering', function () { + insert(); + cy.get(overlay).then($overlay => { + const parent = $overlay.parent(); + $overlay.detach(); + setTimeout(() => parent.append($overlay), 2000); + }); + cy.cleanTest(component); + cy.request({url: (Cypress.env('crx.contextPath') || '') + component + '.json', failOnStatusCode: false}) + .its('status').should('equal', 404); + }); + + it('recovers a lost Insert click without inserting twice', function () { + let lostClick = false; + cy.document().then(doc => { + const discardFirstClick = event => { + if (event.target.closest('[data-action="INSERT"]')) { + lostClick = true; + event.preventDefault(); + event.stopImmediatePropagation(); + doc.removeEventListener('click', discardFirstClick, true); + } + }; + doc.addEventListener('click', discardFirstClick, true); + }); + insert(); + cy.then(() => expect(lostClick, 'injected lost Insert click').to.equal(true)); + cy.get(overlay).should('have.length', 1); + cy.getContentIFrameBody().find('.cmp-adaptiveform-telephoneinput').should('have.length', 1); + }); + + it('recovers a lost layer-switcher opening click', function () { + cy.selectLayer('Layouting'); + cy.get('body').click(0, 0); + cy.get(sitesSelectors.selectLayer.popover.self).should('not.be.visible'); + let lostClick = false; + cy.document().then(doc => { + const discardFirstClick = event => { + if (event.target.closest(sitesSelectors.selectLayer.trigger)) { + lostClick = true; + event.preventDefault(); + event.stopImmediatePropagation(); + doc.removeEventListener('click', discardFirstClick, true); + } + }; + doc.addEventListener('click', discardFirstClick, true); + }); + cy.selectLayer('Edit'); + cy.then(() => expect(lostClick, 'injected lost layer-switcher click').to.equal(true)); + cy.get(sitesSelectors.selectLayer.current + '[data-layer="Edit"].is-selected').should('be.visible'); + }); + + it('waits for delayed insertion and configure refreshes before deleting', function () { + cy.intercept('POST', '**' + container + '/*', request => { + request.on('response', response => response.setDelay(1000)); + }); + insert(); + cy.openEditableToolbar(overlay); + cy.invokeEditableAction('[data-action="CONFIGURE"]'); + cy.fillTextField('input[name="./jcr:title"]', 'Synchronized telephone'); + cy.submitConfigureDialog(); + cy.request((Cypress.env('crx.contextPath') || '') + component + '.json') + .its('body.jcr:title').should('equal', 'Synchronized telephone'); + cy.deleteComponentByPath(component); + }); +}); diff --git a/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js b/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js index 75c2d0702f..6ad4c1f5d7 100644 --- a/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js +++ b/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js @@ -112,20 +112,42 @@ describe('Page - Authoring', function () { beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(checkBoxGroupDrop); }); + afterEach(function () { + cy.cleanTestFixture(checkBoxGroupDrop); + }); + + const getGroup = () => getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup').should('have.length', 1); + const configureRichTextOptions = () => { + dropCheckBoxGroupInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + checkBoxGroupEditPathSelector); + cy.invokeEditableAction("[data-action='CONFIGURE']"); + cy.get("input[name='./isTitleRichText'][type='checkbox']").check({force: true}).should('be.checked'); + cy.get("div[name='richTextTitle']").scrollIntoView().should('be.visible'); + cy.get("input[name='./areOptionsRichText'][type='checkbox']").check({force: true}).should('be.checked'); + cy.get("[data-cq-richtext-editable='true'][data-wrapperclass='cmp-adaptiveform-base__richTextEnumNames']") + .first().scrollIntoView().should('be.visible').focus().clear().type("Select 1").should('contain.text', "Select 1"); + cy.get("input[name='./orientation'][value='vertical']").scrollIntoView().click(); + cy.submitConfigureDialog(); + getGroup().should('contain.text', "Select 1"); + cy.openSiteAuthoring(pagePath); + getGroup().should('contain.text', "Select 1"); + }; + it('insert CheckBoxGroup in form container', function () { dropCheckBoxGroupInContainer(); cy.deleteComponentByPath(checkBoxGroupDrop); }); - it ('open edit dialog of CheckboxGroup', { retries: 3 }, function(){ + it ('open edit dialog of CheckboxGroup', function(){ cy.cleanTest(checkBoxGroupDrop).then(function() { testCheckBoxGroupBehaviour(checkBoxGroupEditPathSelector, checkBoxGroupDrop); }); }); - it ('check value type validations', { retries: 3 }, function() { + it ('check value type validations', function() { cy.cleanTest(checkBoxGroupDrop).then(function() { // For Number Type dropCheckBoxGroupInContainer(); @@ -137,10 +159,11 @@ describe('Page - Authoring', function () { cy.get('.cmp-adaptiveform-checkboxgroup__value button').click(); cy.get(".cmp-adaptiveform-checkboxgroup__value input").invoke('val', 'Not a Number'); cy.get('.cq-dialog-submit').click(); - cy.get('.coral3-Tooltip--error coral-tooltip-content').should('be.visible').should('have.text', 'Value Type Mismatch'); + cy.get(".cmp-adaptiveform-checkboxgroup__value input[name='./default']").should('have.attr', 'aria-invalid', 'true'); + cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(checkBoxGroupDrop); // For Boolean @@ -153,14 +176,15 @@ describe('Page - Authoring', function () { cy.get('.cmp-adaptiveform-checkboxgroup__value button').click(); cy.get(".cmp-adaptiveform-checkboxgroup__value input").invoke('val', 'Not a Boolean'); cy.get('.cq-dialog-submit').click(); - cy.get('.coral-Form-fielderror').should('be.visible').invoke('attr', 'aria-label').should('eq', 'Value Type Mismatch'); + cy.get(".cmp-adaptiveform-checkboxgroup__value input[name='./default']").should('have.attr', 'aria-invalid', 'true'); + cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(checkBoxGroupDrop); }); }) - it ('check for duplicate enum values', { retries: 3 }, function() { + it ('check for duplicate enum values', function() { cy.cleanTest(checkBoxGroupDrop).then(function() { dropCheckBoxGroupInContainer(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + checkBoxGroupEditPathSelector); @@ -168,18 +192,14 @@ describe('Page - Authoring', function () { cy.get("[data-granite-coral-multifield-name='./enum'] coral-button-label:contains('Add')").should("exist").click({force: true}); cy.get('input[name="./enum"]').last().invoke('val', '0'); cy.get('input[name="./enumNames"]').last().invoke('val', 'Item 3'); - cy.get('.cq-dialog-submit').click().then(() => { - cy.get('.cq-dialog-submit').should('not.exist') - }); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup-item').should('have.length', 2); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup').parent().parent().contains('Item 3'); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup').parent().parent().contains('Item 2'); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup').parent().parent().contains('Item 1').should('not.exist'); + cy.submitConfigureDialog(); + getGroup().find('.cmp-adaptiveform-checkboxgroup-item').should('have.length', 2); + getGroup().should('contain.text', 'Item 3').and('contain.text', 'Item 2').and('not.contain.text', 'Item 1'); cy.deleteComponentByPath(checkBoxGroupDrop); }); }); - it('check rich text support for label', {retries: 2}, function(){ + it('check rich text support for label', function(){ cy.cleanTest(checkBoxGroupDrop).then(function() { dropCheckBoxGroupInContainer(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + checkBoxGroupEditPathSelector); @@ -187,13 +207,12 @@ describe('Page - Authoring', function () { cy.get("div[name='richTextTitle']").should('not.be.visible'); // check rich text selector and see if RTE is visible for title. - cy.get('.cmp-adaptiveform-base__istitlerichtext').should('be.visible').click(); - cy.wait(500); // Add small wait for UI update + cy.get("input[name='./isTitleRichText'][type='checkbox']").check({force: true}).should('be.checked'); cy.get("div[name='richTextTitle']").should('exist').scrollIntoView().should('be.visible'); // check rich text selector and see if RTE is visible for enum names. cy.get(".cmp-adaptiveform-base__richTextEnumNames").first().should('not.be.visible'); - cy.get('.cmp-adaptiveform-base__areOptionsRichText').should('exist').click(); + cy.get("input[name='./areOptionsRichText'][type='checkbox']").check({force: true}).should('be.checked'); cy.get("div[name='richTextEnumNames']").then(($el) => { $el[0].scrollIntoView(); }) @@ -201,33 +220,27 @@ describe('Page - Authoring', function () { cy.get("div[name='richTextEnumNames']").first().should('be.visible'); cy.get(".cmp-adaptiveform-base__richTextEnumNames").first().should('be.visible'); cy.get("input[name='./orientation'][value='vertical']").scrollIntoView().click(); - cy.get('.cq-dialog-submit').click({ force: true }); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup-item').should('have.length',2); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup').parent().parent().contains('Select 1'); - getPreviewIframeBody().find('.cmp-adaptiveform-checkboxgroup').parent().parent().contains('Item 2'); + cy.submitConfigureDialog(); + getGroup().find('.cmp-adaptiveform-checkboxgroup-item').should('have.length',2); + getGroup().should('contain.text', 'Select 1').and('contain.text', 'Item 2'); }); }); - it('check rich text inline editor is present', {retries: 2}, function(){ + it('check rich text inline editor is present', function(){ + configureRichTextOptions(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + checkBoxGroupEditPathSelector); cy.invokeEditableAction("[data-action='EDIT']"); cy.get(".rte-toolbar").should('be.visible'); cy.get('.rte-toolbar-item[title="Close"]').should('be.visible').click(); }); - // adding retry since rule editor sometimes does not open at first try - it('rule editor is working with rich text enum names', {retries: 2}, function () { + it('rule editor is working with rich text enum names', function () { + configureRichTextOptions(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + checkBoxGroupEditPathSelector); cy.get(formsSelectors.ruleEditor.action.editRule).should("exist"); - cy.initializeEventHandlerOnChannel("af-rule-editor-initialized").as("isRuleEditorInitialized"); - cy.wait(1000); cy.get(formsSelectors.ruleEditor.action.editRule).click(); - // click on create option from rule editor header - // cy.get("@isRuleEditorInitialized").its('done').should('equal', true); - cy.wait(1000); - cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.action.createRuleButton).should("be.visible").click(); - cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.action.sideToggleButton + ":first").click(); + cy.createRule(); cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.choiceModels.STATEMENT + " .child-choice-name").should("exist"); cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.choiceModels.STATEMENT + " .child-choice-name").click(); diff --git a/ui.tests/test-module/specs/contentfragment/contentfragment.authoring.cy.js b/ui.tests/test-module/specs/contentfragment/contentfragment.authoring.cy.js index 11d875e5e4..96a7de93ae 100644 --- a/ui.tests/test-module/specs/contentfragment/contentfragment.authoring.cy.js +++ b/ui.tests/test-module/specs/contentfragment/contentfragment.authoring.cy.js @@ -65,13 +65,9 @@ describe('Content Fragment - Authoring', function () { .should("exist"); cy.get("[name='./fragmentPath'] input[role='combobox']").should("exist").clear() - .type("/content/dam/wknd/library/sample-assets/adobe-headquarters", { delay: 0 }).click().then(x => { - cy.get('.cq-dialog-submit').click().then(y => { - cy.get('.cq-dialog-submit').should('not.exist').then(z => { - cy.deleteComponentByPath(cfDrop); - }); - }); - }); + .type("/content/dam/wknd/library/sample-assets/adobe-headquarters", { delay: 0 }).click(); + cy.submitConfigureDialog(); + cy.deleteComponentByPath(cfDrop); } const dropExperienceFragmentInSites = function() { @@ -93,12 +89,8 @@ describe('Content Fragment - Authoring', function () { cy.get("[name='./fragmentVariationPath'] input[role='combobox']").should("exist").clear() .type("/content/experience-fragments/test-experience-fragment/master", { delay: 0 }).click(); cy.get(".foundation-picker-buttonlist button").should("be.visible").click(); - cy.get('.cq-dialog-submit').click().then(y => { - cy.get('.cq-dialog-submit').click(); - cy.get('.cq-dialog-submit').should('not.exist').then(z => { - cy.deleteComponentByPath(xfDrop); - }); - }); + cy.submitConfigureDialog(); + cy.deleteComponentByPath(xfDrop); } context("Open Forms Editor", function () { @@ -110,9 +102,14 @@ describe('Content Fragment - Authoring', function () { beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(cfDrop); }); - it('insert content fragment inside adaptive form container component', { retries: 3 }, function() { + afterEach(function () { + cy.cleanTestFixture(cfDrop); + }); + + it('insert content fragment inside adaptive form container component', function() { cy.cleanTest(cfDrop).then(function() { testContentFragmentBehaviour(cfEditPathSelector, cfDrop, false); }); @@ -123,9 +120,18 @@ describe('Content Fragment - Authoring', function () { context("Open Sites Editor", function () { // we can use these values to log in const pagePath = "/content/core-components-examples/library/adaptive-form/container"; + const cfDrop = pagePath + afConstants.RESPONSIVE_GRID_DEMO_SUFFIX + '/formContainer/contentfragment'; + const xfDrop = pagePath + '/jcr:content/root/responsivegrid/experiencefragment'; beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(cfDrop); + cy.cleanTest(xfDrop); + }); + + afterEach(function () { + cy.cleanTestFixture(cfDrop); + cy.cleanTestFixture(xfDrop); }); it('insert content fragment inside adaptive form container component', function() { diff --git a/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js b/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js index 73a35febf4..1a1e6d9640 100644 --- a/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js +++ b/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js @@ -66,7 +66,7 @@ describe('Page - Authoring', function () { cy.get('[name="./multiSelect"][type="checkbox"]').should("exist").uncheck(); cy.get(".cmp-adaptiveform-dropdown__defaultvalue").should("have.css","display","block"); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(dropdown); } @@ -87,6 +87,11 @@ describe('Page - Authoring', function () { beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(dropdown); + }); + + afterEach(function () { + cy.cleanTestFixture(dropdown); }); it('insert Dropdown in form container', function () { @@ -108,9 +113,10 @@ describe('Page - Authoring', function () { cy.get("coral-selectlist-item").contains('Number').should('be.visible').click({force: true}); cy.get(".cmp-adaptiveform-dropdown__defaultvalue input").invoke('val', 'Not a Number'); cy.get('.cq-dialog-submit').click(); - cy.get('.coral-Form-fielderror').should('be.visible').invoke('attr', 'aria-label').should('eq', 'Value Type Mismatch'); + cy.get(".cmp-adaptiveform-dropdown__defaultvalue input[name='./default']").should('have.attr', 'aria-invalid', 'true'); + cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(dropdown); // For Boolean @@ -121,9 +127,10 @@ describe('Page - Authoring', function () { cy.get("coral-selectlist-item").contains('Boolean').click({force: true}); cy.get(".cmp-adaptiveform-dropdown__defaultvalue input").invoke('val', 'Not a Boolean'); cy.get('.cq-dialog-submit').click(); - cy.get('.coral-Form-fielderror').should('be.visible').invoke('attr', 'aria-label').should('eq', 'Value Type Mismatch'); + cy.get(".cmp-adaptiveform-dropdown__defaultvalue input[name='./default']").should('have.attr', 'aria-invalid', 'true'); + cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(dropdown); }) @@ -160,6 +167,11 @@ describe('Page - Authoring', function () { beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(dropdown); + }); + + afterEach(function () { + cy.cleanTestFixture(dropdown); }); it('insert aem forms DropDown', function () { 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 692056aaa4..6010d9db79 100644 --- a/ui.tests/test-module/specs/telephoneinput/telephoneinput.authoring.cy.js +++ b/ui.tests/test-module/specs/telephoneinput/telephoneinput.authoring.cy.js @@ -52,7 +52,7 @@ describe('Page - Authoring', function () { cy.get("[name='./autocomplete']") .should("exist"); cy.get(bemEditDialog).contains('Validation').click({force:true}); - cy.get('.cq-dialog-cancel').should('be.visible').click({force: true}); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(telephoneInputDrop); } @@ -64,6 +64,11 @@ describe('Page - Authoring', function () { beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(telephoneInputDrop); + }); + + afterEach(function () { + cy.cleanTestFixture(telephoneInputDrop); }); it('insert TelephoneInput in form container', function () { @@ -71,7 +76,7 @@ describe('Page - Authoring', function () { cy.deleteComponentByPath(telephoneInputDrop); }); - it ('open edit dialog of TelephoneInput', { retries: 3 }, function(){ + it ('open edit dialog of TelephoneInput', function(){ cy.cleanTest(telephoneInputDrop).then(function() { testTelephoneInputBehaviour(telephoneInputEditPathSelector, telephoneInputDrop); }); @@ -84,9 +89,9 @@ describe('Page - Authoring', function () { cy.invokeEditableAction("[data-action='CONFIGURE']"); cy.get(bemEditDialog).contains('Validation').click({force: true}).then(() => { cy.get('.cmp-adaptiveform-telephoneinput__validationformat').should('have.value', '^[+][0-9]{0,14}$'); - cy.get('.cmp-adaptiveform-telephoneinput__validationpattern select').select('US Phone Number', {force: true}); + cy.selectCoralOption('.cmp-adaptiveform-telephoneinput__validationpattern', '^[+]1[0-9]{0,10}$'); cy.get('.cmp-adaptiveform-telephoneinput__validationformat').should('have.value', '^[+]1[0-9]{0,10}$'); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(telephoneInputDrop); }); }); @@ -104,36 +109,57 @@ 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). + // Snapshot this shared policy before editing and restore its original formats after each test. telephoneInputPolicyPath = '/conf/core-components-examples/settings/wcm/policies/forms-components-examples/components/form/telephoneinput/default'; + let originalPolicy; beforeEach(function () { - cy.openAuthoring(templateDataPath + ".html"); - cy.resetTelephoneInputDesignPolicy(telephoneInputPolicyPath); + originalPolicy = undefined; + cy.openAuthoring(templateDataPath); + const contextPath = Cypress.env('crx.contextPath') || ''; + cy.request(contextPath + telephoneInputPolicyPath + '.infinity.json').then(({body}) => { + originalPolicy = body; + cy.restoreTelephoneInputDesignPolicy(telephoneInputPolicyPath, {allowedFormat3: body.allowedFormat3}); + }); + }); + + afterEach(function () { + if (originalPolicy) { + cy.restoreTelephoneInputDesignPolicy(telephoneInputPolicyPath, originalPolicy); + } + cy.cleanTestFixture(telephoneInputDrop); }); it('Adding removing patterns from design policy', function () { cy.get(telephoneInputPolicy).click({force: true}); cy.get(bemDesignDialog).contains('Validation patterns').click(); // cy.get('[role="tablist"][orientation="horizontal"] [role="tab"]').eq(2).click(); - cy.get('[name="./allowedFormat3"]').eq(0).click({force: true}); + cy.get('input[name="./allowedFormat3"][type="checkbox"]').uncheck({force: true}).should('not.be.checked'); cy.get("[data-granite-coral-multifield-name='./allowedCustomFormats']").should('be.visible'); cy.get("[data-granite-coral-multifield-name='./allowedCustomFormats'] coral-button-label:contains('Add')").should('exist').click({force: true}); cy.get('[name="./allowedCustomFormats/item0/customFormatKey"]').should('exist').then(() => { - cy.get('[name="./allowedCustomFormats/item0/customFormatKey"]').focus().clear().type(customKey); - cy.get('[name="./allowedCustomFormats/item0/customFormatValue"]').focus().clear().type(customValue); - cy.get(submitBtnSelector).click(); + cy.fillTextField('[name="./allowedCustomFormats/item0/customFormatKey"]', customKey); + cy.fillTextField('[name="./allowedCustomFormats/item0/customFormatValue"]', customValue); + cy.intercept('POST', '**' + telephoneInputPolicyPath + '*', request => { + request.on('response', response => response.setDelay(1000)); + }).as('saveTelephonePolicy'); + cy.get(submitBtnSelector + ':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.cleanTest(telephoneInputDrop); dropTelephoneInputInContainer(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + telephoneInputEditPathSelector); cy.invokeEditableAction("[data-action='CONFIGURE']"); cy.get(bemEditDialog).contains('Validation').click({force: true}).then(() => { cy.get('.cmp-adaptiveform-telephoneinput__validationformat').should('have.value', '^[+][0-9]{0,14}$'); - cy.get('.cmp-adaptiveform-telephoneinput__validationpattern select').select(customKey, {force: true}).then(() => { + cy.selectCoralOption('.cmp-adaptiveform-telephoneinput__validationpattern', customValue).then(() => { cy.get('.cmp-adaptiveform-telephoneinput__validationformat').should('have.value', customValue); - cy.get('.cq-dialog-cancel').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(telephoneInputDrop); }) }); @@ -150,6 +176,11 @@ describe('Page - Authoring', function () { beforeEach(function () { // this is done since cypress session results in 403 sometimes cy.openAuthoring(pagePath); + cy.cleanTest(telephoneInputDrop); + }); + + afterEach(function () { + cy.cleanTestFixture(telephoneInputDrop); }); it('insert aem forms TelephoneInput', function () { @@ -157,7 +188,7 @@ describe('Page - Authoring', function () { cy.deleteComponentByPath(telephoneInputDrop); }); - it('open edit dialog of aem forms TelephoneInput', { retries: 3 }, function() { + it('open edit dialog of aem forms TelephoneInput', function() { cy.cleanTest(telephoneInputDrop).then(function(){ testTelephoneInputBehaviour(telephoneInputEditPathSelector, telephoneInputDrop, true); }); diff --git a/ui.tests/test-module/specs/wizard/wizard.authoring.cy.js b/ui.tests/test-module/specs/wizard/wizard.authoring.cy.js index 339fbd6a91..7339cb326f 100644 --- a/ui.tests/test-module/specs/wizard/wizard.authoring.cy.js +++ b/ui.tests/test-module/specs/wizard/wizard.authoring.cy.js @@ -134,6 +134,10 @@ describe('Page - Authoring', function () { cy.openAuthoring(pagePath); }); + afterEach(function () { + cy.cleanTestFixture(wizardLayoutDrop); + }); + it('runtime library should not be loaded', function() { cy.cleanTest(wizardLayoutDrop).then(function () { cy.intercept('GET', /jcr:content\/guideContainer\/wizard\.html/).as('wizardRequest'); @@ -164,7 +168,7 @@ describe('Page - Authoring', function () { cy.get("[name='./dataRef']").should("exist"); cy.get("[name='./visible']").should("exist"); cy.get("[name='./enabled']").should("exist"); - cy.get('.cq-dialog-cancel').should('be.visible').click({force: true}).then(() => { + cy.cancelConfigureDialog().then(() => { cy.deleteComponentByPath(wizardLayoutDrop); }); }); @@ -174,14 +178,14 @@ describe('Page - Authoring', function () { }); }); - it('verify Navigation Working between tabs in Authoring', {retries: 3}, function () { + it('verify Navigation Working between tabs in Authoring', function () { cy.cleanTest(wizardLayoutDrop).then(function () { dropWizardInContainer(); addComponentInWizard("Adaptive Form Number Input", afConstants.components.forms.resourceType.formnumberinput); addComponentInWizard("Adaptive Form Text Box", afConstants.components.forms.resourceType.formtextinput); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + wizardEditPathSelector); cy.invokeEditableAction(editDialogNavigationPanelSelector); - cy.wait(2000).then(() => { + cy.then(() => { cy.get("table.cmp-panelselector__table").find("tr").should("have.length", 2); // In select panel, text will be in format: : cy.get("table.cmp-panelselector__table tr").eq(0) @@ -200,14 +204,14 @@ describe('Page - Authoring', function () { }); }); - it('open editable toolbar of 2nd wizard panel', {retries: 3}, function () { + it('open editable toolbar of 2nd wizard panel', function () { cy.cleanTest(wizardLayoutDrop).then(function () { dropWizardInContainer(); addComponentInWizard("Adaptive Form Number Input", afConstants.components.forms.resourceType.formnumberinput); addComponentInWizard("Adaptive Form Panel", afConstants.components.forms.resourceType.panelcontainer); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + wizardEditPathSelector); cy.invokeEditableAction(editDialogNavigationPanelSelector); - cy.wait(2000).then(() => { + cy.then(() => { cy.get("table.cmp-panelselector__table").find("tr").should("have.length", 2); cy.get("table.cmp-panelselector__table").find(panelcontainerDataId).find("td").first().should('be.visible').click(); cy.get('body').click(0, 0); @@ -260,15 +264,18 @@ describe('Page - Authoring', function () { cy.openAuthoring(pagePath); }); - it('insert aem forms Wizard', {retries: 3}, function () { + afterEach(function () { + cy.cleanTestFixture(wizardEditPath); + }); + + it('insert aem forms Wizard', function () { cy.cleanTest(wizardEditPath).then(function () { dropWizardInSites(); cy.deleteComponentByPath(wizardEditPath); }); }); - // adding retry, sometimes site editor does not load - it('open edit dialog of aem forms Wizard', {retries: 3}, function () { + it('open edit dialog of aem forms Wizard', function () { cy.cleanTest(wizardEditPath).then(function () { dropWizardInSites(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + wizardEditPathSelector); @@ -281,27 +288,26 @@ describe('Page - Authoring', function () { cy.get("[name='./dataRef']").should("exist"); cy.get("[name='./visible']").should("exist"); cy.get("[name='./enabled']").should("exist"); - cy.get('.cq-dialog-cancel').should('be.visible').click({force: true}).then(() => { + cy.cancelConfigureDialog().then(() => { cy.deleteComponentByPath(wizardEditPath); }) }); }); - it('open editable toolbar of 2nd wizard panel', {retries: 3}, function () { + it('open editable toolbar of 2nd wizard panel', function () { cy.cleanTest(wizardEditPath).then(function () { dropWizardInSites(); addComponentInWizardOfSites("Adaptive Form Number Input", afConstants.components.forms.resourceType.formnumberinput); addComponentInWizardOfSites("Adaptive Form Panel", afConstants.components.forms.resourceType.panelcontainer); - cy.wait(2000).then(() => { - cy.get("#sidepanel-toggle-button").click(); - cy.get('coral-tab[icon="layers"][aria-label="Content Tree"]').click(); - cy.get(`div[data-path='${wizardEditPath}']`).click({force: true}); - cy.get(`div[data-path='${panelcontainerPath}']`).click({force: true}); - cy.get('#EditableToolbar').should('be.visible'); - cy.get('body').click(0, 0); - cy.deleteComponentByPath(wizardEditPath); - }); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + wizardEditPathSelector); + cy.invokeEditableAction(navigationPanelSelector); + cy.get("table.cmp-panelselector__table:visible").find(panelcontainerDataId).find("td").first() + .should('be.visible').click(); + cy.get('body').click(0, 0); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + panelcontainerDataPath); + cy.get('#EditableToolbar button' + panelcontainerDataPath).should('be.visible'); + cy.deleteComponentByPath(wizardEditPath); }); }); From 9c0661f94cc926abb854d4073870115d733af6c1 Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Tue, 6 Oct 2026 15:28:06 +0530 Subject: [PATCH 02/10] fix(tests): honor layer readiness timeout during polling Reproduce the early iteration-limit failure with a delayed layer option and allow polling throughout the existing ten-second timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ui.tests/test-module/README.md | 2 +- ui.tests/test-module/libs/support/commands.js | 2 +- .../specs/authoring/authoring.synchronization.cy.js | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ui.tests/test-module/README.md b/ui.tests/test-module/README.md index a553e9e5fb..c5d8d948e0 100644 --- a/ui.tests/test-module/README.md +++ b/ui.tests/test-module/README.md @@ -51,7 +51,7 @@ commit them. For an ngrok browser-warning endpoint, a non-browser `userAgent` override can be supplied together with request login. The synchronization spec injects a delayed overlay, lost Insert and layer-switcher -clicks, and delayed insertion/configure responses. Component insertion must finish the +clicks, late layer options, and delayed insertion/configure responses. Component insertion must finish the editor refresh before a caller configures or deletes the new component. Use `cleanTest` after entering the Edit layer, `cancelConfigureDialog` for configure-dialog cancellation, and `submitConfigureDialog` when a save must diff --git a/ui.tests/test-module/libs/support/commands.js b/ui.tests/test-module/libs/support/commands.js index 7fa389a224..aae607ea2f 100644 --- a/ui.tests/test-module/libs/support/commands.js +++ b/ui.tests/test-module/libs/support/commands.js @@ -372,7 +372,7 @@ Cypress.Commands.add("selectLayer", (layer) => { return cy.get('body'); }, $body => $body.find(layerOption).is(':visible'), - {limit: 5, delay: 500, timeout: 10000, log: false} + {limit: 21, delay: 500, timeout: 10000, log: false} ); cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isLayerOverlaysRepositioned"); cy.get(layerOption).click({force: true}); diff --git a/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js b/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js index 72d907d696..4852f3cf87 100644 --- a/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js +++ b/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js @@ -75,6 +75,11 @@ describe('Authoring synchronization', {retries: 0}, function () { cy.get(sitesSelectors.selectLayer.popover.self).should('not.be.visible'); let lostClick = false; cy.document().then(doc => { + const option = doc.querySelector(sitesSelectors.selectLayer.popover.edit); + const parent = option.parentNode; + const next = option.nextSibling; + option.remove(); + setTimeout(() => parent.insertBefore(option, next), 4500); const discardFirstClick = event => { if (event.target.closest(sitesSelectors.selectLayer.trigger)) { lostClick = true; From 37862042dcf04836e0345112be27919b8378e463 Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Tue, 6 Oct 2026 15:46:01 +0530 Subject: [PATCH 03/10] test: remove standalone authoring synchronization spec Keep the component fixes and shared helpers, remove the synthetic synchronization suite as requested, and update the documented spec selection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ui.tests/test-module/README.md | 7 +- .../authoring/authoring.synchronization.cy.js | 111 ------------------ 2 files changed, 3 insertions(+), 115 deletions(-) delete mode 100644 ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js diff --git a/ui.tests/test-module/README.md b/ui.tests/test-module/README.md index c5d8d948e0..12c3ff18f1 100644 --- a/ui.tests/test-module/README.md +++ b/ui.tests/test-module/README.md @@ -37,7 +37,7 @@ Override `baseUrl` rather than changing the committed localhost configuration: ```sh npx --no-install cypress run --browser chrome --headless \ - --spec 'specs/authoring/authoring.synchronization.cy.js,specs/wizard/wizard.authoring.cy.js,specs/checkboxgroup/checkboxgroup.authoring.cy.js,specs/dropdown/dropdown.authoring.cy.js,specs/contentfragment/contentfragment.authoring.cy.js,specs/telephoneinput/telephoneinput.authoring.cy.js' \ + --spec 'specs/wizard/wizard.authoring.cy.js,specs/checkboxgroup/checkboxgroup.authoring.cy.js,specs/dropdown/dropdown.authoring.cy.js,specs/contentfragment/contentfragment.authoring.cy.js,specs/telephoneinput/telephoneinput.authoring.cy.js' \ --config 'baseUrl=https://author.example,retries=0,video=false' \ --env 'crx.loginViaRequest=true' ``` @@ -50,9 +50,8 @@ settings. Supply credentials using Cypress environment configuration; do not commit them. For an ngrok browser-warning endpoint, a non-browser `userAgent` override can be supplied together with request login. -The synchronization spec injects a delayed overlay, lost Insert and layer-switcher -clicks, late layer options, and delayed insertion/configure responses. Component insertion must finish the -editor refresh before a caller configures or deletes the new component. +Component insertion must finish the editor refresh before a caller configures +or deletes the new component. Use `cleanTest` after entering the Edit layer, `cancelConfigureDialog` for configure-dialog cancellation, and `submitConfigureDialog` when a save must finish before the next toolbar action. Select classic Coral options through diff --git a/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js b/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js deleted file mode 100644 index 4852f3cf87..0000000000 --- a/ui.tests/test-module/specs/authoring/authoring.synchronization.cy.js +++ /dev/null @@ -1,111 +0,0 @@ -/******************************************************************************* - * Copyright 2026 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. - ******************************************************************************/ - -/* global cy, Cypress, expect */ -const sitesSelectors = require('../../libs/commons/sitesSelectors'); -const constants = require('../../libs/commons/formsConstants'); - -describe('Authoring synchronization', {retries: 0}, function () { - const page = '/content/forms/af/core-components-it/blank'; - const container = page + constants.FORM_EDITOR_FORM_CONTAINER_SUFFIX; - const component = container + '/telephoneinput'; - const overlay = sitesSelectors.overlays.overlay.component + '[data-path=\'' + component + '\']'; - const insert = () => cy.insertComponent( - sitesSelectors.overlays.overlay.component + '[data-path=\'' + container + '/*\']', - 'Adaptive Form Telephone input', - constants.components.forms.resourceType.formtelephoneinput - ); - - beforeEach(function () { - cy.openAuthoring(page); - cy.cleanTest(component); - }); - - afterEach(function () { - cy.cleanTestFixture(component); - }); - - it('cleans persisted fixtures while their overlay is still rendering', function () { - insert(); - cy.get(overlay).then($overlay => { - const parent = $overlay.parent(); - $overlay.detach(); - setTimeout(() => parent.append($overlay), 2000); - }); - cy.cleanTest(component); - cy.request({url: (Cypress.env('crx.contextPath') || '') + component + '.json', failOnStatusCode: false}) - .its('status').should('equal', 404); - }); - - it('recovers a lost Insert click without inserting twice', function () { - let lostClick = false; - cy.document().then(doc => { - const discardFirstClick = event => { - if (event.target.closest('[data-action="INSERT"]')) { - lostClick = true; - event.preventDefault(); - event.stopImmediatePropagation(); - doc.removeEventListener('click', discardFirstClick, true); - } - }; - doc.addEventListener('click', discardFirstClick, true); - }); - insert(); - cy.then(() => expect(lostClick, 'injected lost Insert click').to.equal(true)); - cy.get(overlay).should('have.length', 1); - cy.getContentIFrameBody().find('.cmp-adaptiveform-telephoneinput').should('have.length', 1); - }); - - it('recovers a lost layer-switcher opening click', function () { - cy.selectLayer('Layouting'); - cy.get('body').click(0, 0); - cy.get(sitesSelectors.selectLayer.popover.self).should('not.be.visible'); - let lostClick = false; - cy.document().then(doc => { - const option = doc.querySelector(sitesSelectors.selectLayer.popover.edit); - const parent = option.parentNode; - const next = option.nextSibling; - option.remove(); - setTimeout(() => parent.insertBefore(option, next), 4500); - const discardFirstClick = event => { - if (event.target.closest(sitesSelectors.selectLayer.trigger)) { - lostClick = true; - event.preventDefault(); - event.stopImmediatePropagation(); - doc.removeEventListener('click', discardFirstClick, true); - } - }; - doc.addEventListener('click', discardFirstClick, true); - }); - cy.selectLayer('Edit'); - cy.then(() => expect(lostClick, 'injected lost layer-switcher click').to.equal(true)); - cy.get(sitesSelectors.selectLayer.current + '[data-layer="Edit"].is-selected').should('be.visible'); - }); - - it('waits for delayed insertion and configure refreshes before deleting', function () { - cy.intercept('POST', '**' + container + '/*', request => { - request.on('response', response => response.setDelay(1000)); - }); - insert(); - cy.openEditableToolbar(overlay); - cy.invokeEditableAction('[data-action="CONFIGURE"]'); - cy.fillTextField('input[name="./jcr:title"]', 'Synchronized telephone'); - cy.submitConfigureDialog(); - cy.request((Cypress.env('crx.contextPath') || '') + component + '.json') - .its('body.jcr:title').should('equal', 'Synchronized telephone'); - cy.deleteComponentByPath(component); - }); -}); From bb58b09e21bb8763d25bb0dae0825e14cc54196a Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Tue, 6 Oct 2026 16:15:50 +0530 Subject: [PATCH 04/10] fix(tests): select classic Coral TextInput validation patterns Use Coral option items and the shared selection helper instead of a nativeSelect that is absent on release/650. Isolate the custom-regex case and wait for configure cancellation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../specs/textinput/textinput.authoring.cy.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ui.tests/test-module/specs/textinput/textinput.authoring.cy.js b/ui.tests/test-module/specs/textinput/textinput.authoring.cy.js index b0b440164f..c255ce53aa 100644 --- a/ui.tests/test-module/specs/textinput/textinput.authoring.cy.js +++ b/ui.tests/test-module/specs/textinput/textinput.authoring.cy.js @@ -214,6 +214,7 @@ describe('Page - Authoring', function () { it('should switch validation pattern dropdown to "Custom" when an unmapped regex is authored', function () { const customValidationFormatValue = '^custom-regex-[0-9]{3}$'; + cy.cleanTest(textInputDrop); dropTextInputInContainer(); cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textInputEditPathSelector); cy.invokeEditableAction("[data-action='CONFIGURE']"); @@ -230,17 +231,16 @@ describe('Page - Authoring', function () { // Pick a non-default option (not empty, not "custom") so the format field is shown and synced. let chosenValue; - cy.get('@validationDropdown').find('select[handle="nativeSelect"]').then(($nativeSelect) => { - const options = Array.from($nativeSelect[0]?.options || []); + cy.get('@validationDropdown').find('coral-select-item[value]').then(($options) => { + const options = Array.from($options); const chosen = options.find((opt) => - Boolean(opt.value) && - opt.value !== 'custom' && - opt.value !== '#####################.###############' + Boolean(opt.getAttribute('value')) && + opt.getAttribute('value') !== 'custom' && + opt.getAttribute('value') !== '#####################.###############' ); expect(chosen, 'non-default validation pattern option').to.exist; - chosenValue = chosen.value; - cy.wrap($nativeSelect).select(chosenValue, { force: true }); - cy.wrap($nativeSelect).trigger('change', { force: true }); + chosenValue = chosen.getAttribute('value'); + cy.selectCoralOption('.cmp-adaptiveform-textinput__validationpattern', chosenValue); }); cy.then(() => { cy.get('@validationDropdown').should('have.value', chosenValue); @@ -260,7 +260,7 @@ describe('Page - Authoring', function () { cy.get('.cq-dialog').click(5, 5, { force: true }); cy.get('@validationDropdown').should('have.value', 'custom'); cy.get('@validationDropdown').children('button').should('contain.text', 'Custom'); - cy.get('.cq-dialog-cancel').should('be.visible').click(); + cy.cancelConfigureDialog(); cy.deleteComponentByPath(textInputDrop); }); }) From d3ef7a4f651bb5ac0f472f04669f27d3df2f4622 Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Tue, 6 Oct 2026 16:19:33 +0530 Subject: [PATCH 05/10] fix(tests): finish Button inline editing before deletion Wait for the inline toolbar to open and close using bounded outside-click recovery before reopening the editable toolbar. Assert the actual preview label and remove whole-test retries from the reported case. Live validation remains blocked by the offline supplied endpoint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../specs/button/button.authoring.cy.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/ui.tests/test-module/specs/button/button.authoring.cy.js b/ui.tests/test-module/specs/button/button.authoring.cy.js index 1388b5a7af..a70ad17765 100644 --- a/ui.tests/test-module/specs/button/button.authoring.cy.js +++ b/ui.tests/test-module/specs/button/button.authoring.cy.js @@ -17,6 +17,7 @@ const sitesSelectors = require('../../libs/commons/sitesSelectors'), afConstants = require('../../libs/commons/formsConstants'); +const {recurse} = require('cypress-recurse'); /** * Testing Form Button with Sites Editor @@ -80,13 +81,26 @@ describe('Button - Authoring', function () { dropButtonInContainer(); } cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); - cy.invokeEditableAction("[data-action='EDIT']").then(() => { - getContentIframeBody().find('.cmp-adaptiveform-button__text').then(($span) => { + cy.invokeEditableAction("[data-action='EDIT']"); + cy.get('.rte-toolbar:visible').should('have.length', 1); + getContentIframeBody().find('.cmp-adaptiveform-button__text').should('have.length', 1) + .then(($span) => { $span[0].textContent = ''; }); - cy.get('body').click(0,0); - cy.get("[data-path='/content/forms/af/core-components-it/blank/jcr:content/guideContainer/button']").should('exist').should('not.have.text', ''); - }) + recurse( + () => { + cy.get('body').then($body => { + if ($body.find('.rte-toolbar:visible').length) { + cy.get('body').click(0, 0); + } + }); + return cy.get('body'); + }, + $body => !$body.find('.rte-toolbar:visible').length, + {limit: 21, delay: 500, timeout: 10000, log: false} + ); + getContentIframeBody().find('.cmp-adaptiveform-button__text') + .should('have.length', 1).and('not.have.text', ''); }; @@ -111,7 +125,7 @@ describe('Button - Authoring', function () { }); }); - it ('open Inline edit dialog of Button',{ retries: 3 }, function(){ + it ('open Inline edit dialog of Button', function(){ cy.cleanTest(buttonDrop).then(function(){ testButtonBehaviourInilineEdit(buttonEditPathSelector, buttonDrop); cy.deleteComponentByPath(buttonDrop); From 63685e3ded109f3db73972db1c6e1e74d79631c6 Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Tue, 6 Oct 2026 17:21:53 +0530 Subject: [PATCH 06/10] fix(tests): include AEM referrer on authenticated POSTs Preserve same-origin referrer metadata for awaited preferences, policy restoration and fixture cleanup requests. Validate all three paths with normal Chrome user agent against a controlled rejection fixture; keep HTTP errors visible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ui.tests/test-module/README.md | 6 +++++- ui.tests/test-module/libs/support/commands.js | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ui.tests/test-module/README.md b/ui.tests/test-module/README.md index 12c3ff18f1..ce7b3fd2ca 100644 --- a/ui.tests/test-module/README.md +++ b/ui.tests/test-module/README.md @@ -64,8 +64,12 @@ assuming its first Create click is handled while the iframe initializes. Use `cleanTestFixture` in teardown to remove only the named test-owned component through authenticated requests, even if the editor or configure dialog failed. Tutorial preferences are saved through awaited requests; request-login authoring -does not depend on the landing-page redirect. +does not depend on the landing-page redirect. Authenticated preferences, policy, +and fixture-cleanup POSTs include both the CSRF token and the configured AEM +`baseUrl` as their referrer, including with the default browser user agent. Repeat stability runs with retries disabled, including per-test retry overrides. Verify that test-owned components are removed and the telephone policy matches its original snapshot after both passing and failing runs. +Use the default browser user agent for acceptance: a non-browser user agent can +bypass Sling's browser referrer checks and mask requests that fail in CI. diff --git a/ui.tests/test-module/libs/support/commands.js b/ui.tests/test-module/libs/support/commands.js index aae607ea2f..b0001e3a0d 100644 --- a/ui.tests/test-module/libs/support/commands.js +++ b/ui.tests/test-module/libs/support/commands.js @@ -112,6 +112,10 @@ function getUserInfoHome(contextPath) { cy.request(USER_INFO_SERVLET).its('body.home').as("home") } +function getAuthenticatedRequestHeaders(token) { + return {'CSRF-Token': token, Referer: Cypress.config('baseUrl')}; +} + // Cypress command to open authoring page Cypress.Commands.add("enableOrDisableTutorials", (enable) => { @@ -151,7 +155,7 @@ Cypress.Commands.add("enableOrDisableTutorials", (enable) => { method: 'POST', url, form: true, - headers: {'CSRF-Token': preferences[":cq_csrf_token"]}, + headers: getAuthenticatedRequestHeaders(preferences[":cq_csrf_token"]), body: preferences, log: false }); @@ -166,7 +170,7 @@ Cypress.Commands.add("restoreTelephoneInputDesignPolicy", (policyPath, policy) = const contextPath = Cypress.env('crx.contextPath') ? Cypress.env('crx.contextPath') : ""; const url = contextPath + policyPath; cy.request(contextPath + '/libs/granite/csrf/token.json').its('body.token').then((token) => { - const options = {method: 'POST', url, form: true, headers: {'CSRF-Token': token}, log: false}; + const options = {method: 'POST', url, form: true, headers: getAuthenticatedRequestHeaders(token), log: false}; cy.request({...options, body: { 'allowedCustomFormats@Delete': '', 'allowedFormat3@Delete': '', @@ -725,7 +729,7 @@ Cypress.Commands.add("cleanTestFixture", (componentPath) => { .to.equal(componentPath.split('/').pop()); cy.request({url: contextPath + '/libs/granite/csrf/token.json', log: false}).its('body.token').then(token => { cy.request({ - method: 'POST', url, form: true, headers: {'CSRF-Token': token}, + method: 'POST', url, form: true, headers: getAuthenticatedRequestHeaders(token), body: {':operation': 'delete', ':cq_csrf_token': token}, log: false }); }); From f7ecd58dcebd832628d102cfc8e6b52cbbefc08a Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Tue, 6 Oct 2026 19:54:08 +0530 Subject: [PATCH 07/10] Fix SP and LTS authoring readiness and validation regressions Use selected layer state and public Granite validation accessors. Preserve Button empty-caption coverage and verify inline persistence. Document normal-browser remote setup and compatibility checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 2 + docs/e2e-testing/authoring-compatibility.md | 119 ++++++++++++++++++ ui.tests/test-module/README.md | 11 +- ui.tests/test-module/libs/support/commands.js | 30 +++-- .../specs/button/button.authoring.cy.js | 57 ++++++--- .../checkboxgroup.authoring.cy.js | 6 +- .../specs/dropdown/dropdown.authoring.cy.js | 6 +- 7 files changed, 194 insertions(+), 37 deletions(-) create mode 100644 docs/e2e-testing/authoring-compatibility.md diff --git a/CLAUDE.md b/CLAUDE.md index 9d8ba79973..b9161e8eb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,5 +13,7 @@ AEM Core Forms Components — Adaptive Forms v2 component library. When a change needs a new component version (`v1` → `v2`) vs. a fix-in-place, and the full checklist of what to update when bumping a version (clientlib self-containment, `_cq_dialog`/`_cq_styleConfig` inheritance, `pom.xml`, runtime clientlib embed lists, etc.). ### E2E Testing +- **SP/LTS Authoring Compatibility**: [`docs/e2e-testing/authoring-compatibility.md`](docs/e2e-testing/authoring-compatibility.md) + Stable selectors and Granite validation APIs, operation-specific waits, referrer requirements, normal-browser ngrok setup, and first-pass validation before repetition. - **Feature Toggle Tests**: [`docs/e2e-testing/feature-toggles.md`](docs/e2e-testing/feature-toggles.md) How to add Cypress e2e tests for new feature toggles: OSGi config changes, system property wiring, and the isLatestAddon + fetchFeatureToggles test pattern. diff --git a/docs/e2e-testing/authoring-compatibility.md b/docs/e2e-testing/authoring-compatibility.md new file mode 100644 index 0000000000..988d2a8ab6 --- /dev/null +++ b/docs/e2e-testing/authoring-compatibility.md @@ -0,0 +1,119 @@ +# Authoring test compatibility: 6.5 SP and LTS + +Authoring assertions must describe the field or editor behavior, not the markup +of one Coral implementation. A passing repetition on one server does not prove +compatibility with another AEM service pack or LTS instance. + +## Selectors and validation + +- Scope fields through our component classes and field names, for example + `.cmp-adaptiveform-checkboxgroup__value input[name='./default']`. +- Avoid internal presentation classes such as `.coral3-Search-input`, + `.coral-Form-errorlabel`, and `.coral-Form-fielderror`. Error UI can be a + tooltip, an icon, or an inline label. Covered or hidden error UI is not proof + that the field is valid. +- Use `cy.assertFieldInvalid(selector, message)` to require one field, + `aria-invalid="true"`, and the expected Granite validation message. It reads + the validation state after submission; it does not manufacture an error. +- The served AEM 6.5.25.0 `foundation-validation` adapter exposes + `getValidationMessage()`. The published Granite API also describes + `validationMessage()`. The shared command checks the available accessor + instead of branching on an AEM version string. A missing adapter, unsupported + accessor, ambiguous field, or unexpected message must fail explicitly. +- Use public Coral elements and values for selection through + `cy.selectCoralOption()`. Do not assume every `coral-select` has a native + `select[handle="nativeSelect"]`. +- Scope component insertion to the visible Insert dialog and require a unique + visible native input. Use the public dialog `open` property rather than + Coral's internal state classes when checking for a leftover dialog. + +The [Granite validation reference](https://developer.adobe.com/experience-manager/reference-materials/6-5/granite-ui/api/jcr_root/libs/granite/ui/components/coral/foundation/clientlibs/foundation/js/validation/index.html) +describes the adapter contract. Check the served clientlib when its API differs +from the reference; do not assume a documented method exists on every instance. + +## Layer selection and synchronization + +`cy.selectLayer()` waits for the requested layer to be visibly selected. +An overlay-reposition event is not an unconditional layer-selection contract. +Requiring it caused existing tests to time out on both SP and LTS even after +the selected-layer assertion succeeded. + +Keep operation-specific refresh waits for insertion, save, and deletion. +An event wait justified for one operation must not be imposed on every editor +transition. Test an actual layer change as well as an already-selected layer: +the latter returns early and cannot validate a transition wait. + +## Button inline editing + +An empty caption can remove both `jcr:title` and the rendered caption span. +Do not require a nonempty span after explicitly clearing the label. The inline +test saves a nonempty caption and verifies preview text and persisted title, +then repeats the original empty-label edit and verifies the empty native button +and absent/empty title before deletion. Both edits wait for the inline toolbar +to close. API teardown removes only the named test fixture if an assertion fails. + +## Authentication and browser user agents + +Use the normal browser user agent for acceptance. Sling's referrer filter +classifies browser requests using `Mozilla`/`Opera` in the user agent; changing +it to a non-browser value can hide a missing-referrer POST regression. + +Authenticated preferences, policy restoration, and fixture cleanup POSTs need +the session, CSRF token, and same-server referrer. Request-based login also +includes the configured `baseUrl` as its referrer. Await requests and preserve +HTTP failures; do not set `failOnStatusCode: false` to make rejected writes pass. + +## Remote ngrok runs with normal Chrome + +An ngrok browser warning can return HTTP 200 HTML instead of AEM JSON or +JavaScript (`ERR_NGROK_6024`). A missing `body.token` or JavaScript +`Unexpected token '<'` can therefore be a tunnel response, not an AEM selector +failure. Inspect the response content type before changing application code. + +For temporary remote support, forward +`ngrok-skip-browser-warning: true` on the initial `cy.visit()` and node-side +`cy.request()` calls. Browser subresources need the header too. The following +Chrome-only support hook was used successfully on the remote SP instance: + +```js +before(() => { + cy.then(() => Cypress.automation('remote:debugger:protocol', { + command: 'Network.enable' + }).then(() => Cypress.automation('remote:debugger:protocol', { + command: 'Network.setExtraHTTPHeaders', + params: {headers: {'ngrok-skip-browser-warning': 'true'}} + }))); +}); +``` + +This is tunnel-specific support, not a replacement for the normal CI browser +configuration. Broad middleware interception introduced while debugging stalled +the remote runner; removing it and setting the native browser header allowed the +same Text case to complete. Do not diagnose an AEM `visit()` problem solely from +a stalled custom runner. + +When inspecting Chrome, use the test browser's actual +`--remote-debugging-port`. The first `DevTools listening` line can belong to +Cypress's Electron process, not the test Chrome. An empty target list from the +wrong debugger does not show that the test browser has no page. + +## Acceptance order + +1. Verify the actual product version from + `/system/console/status-productinfo.txt` and the active component bundles. + A Core Components bundle version does not distinguish SP from LTS. +2. Capture fixture and policy baselines; preserve unrelated authored content. +3. Run every reported failing case once with global and per-test retries + disabled. Verify selected cases were collected and actually passed. +4. Include earlier reported cases when changing shared helpers. New-case passes + are not evidence that the original fixes still work. +5. Publish the verified fixes, then run the entire configured Cypress suite. + In `release/650`, the current inventory is 153 JavaScript specs. +6. Verify cleanup/restoration and obtain results for each CI variant. Record + pending, unsupported, skipped-after-hook, and retry-assisted results + separately from clean passes. + +A helper contract check against both accessor shapes is not a live LTS test. +Finite repetitions cannot guarantee that a race never recurs. Keep the PR +evidence explicit about the server, source revision, selected cases, and +remaining validation gaps. diff --git a/ui.tests/test-module/README.md b/ui.tests/test-module/README.md index ce7b3fd2ca..fb7a99b8ca 100644 --- a/ui.tests/test-module/README.md +++ b/ui.tests/test-module/README.md @@ -47,8 +47,10 @@ Enable it when an external author endpoint requires a session before navigation or its UI login redirects escape the Cypress frame. It uses AEM's form-login endpoint and the existing `crx.username`, `crx.password`, and `crx.contextPath` settings. Supply credentials using Cypress environment configuration; do not -commit them. For an ngrok browser-warning endpoint, a non-browser `userAgent` -override can be supplied together with request login. +commit them. For an ngrok browser-warning endpoint, send its warning-bypass +header without replacing the normal browser user agent. See +[SP/LTS authoring compatibility](../../docs/e2e-testing/authoring-compatibility.md) +for the verified remote setup and cross-version assertion patterns. Component insertion must finish the editor refresh before a caller configures or deletes the new component. @@ -61,6 +63,11 @@ CSS classes. Use `createRule` to wait for the rule editor's statement builder, rather than assuming its first Create click is handled while the iframe initializes. +Layer selection waits for the selected-layer UI state, not an overlay-reposition +event that every layer transition may not emit. Use `assertFieldInvalid` to +check the field's invalid state and Granite validation message without depending +on Coral's version-specific tooltip or error-label markup. + Use `cleanTestFixture` in teardown to remove only the named test-owned component through authenticated requests, even if the editor or configure dialog failed. Tutorial preferences are saved through awaited requests; request-login authoring diff --git a/ui.tests/test-module/libs/support/commands.js b/ui.tests/test-module/libs/support/commands.js index b0001e3a0d..7e3061a4f4 100644 --- a/ui.tests/test-module/libs/support/commands.js +++ b/ui.tests/test-module/libs/support/commands.js @@ -68,6 +68,7 @@ Cypress.Commands.add("login", (pagePath, failurehandler = () => {}) => { method: 'POST', url: contextPath + '/libs/granite/core/content/login.html/j_security_check', form: true, + headers: {Referer: Cypress.config('baseUrl')}, body: {j_username: username, j_password: password, j_validate: 'true', resource: contextPath + '/index.html'}, followRedirect: false, log: false @@ -378,10 +379,8 @@ Cypress.Commands.add("selectLayer", (layer) => { $body => $body.find(layerOption).is(':visible'), {limit: 21, delay: 500, timeout: 10000, log: false} ); - cy.initializeEventHandlerOnChannel(siteConstants.EVENT_NAME_OVERLAYS_REPOSITIONED).as("isLayerOverlaysRepositioned"); cy.get(layerOption).click({force: true}); - cy.get(selectedLayer).should('be.visible'); - return cy.get("@isLayerOverlaysRepositioned").its('done').should('equal', true); + return cy.get(selectedLayer).should('be.visible'); }); }); @@ -420,6 +419,21 @@ Cypress.Commands.add("invokeEditableAction", (actionSelector) => { cy.get(actionSelector).should('be.visible').click({force: true}); }); +Cypress.Commands.add("assertFieldInvalid", (selector, message) => { + return cy.window().then(win => { + return cy.get(selector).should($field => { + expect($field, 'unique validation field').to.have.length(1); + expect($field).to.have.attr('aria-invalid', 'true'); + const validation = win.$($field[0]).adaptTo('foundation-validation'); + expect(validation, 'Granite field validation API').to.exist; + const messageAccessor = typeof validation.getValidationMessage === 'function' + ? validation.getValidationMessage : validation.validationMessage; + expect(messageAccessor, 'Granite validation message accessor').to.be.a('function'); + expect(messageAccessor.call(validation), 'field validation message').to.equal(message); + }); + }); +}); + Cypress.Commands.add("cancelConfigureDialog", () => { return recurse( () => { @@ -781,7 +795,8 @@ Cypress.Commands.add("deleteComponentByPath", (componentPath) => { // failure screenshot showing the edit dialog still open when the delete-confirm dialog was // expected). Wait for any leftover open dialog to actually close first. cy.get('body').should($body => { - expect($body.find('coral-dialog.is-open:visible').length, 'no leftover open dialog before delete').to.equal(0); + const openDialogs = $body.find('coral-dialog').filter((index, dialog) => dialog.open); + expect(openDialogs.filter(':visible').length, 'no leftover open dialog before delete').to.equal(0); }); // open editable toolbar cy.openEditableToolbar(siteSelectors.overlays.overlay.component + componentPathSelector); @@ -817,7 +832,8 @@ Cypress.Commands.add("deleteComponentByTitle", (title) => { cy.initializeEventHandlerOnChannel(overlayRepositionEvent).as("isOverlayRepositionEventComplete"); // Same leftover-open-dialog race as deleteComponentByPath above; wait for it to actually close. cy.get('body').should($body => { - expect($body.find('coral-dialog.is-open:visible').length, 'no leftover open dialog before delete').to.equal(0); + const openDialogs = $body.find('coral-dialog').filter((index, dialog) => dialog.open); + expect(openDialogs.filter(':visible').length, 'no leftover open dialog before delete').to.equal(0); }); // open editable toolbar cy.openEditableToolbar(siteSelectors.overlays.overlay.component + componentPathSelector); @@ -836,7 +852,7 @@ Cypress.Commands.add("deleteComponentByTitle", (title) => { Cypress.Commands.add("insertComponent", (selector, componentString, componentType) => { const dialog = '.InsertComponentDialog:visible', insertComponentDialog_Selector = dialog + ' [value="' + componentType + '"]', - insertComponentDialog_searchField = dialog + " .coral3-Search-input, " + dialog + " input[type='search']"; + insertComponentDialog_searchField = dialog + " input:not([type='hidden']):visible"; recurse( () => { cy.get('body').then($body => { @@ -853,7 +869,7 @@ Cypress.Commands.add("insertComponent", (selector, componentString, componentTyp recurse( // the commands to repeat, and they yield the input element () => { - cy.get(insertComponentDialog_searchField).should('be.visible').clear(); + cy.get(insertComponentDialog_searchField).should('have.length', 1).and('be.visible').clear(); return cy.get(insertComponentDialog_searchField).type(componentString); }, // the predicate takes the output of the above commands diff --git a/ui.tests/test-module/specs/button/button.authoring.cy.js b/ui.tests/test-module/specs/button/button.authoring.cy.js index a70ad17765..b84c25c110 100644 --- a/ui.tests/test-module/specs/button/button.authoring.cy.js +++ b/ui.tests/test-module/specs/button/button.authoring.cy.js @@ -75,32 +75,45 @@ describe('Button - Authoring', function () { } const testButtonBehaviourInilineEdit = function(buttonEditPathSelector, buttonDrop, isSites) { + const updatedLabel = 'Updated Button'; if (isSites) { dropButtonInSites(); } else { dropButtonInContainer(); } - cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); - cy.invokeEditableAction("[data-action='EDIT']"); - cy.get('.rte-toolbar:visible').should('have.length', 1); - getContentIframeBody().find('.cmp-adaptiveform-button__text').should('have.length', 1) - .then(($span) => { - $span[0].textContent = ''; - }); - recurse( - () => { - cy.get('body').then($body => { - if ($body.find('.rte-toolbar:visible').length) { - cy.get('body').click(0, 0); - } + const contextPath = Cypress.env('crx.contextPath') || ''; + [updatedLabel, ''].forEach(label => { + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); + cy.invokeEditableAction("[data-action='EDIT']"); + cy.get('.rte-toolbar:visible').should('have.length', 1); + getContentIframeBody().find('.cmp-adaptiveform-button__text').should('have.length', 1) + .then(($span) => { + $span[0].textContent = label; }); - return cy.get('body'); - }, - $body => !$body.find('.rte-toolbar:visible').length, - {limit: 21, delay: 500, timeout: 10000, log: false} - ); - getContentIframeBody().find('.cmp-adaptiveform-button__text') - .should('have.length', 1).and('not.have.text', ''); + recurse( + () => { + cy.get('body').then($body => { + if ($body.find('.rte-toolbar:visible').length) { + cy.get('body').click(0, 0); + } + }); + return cy.get('body'); + }, + $body => !$body.find('.rte-toolbar:visible').length, + {limit: 21, delay: 500, timeout: 10000, log: false} + ); + getContentIframeBody().find('.cmp-adaptiveform-button button').should($button => { + expect($button).to.have.length(1); + expect($button.text().trim()).to.equal(label); + }); + cy.request(contextPath + buttonDrop + '.json').its('body').should(body => { + if (label) { + expect(body['jcr:title']).to.equal(label); + } else { + expect([undefined, '']).to.include(body['jcr:title']); + } + }); + }); }; @@ -114,6 +127,10 @@ describe('Button - Authoring', function () { cy.openAuthoring(pagePath); }); + afterEach(function () { + cy.cleanTestFixture(buttonDrop); + }); + it('insert Button in form container', function () { dropButtonInContainer(); cy.deleteComponentByPath(buttonDrop); diff --git a/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js b/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js index 6ad4c1f5d7..dc3953a290 100644 --- a/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js +++ b/ui.tests/test-module/specs/checkboxgroup/checkboxgroup.authoring.cy.js @@ -159,8 +159,7 @@ describe('Page - Authoring', function () { cy.get('.cmp-adaptiveform-checkboxgroup__value button').click(); cy.get(".cmp-adaptiveform-checkboxgroup__value input").invoke('val', 'Not a Number'); cy.get('.cq-dialog-submit').click(); - cy.get(".cmp-adaptiveform-checkboxgroup__value input[name='./default']").should('have.attr', 'aria-invalid', 'true'); - cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); + cy.assertFieldInvalid(".cmp-adaptiveform-checkboxgroup__value input[name='./default']", 'Value Type Mismatch'); cy.cancelConfigureDialog(); @@ -176,8 +175,7 @@ describe('Page - Authoring', function () { cy.get('.cmp-adaptiveform-checkboxgroup__value button').click(); cy.get(".cmp-adaptiveform-checkboxgroup__value input").invoke('val', 'Not a Boolean'); cy.get('.cq-dialog-submit').click(); - cy.get(".cmp-adaptiveform-checkboxgroup__value input[name='./default']").should('have.attr', 'aria-invalid', 'true'); - cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); + cy.assertFieldInvalid(".cmp-adaptiveform-checkboxgroup__value input[name='./default']", 'Value Type Mismatch'); cy.cancelConfigureDialog(); cy.deleteComponentByPath(checkBoxGroupDrop); diff --git a/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js b/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js index 1a1e6d9640..085c9744c7 100644 --- a/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js +++ b/ui.tests/test-module/specs/dropdown/dropdown.authoring.cy.js @@ -113,8 +113,7 @@ describe('Page - Authoring', function () { cy.get("coral-selectlist-item").contains('Number').should('be.visible').click({force: true}); cy.get(".cmp-adaptiveform-dropdown__defaultvalue input").invoke('val', 'Not a Number'); cy.get('.cq-dialog-submit').click(); - cy.get(".cmp-adaptiveform-dropdown__defaultvalue input[name='./default']").should('have.attr', 'aria-invalid', 'true'); - cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); + cy.assertFieldInvalid(".cmp-adaptiveform-dropdown__defaultvalue input[name='./default']", 'Value Type Mismatch'); cy.cancelConfigureDialog(); cy.deleteComponentByPath(dropdown); @@ -127,8 +126,7 @@ describe('Page - Authoring', function () { cy.get("coral-selectlist-item").contains('Boolean').click({force: true}); cy.get(".cmp-adaptiveform-dropdown__defaultvalue input").invoke('val', 'Not a Boolean'); cy.get('.cq-dialog-submit').click(); - cy.get(".cmp-adaptiveform-dropdown__defaultvalue input[name='./default']").should('have.attr', 'aria-invalid', 'true'); - cy.get('.cq-dialog-content:visible .coral-Form-errorlabel').should('contain.text', 'Value Type Mismatch'); + cy.assertFieldInvalid(".cmp-adaptiveform-dropdown__defaultvalue input[name='./default']", 'Value Type Mismatch'); cy.cancelConfigureDialog(); cy.deleteComponentByPath(dropdown); From 556e4d5ed82a361d5a1ab68d73f4ba62d0995d40 Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Wed, 7 Oct 2026 10:32:52 +0530 Subject: [PATCH 08/10] fix(tests): isolate Button inline fixtures and recover opening clicks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/e2e-testing/authoring-compatibility.md | 7 ++++ .../specs/button/button.authoring.cy.js | 42 +++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/e2e-testing/authoring-compatibility.md b/docs/e2e-testing/authoring-compatibility.md index 988d2a8ab6..dc41e96d0c 100644 --- a/docs/e2e-testing/authoring-compatibility.md +++ b/docs/e2e-testing/authoring-compatibility.md @@ -52,6 +52,13 @@ then repeats the original empty-label edit and verifies the empty native button and absent/empty title before deletion. Both edits wait for the inline toolbar to close. API teardown removes only the named test fixture if an assertion fails. +Each inline-editor case creates its own Button. The rich-text case enables and +saves `isTitleRichText` before opening the editor; it cannot reuse the preceding +case's fixture because `afterEach` deletes it. Opening retries the overlay/EDIT +action only while the inline toolbar is absent, within the existing ten-second +budget. Once the toolbar is visible, EDIT is not invoked again. A persistent +opening failure still fails the test rather than retrying the whole test. + ## Authentication and browser user agents Use the normal browser user agent for acceptance. Sling's referrer filter diff --git a/ui.tests/test-module/specs/button/button.authoring.cy.js b/ui.tests/test-module/specs/button/button.authoring.cy.js index b84c25c110..1596391913 100644 --- a/ui.tests/test-module/specs/button/button.authoring.cy.js +++ b/ui.tests/test-module/specs/button/button.authoring.cy.js @@ -74,6 +74,27 @@ describe('Button - Authoring', function () { cy.deleteComponentByPath(buttonDrop); } + const openButtonInlineEditor = function(buttonEditPathSelector) { + recurse( + () => { + cy.get('body').then($body => { + if (!$body.find('.rte-toolbar:visible').length) { + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); + cy.get('body').then($currentBody => { + if (!$currentBody.find('.rte-toolbar:visible').length) { + cy.invokeEditableAction("[data-action='EDIT']"); + } + }); + } + }); + return cy.get('body'); + }, + $body => $body.find('.rte-toolbar:visible').length === 1, + {limit: 21, delay: 500, timeout: 10000, log: false} + ); + return cy.get('.rte-toolbar:visible').should('have.length', 1); + }; + const testButtonBehaviourInilineEdit = function(buttonEditPathSelector, buttonDrop, isSites) { const updatedLabel = 'Updated Button'; if (isSites) { @@ -83,9 +104,7 @@ describe('Button - Authoring', function () { } const contextPath = Cypress.env('crx.contextPath') || ''; [updatedLabel, ''].forEach(label => { - cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); - cy.invokeEditableAction("[data-action='EDIT']"); - cy.get('.rte-toolbar:visible').should('have.length', 1); + openButtonInlineEditor(buttonEditPathSelector); getContentIframeBody().find('.cmp-adaptiveform-button__text').should('have.length', 1) .then(($span) => { $span[0].textContent = label; @@ -162,11 +181,18 @@ describe('Button - Authoring', function () { }); it('check rich text inline editor is present', function(){ - cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); - cy.invokeEditableAction("[data-action='EDIT']"); - cy.get(".rte-toolbar").should('be.visible'); - cy.get('.rte-toolbar-item[title="Close"]').should('be.visible').click(); - cy.deleteComponentByPath(buttonDrop); + cy.cleanTest(buttonDrop).then(function() { + dropButtonInContainer(); + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); + cy.invokeEditableAction("[data-action='CONFIGURE']"); + cy.get("input[name='./isTitleRichText'][type='checkbox']").check({force: true}).should('be.checked'); + cy.get("div[name='richTextTitle']").should('be.visible'); + cy.submitConfigureDialog(); + openButtonInlineEditor(buttonEditPathSelector); + cy.get('.rte-toolbar:visible .rte-toolbar-item[title="Close"]').should('be.visible').click(); + cy.get('.rte-toolbar:visible').should('not.exist'); + cy.deleteComponentByPath(buttonDrop); + }); }); }); From 1953c22491672df72654f94fc5801a1b4e2d2acf Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Wed, 7 Oct 2026 11:44:41 +0530 Subject: [PATCH 09/10] fix(tests): configure Button RTE and isolate equality-rule targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/e2e-testing/authoring-compatibility.md | 20 +++++- .../specs/button/button.authoring.cy.js | 15 +++-- .../authoring/ruleEditor.authoring.cy.js | 65 +++++++++++-------- 3 files changed, 65 insertions(+), 35 deletions(-) diff --git a/docs/e2e-testing/authoring-compatibility.md b/docs/e2e-testing/authoring-compatibility.md index dc41e96d0c..dfd269c9c7 100644 --- a/docs/e2e-testing/authoring-compatibility.md +++ b/docs/e2e-testing/authoring-compatibility.md @@ -52,13 +52,29 @@ then repeats the original empty-label edit and verifies the empty native button and absent/empty title before deletion. Both edits wait for the inline toolbar to close. API teardown removes only the named test fixture if an assertion fails. -Each inline-editor case creates its own Button. The rich-text case enables and -saves `isTitleRichText` before opening the editor; it cannot reuse the preceding +Each RTE inline-editor case creates its own Button and enables and saves +`isTitleRichText` before opening the editor; it cannot reuse the preceding case's fixture because `afterEach` deletes it. Opening retries the overlay/EDIT action only while the inline toolbar is absent, within the existing ten-second budget. Once the toolbar is visible, EDIT is not invoked again. A persistent opening failure still fails the test rather than retrying the whole test. +The Forms toolbar selects the plaintext editor when the rendered label/text +has no `data-richtext` attribute. A default Button therefore need not expose +`.rte-toolbar`: retrying EDIT or increasing the timeout cannot turn plaintext +editing into rich-text editing. RTE caption-edit tests must explicitly enable +rich text and wait for the configure save to refresh the editable. + +## Rule-save assertions + +Parse the rule-save POST's `:content` JSON and compare `fd:events.change` +structurally, as on master. Require the exact expected scripts rather than a +substring of serialized JSON. The equality-rule test cleans its named text-input +fixture before insertion and after each attempt so failed saves cannot leave +rules that accumulate on subsequent runs. It selects the edited field's generated +name explicitly as the hide target, rather than the first option: a leftover +sibling otherwise changes a self-hide rule into a `dispatchEvent` rule. + ## Authentication and browser user agents Use the normal browser user agent for acceptance. Sling's referrer filter diff --git a/ui.tests/test-module/specs/button/button.authoring.cy.js b/ui.tests/test-module/specs/button/button.authoring.cy.js index 1596391913..b886530166 100644 --- a/ui.tests/test-module/specs/button/button.authoring.cy.js +++ b/ui.tests/test-module/specs/button/button.authoring.cy.js @@ -95,6 +95,14 @@ describe('Button - Authoring', function () { return cy.get('.rte-toolbar:visible').should('have.length', 1); }; + const enableButtonRichText = function(buttonEditPathSelector) { + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); + cy.invokeEditableAction("[data-action='CONFIGURE']"); + cy.get("input[name='./isTitleRichText'][type='checkbox']").check({force: true}).should('be.checked'); + cy.get("div[name='richTextTitle']").should('be.visible'); + cy.submitConfigureDialog(); + }; + const testButtonBehaviourInilineEdit = function(buttonEditPathSelector, buttonDrop, isSites) { const updatedLabel = 'Updated Button'; if (isSites) { @@ -102,6 +110,7 @@ describe('Button - Authoring', function () { } else { dropButtonInContainer(); } + enableButtonRichText(buttonEditPathSelector); const contextPath = Cypress.env('crx.contextPath') || ''; [updatedLabel, ''].forEach(label => { openButtonInlineEditor(buttonEditPathSelector); @@ -183,11 +192,7 @@ describe('Button - Authoring', function () { it('check rich text inline editor is present', function(){ cy.cleanTest(buttonDrop).then(function() { dropButtonInContainer(); - cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + buttonEditPathSelector); - cy.invokeEditableAction("[data-action='CONFIGURE']"); - cy.get("input[name='./isTitleRichText'][type='checkbox']").check({force: true}).should('be.checked'); - cy.get("div[name='richTextTitle']").should('be.visible'); - cy.submitConfigureDialog(); + enableButtonRichText(buttonEditPathSelector); openButtonInlineEditor(buttonEditPathSelector); cy.get('.rte-toolbar:visible .rte-toolbar-item[title="Close"]').should('be.visible').click(); cy.get('.rte-toolbar:visible').should('not.exist'); diff --git a/ui.tests/test-module/specs/ruleeditor/authoring/ruleEditor.authoring.cy.js b/ui.tests/test-module/specs/ruleeditor/authoring/ruleEditor.authoring.cy.js index 07452db07d..55cd069322 100644 --- a/ui.tests/test-module/specs/ruleeditor/authoring/ruleEditor.authoring.cy.js +++ b/ui.tests/test-module/specs/ruleeditor/authoring/ruleEditor.authoring.cy.js @@ -78,17 +78,11 @@ describe('Rule editor authoring sanity for core-components',function(){ cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.action.closeRuleEditor).click(); } - const createRuleToHideTextInputOnEqualityOperator = function() { + const createRuleToHideTextInputOnEqualityOperator = function(fieldName) { // Edit rule option not existing on button toolbar cy.get(formsSelectors.ruleEditor.action.editRule).should("exist"); - cy.initializeEventHandlerOnChannel("af-rule-editor-initialized").as("isRuleEditorInitialized"); - cy.wait(1000); cy.get(formsSelectors.ruleEditor.action.editRule).click(); - - // click on create option from rule editor header - // cy.get("@isRuleEditorInitialized").its('done').should('equal', true); - cy.wait(1000); - cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.action.createRuleButton).should("be.visible").click(); + cy.createRule(); cy.getRuleEditorIframe().find(formsSelectors.ruleEditor.action.sideToggleButton + ":first").click(); @@ -122,7 +116,9 @@ describe('Rule editor authoring sanity for core-components',function(){ cy.getRuleEditorIframe().find(".terminal-view.AFCOMPONENT.VARIABLE").should("be.visible"); cy.getRuleEditorIframe().find(".terminal-view.AFCOMPONENT.VARIABLE").click(); - cy.getRuleEditorIframe().find(".terminal-view.AFCOMPONENT.VARIABLE coral-overlay.is-open .expression-selectlist coral-selectlist-item:first").click({force: true}); + cy.getRuleEditorIframe().find(".terminal-view.AFCOMPONENT.VARIABLE coral-overlay.is-open .expression-selectlist coral-selectlist-item") + .filter((index, item) => item.value === "$form." + fieldName) + .should("have.length", 1).click({force: true}); cy.intercept('POST', /content\/forms\/af\/core-components-it\/samples\/ruleeditor\/blank.*/).as('ruleEditorRequest'); @@ -132,7 +128,10 @@ describe('Rule editor authoring sanity for core-components',function(){ cy.wait('@ruleEditorRequest').then((interception) => { expect(interception.response.statusCode).to.equal(201); const submittedData = Object.fromEntries(new URLSearchParams(interception.request.body)); - expect(submittedData[":content"]).contains("\"fd:events\":{\"change\":[\"if(contains($event.payload.changes[].propertyName, 'value'), if($field.$value == 'abc', {visible : false()}, {}), {})\"]}"); + const submittedContent = JSON.parse(submittedData[":content"]); + expect(submittedContent["fd:events"].change).to.deep.equal([ + "if(contains($event.payload.changes[].propertyName, 'value'), if($field.$value == 'abc', {visible : false()}, {}), {})" + ]); }); // check and close rule editor @@ -349,27 +348,37 @@ describe('Rule editor authoring sanity for core-components',function(){ cy.deleteComponentByPath(buttonEditPath); }) - it('should add rule on texbox equality operator to hide a text box', function () { - cy.openAuthoring(formPath); - cy.selectLayer("Edit"); - cy.get(sitesSelectors.overlays.overlay.component + "[data-path='" + formContainerPath + "/*']").should("exist"); + context('Text box equality rule', function () { + afterEach(function () { + cy.cleanTestFixture(textinputEditPath); + }); - cy.insertComponent(sitesSelectors.overlays.overlay.component + "[data-path='" + formContainerPath + "/*']", - "Adaptive Form Text Box", afConstants.components.forms.resourceType.formtextinput); - // cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textinputEditPathSelector); - // cy.get(formsSelectors.ruleEditor.action.configure).should("exist"); - // cy.get(formsSelectors.ruleEditor.action.configure).click(); - // cy.get(".cmp-adaptiveform-base__editdialogbasic [name='./name']").clear().type("textinput"); - // cy.get(".cq-dialog-actions.cq-dialog-submit").click(); - - cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textinputEditPathSelector); + it('should add rule on texbox equality operator to hide a text box', function () { + cy.openAuthoring(formPath); + cy.selectLayer("Edit"); + cy.get(sitesSelectors.overlays.overlay.component + "[data-path='" + formContainerPath + "/*']").should("exist"); + cy.cleanTest(textinputEditPath); - createRuleToHideTextInputOnEqualityOperator(); - cy.get(sitesSelectors.overlays.overlay.component + textinputEditPathSelector).should("exist"); + cy.insertComponent(sitesSelectors.overlays.overlay.component + "[data-path='" + formContainerPath + "/*']", + "Adaptive Form Text Box", afConstants.components.forms.resourceType.formtextinput); + // cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textinputEditPathSelector); + // cy.get(formsSelectors.ruleEditor.action.configure).should("exist"); + // cy.get(formsSelectors.ruleEditor.action.configure).click(); + // cy.get(".cmp-adaptiveform-base__editdialogbasic [name='./name']").clear().type("textinput"); + // cy.get(".cq-dialog-actions.cq-dialog-submit").click(); + + const contextPath = Cypress.env('crx.contextPath') || ''; + cy.request(contextPath + textinputEditPath + '.json').its('body.name') + .should('be.a', 'string').and('not.be.empty').then(fieldName => { + cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textinputEditPathSelector); + createRuleToHideTextInputOnEqualityOperator(fieldName); + }); + cy.get(sitesSelectors.overlays.overlay.component + textinputEditPathSelector).should("exist"); - cy.selectLayer("Edit"); - cy.deleteComponentByPath(textinputEditPath); - }) + cy.selectLayer("Edit"); + cy.deleteComponentByPath(textinputEditPath); + }); + }); if (cy.af.isLatestAddon()) { it('should add validation rule on date fields', function () { From f1a780f565c05096cf39c35b51e55ecf88e3588b Mon Sep 17 00:00:00 2001 From: Rishi Mehta <rismehta@adobe.com> Date: Wed, 7 Oct 2026 12:42:05 +0530 Subject: [PATCH 10/10] fix(ci): use a supported Node runtime for Lighthouse Install Node.js 22.19.0 in the shared Cypress setup and pin Lighthouse to 13.5.0 to prevent JSON import attribute parsing failures on the older test-runner images. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .circleci/config.yml | 5 ++++- ui.tests/README.md | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 78fb19dfeb..0f64e5d218 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,6 +3,7 @@ version: 2.1 orbs: codecov: codecov/codecov@1.0.5 browser-tools: circleci/browser-tools@1.4.3 + node: circleci/node@5.2.0 common: restore_cache: &restore_cache @@ -15,6 +16,8 @@ common: cypress_test_steps: &cypress_test_steps steps: - checkout + - node/install: + node-version: '22.19.0' - browser-tools/install-chrome - browser-tools/install-chromedriver - run: @@ -39,7 +42,7 @@ common: command: npm install chrome-launcher - run: name: Install lighthouse - command: npm install lighthouse + command: npm install lighthouse@13.5.0 - run: name: Install Web Driver command: npm install selenium-webdriver diff --git a/ui.tests/README.md b/ui.tests/README.md index abf547e156..0661d67753 100644 --- a/ui.tests/README.md +++ b/ui.tests/README.md @@ -28,6 +28,10 @@ npm run cypress:run:file -- ./specs/textinput/textinput.runtime.spec.js * Chrome and/or Firefox browser installed locally in default location * An AEM author instance running at http://localhost:4502 +CircleCI installs Node.js 22.19.0 before the shared UI-test dependencies and +uses Lighthouse 13.5.0. Lighthouse requires Node.js >=22.19; the older Node.js +bundled in the AEM test-runner images cannot parse its JSON import attributes. +Keep the CI Node.js and Lighthouse versions compatible when upgrading either. #### Remarks * After execution, reports and logs are available in `test-module/target/reports` folder