From 96a5cf76f07a89b94833ffa51f81dc05aa501fe4 Mon Sep 17 00:00:00 2001 From: Okan Sahin <39759830+mokimo@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:28:16 +0200 Subject: [PATCH 01/12] Safeguard aria-label check to avoid JS errors (#4448) Safeguard aria-label check --- libs/utils/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 51afaa30ae9..8f693b29428 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -991,9 +991,9 @@ export function decorateLinks(el) { const pipeRegex = /\s?\|([^|]*)$/; if (pipeRegex.test(a.textContent) && !/\.[a-z]+/i.test(a.textContent)) { const node = [...a.childNodes].reverse()[0]; - const ariaLabel = node.textContent.match(pipeRegex)[1]; + const ariaLabel = node.textContent.match(pipeRegex)?.[1]; node.textContent = node.textContent.replace(pipeRegex, ''); - a.setAttribute('aria-label', ariaLabel.trim()); + a.setAttribute('aria-label', (ariaLabel || '').trim()); } return rdx; From 8c2c17d7bc028570c03f7b3b1bb68a353aafbc1f Mon Sep 17 00:00:00 2001 From: Rares Munteanu Date: Wed, 25 Jun 2025 13:28:24 +0200 Subject: [PATCH 02/12] [MWPW-175255] Actionless action item a11y (#4461) --- libs/blocks/action-scroller/action-scroller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/blocks/action-scroller/action-scroller.js b/libs/blocks/action-scroller/action-scroller.js index dd0a3af5056..51add7da6c0 100644 --- a/libs/blocks/action-scroller/action-scroller.js +++ b/libs/blocks/action-scroller/action-scroller.js @@ -119,6 +119,7 @@ export default function init(el) { el.replaceChildren(items, ...buttons); if (hasNav) { handleBtnState(items, buttons); + if (!items.querySelectorAll('a').length) items.setAttribute('tabindex', 0); allActionScrollers.push({ scroller: items, buttons }); import('../../utils/action.js').then(({ debounce }) => { items.addEventListener('scroll', debounce(() => handleBtnState(items, buttons), 50)); From 1f214f894b14752c25cbb03d70173f07e99f24fc Mon Sep 17 00:00:00 2001 From: Mira Fedas <30750556+mirafedas@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:14:35 +0200 Subject: [PATCH 03/12] MWPW-175427: mini-compare-chart bullet-list collapse icon fix (#4453) fixed the icon for collapsible rows --- libs/blocks/merch-card/merch-card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/blocks/merch-card/merch-card.js b/libs/blocks/merch-card/merch-card.js index f94fbce622a..1c5c23a2531 100644 --- a/libs/blocks/merch-card/merch-card.js +++ b/libs/blocks/merch-card/merch-card.js @@ -458,7 +458,7 @@ const createFirstRow = async (firstRow, isMobile, checkmarkCopyContainer, defaul 'aria-expanded': defaultChevronState === 'open', 'aria-controls': checkmarkCopyContainer.id, 'daa-lh': `${checkmarkCopyContainer.id}-toggle-button`, - }, addIcon); + }, defaultChevronState === 'open' ? removeIcon : addIcon); firstRowTextParagraph = createTag('div', { class: 'footer-rows-title' }, firstRowText); firstRowTextParagraph.appendChild(toggleIcon); From f6eda3b449e83e3469f54ca60acb5d17445acb09 Mon Sep 17 00:00:00 2001 From: Chris Peyer Date: Wed, 25 Jun 2025 21:21:27 -0400 Subject: [PATCH 04/12] MWPW-175169 Fix gb-changed highlighting; Add overlay toggle (#4462) * MWPW-175169 Fix gb-changed highlighting * Add ability to toggle the gray overlay on/off --- libs/blocks/graybox/graybox.css | 109 ++++++++++++++++++++++++- libs/blocks/graybox/graybox.js | 122 +++++++++++++++++++++++----- test/blocks/graybox/graybox.test.js | 104 +++++++++++++++++++++++- 3 files changed, 313 insertions(+), 22 deletions(-) diff --git a/libs/blocks/graybox/graybox.css b/libs/blocks/graybox/graybox.css index 72e250b0df1..996d6fe560e 100644 --- a/libs/blocks/graybox/graybox.css +++ b/libs/blocks/graybox/graybox.css @@ -1,3 +1,4 @@ +/* stylelint-disable selector-class-pattern */ :root { --base-z-index: 100; --gb-container-bg: white; @@ -80,7 +81,7 @@ background-color: var(--gb-overlay-color); content: ""; display: block; - height: 100%; + height: 100vh; left: 0; pointer-events: none; position: fixed; @@ -105,7 +106,7 @@ /* The elements that should appear above the overlay */ .gb-graybox-body .gb-changed { position: relative; - z-index: calc(var(--base-z-index) - 1); + z-index: calc(var(--base-z-index) + 1); } .graybox-container { @@ -322,3 +323,107 @@ border-radius: 5px; } } + +/* Spectrum Switch */ +.spectrum-Switch { + display: inline-flex; + align-items: flex-start; + position: relative; + margin: auto; + min-block-size: var(--mod-switch-height, var(--spectrum-switch-min-height)); + max-inline-size: 100%; + vertical-align: top; +} + +.spectrum-Switch.gb-toggle-disabled { + pointer-events: none; + opacity: 0.5; +} + +/* .gb-toggle-disabled .spectrum-Switch { + pointer-events: none; +} */ + +.spectrum-Switch-input { + margin: 0; + box-sizing: border-box; + padding: 0; + position: absolute; + inline-size: 100%; + block-size: 100%; + inset-block-start: 0; + inset-inline-start: 0; + opacity: 0; + z-index: 1; + cursor: pointer; +} + +.spectrum-Switch-switch { + display: inline-block; + box-sizing: border-box; + position: relative; + inline-size: 26px; + margin-block: 9px; + margin-inline: 0; + flex-grow: 0; + flex-shrink: 0; + vertical-align: middle; + transition: background 0.16s ease-in-out, border 0.16s ease-in-out; + block-size: 14px; + inset-inline-start: 0; + inset-inline-end: 0; + border-radius: 7px; + background-color: rgb(225 225 225); +} + +.spectrum-Switch-switch::after, .spectrum-Switch-switch::before { + display: block; + position: absolute; + content: ""; + inset-block-start: 0; + inset-inline-start: 0; +} + +.spectrum-Switch-switch::before { + box-sizing: border-box; + transition: background 0.16s ease-in-out, border 0.16s ease-in-out, transform 0.13s ease-in-out, box-shadow 0.16s ease-in-out; + inline-size: 14px; + block-size: 14px; + border-width: 2px; + border-radius: 7px; + border-style: solid; + background-color: rgb(248 248 248); + border-color: rgb(113 113 113); +} + +.spectrum-Switch-switch::after { + border-radius: 11px; + inset-inline-end: 0; + inset-block-end: 0; + margin: 0; + transition: opacity 0.16s ease-out, margin 0.16s ease-out; +} + +.spectrum-Switch-label { + color: rgb(41 41 41); + margin-inline: 10px; + margin-block-start: 6px; + margin-block-end: 0; + font-size: 14px; + line-height: 1.3; + transition: color 0.16s ease-in-out; +} + +.spectrum-Switch-input:checked+.spectrum-Switch-switch::before { + transform: translate(calc(26px - 100%)); + border-color: rgb(80 80 80); +} + +.spectrum-Switch:hover .spectrum-Switch-input+.spectrum-Switch-switch::before { + border-color: rgb(80 80 80); + box-shadow: none; +} + +.spectrum-Switch-input:checked+.spectrum-Switch-switch { + background-color: rgb(41 41 41); +} diff --git a/libs/blocks/graybox/graybox.js b/libs/blocks/graybox/graybox.js index 3e03d6ca37f..cfced9e67f2 100644 --- a/libs/blocks/graybox/graybox.js +++ b/libs/blocks/graybox/graybox.js @@ -19,12 +19,20 @@ const CLASS = { NO_BORDER: 'gb-no-border', NO_CHANGE: 'gb-no-change', NO_CLICK: 'gb-no-click', + OVERLAY_OFF: 'gb-overlay-off', PAGE_OVERLAY: 'gb-page-overlay', PHONE_PREVIEW: 'gb-phone-preview', TABLET_PREVIEW: 'gb-tablet-preview', SELECTED_BUTTON: 'gb-selected-button', }; +const DISABLED = { + CHANGED: 'gb-disabled-changed', + NO_CHANGE: 'gb-disabled-no-change', + NO_CLICK: 'gb-disabled-no-click', + PAGE_OVERLAY: 'gb-disabled-page-overlay', +}; + const METADATA = { DESC: 'gb-desc', FOOTER: 'gb-footer', @@ -40,6 +48,7 @@ const USER_AGENT = { const DEFAULT_TITLE = ''; let deviceModal; +let deviceIframe; const setMetadata = (metadata) => { const { selector, val } = metadata; @@ -168,12 +177,35 @@ function setUserAgent(window, userAgent) { // eslint-disable-next-line no-promise-executor-return const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); -const injectCSSIntoIframe = (iframe, cssRules) => { - iframe.addEventListener('load', () => { +const toggleGrayboxOverlay = (isChecked) => { + if (!isChecked) { + document.body.classList.add(CLASS.OVERLAY_OFF); + } else { + document.body.classList.remove(CLASS.OVERLAY_OFF); + } + + const toggleMap = [ + [CLASS.CHANGED, DISABLED.CHANGED], + [CLASS.NO_CHANGE, DISABLED.NO_CHANGE], + [CLASS.NO_CLICK, DISABLED.NO_CLICK], + [CLASS.PAGE_OVERLAY, DISABLED.PAGE_OVERLAY], + ]; + + toggleMap.forEach(([enabled, disabled]) => { + const from = isChecked ? disabled : enabled; + const to = isChecked ? enabled : disabled; + document.querySelectorAll(`.${from}`).forEach((el) => { + el.classList.replace(from, to); + }); + }); +}; + +const injectCSSIntoIframe = (cssRules) => { + deviceIframe.addEventListener('load', () => { try { const style = document.createElement('style'); style.textContent = cssRules; - iframe.contentWindow.document.head.appendChild(style); + deviceIframe.contentWindow.document.head.appendChild(style); } catch (error) { // eslint-disable-next-line no-console console.warn('Could not inject CSS into iframe. It might be cross-origin.', error); @@ -181,32 +213,32 @@ const injectCSSIntoIframe = (iframe, cssRules) => { }); }; -const setIframeUA = (iFrame, isMobile, isTablet) => { +const setIframeUA = (isMobile, isTablet) => { if (isMobile) { document.body.classList.add(CLASS.PHONE_PREVIEW); deviceModal.classList.add('mobile'); - setUserAgent(iFrame.contentWindow, USER_AGENT.iPhone); + setUserAgent(deviceIframe.contentWindow, USER_AGENT.iPhone); } else if (isTablet) { document.body.classList.add(CLASS.TABLET_PREVIEW); deviceModal.classList.add('tablet'); - setUserAgent(iFrame.contentWindow, USER_AGENT.iPad); + setUserAgent(deviceIframe.contentWindow, USER_AGENT.iPad); } }; -const setupIframe = (iFrame, isMobile, isTablet) => { +const setupIframe = (isMobile, isTablet) => { const cssRules = ` body > .mep-preview-overlay { display: none !important; } `; - injectCSSIntoIframe(iFrame, cssRules); + injectCSSIntoIframe(cssRules); - iFrame.style.height = ''; + deviceIframe.style.height = ''; - setIframeUA(iFrame, isMobile, isTablet); + setIframeUA(isMobile, isTablet); // Spoof iFrame dimensions as screen size for MEP - iFrame.contentWindow.screen = { + deviceIframe.contentWindow.screen = { width: isMobile ? 350 : 768, height: isMobile ? 800 : 1024, }; @@ -239,10 +271,15 @@ const openDeviceModal = async (e) => { const docFrag = new DocumentFragment(); const iFrameUrl = new URL(window.location.href); iFrameUrl.searchParams.set('graybox', 'menu-off'); + if (document.body.classList.contains(CLASS.OVERLAY_OFF)) { + iFrameUrl.searchParams.set('graybox-overlay', 'off'); + } else { + iFrameUrl.searchParams.delete('graybox-overlay'); + } const deviceBorder = createTag('img', { class: 'graybox-device-border', src: isMobile ? iphoneFrame : ipadFrame }); - const iFrame = createTag('iframe', { src: iFrameUrl.href, width: '100%', height: '100%' }); + deviceIframe = createTag('iframe', { src: iFrameUrl.href, width: '100%', height: '100%' }); - const modal = createTag('div', null, [deviceBorder, iFrame]); + const modal = createTag('div', null, [deviceBorder, deviceIframe]); docFrag.append(modal); deviceModal = await getModal( @@ -253,14 +290,14 @@ const openDeviceModal = async (e) => { const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); if (isSafari) { deviceModal.style.display = 'none'; - setupIframe(iFrame, isMobile, isTablet); - iFrame.addEventListener('load', () => { + setupIframe(isMobile, isTablet); + deviceIframe.addEventListener('load', () => { // safari needs to wait for the iframe to load before setting the user agent - setIframeUA(iFrame, isMobile, isTablet); + setIframeUA(isMobile, isTablet); deviceModal.style.display = ''; }); } else { - setupIframe(iFrame, isMobile, isTablet); + setupIframe(isMobile, isTablet); } const onDeviceModalClose = () => { @@ -275,6 +312,8 @@ const openDeviceModal = async (e) => { CLASS.PHONE_PREVIEW, CLASS.TABLET_PREVIEW, ); + + deviceIframe = null; }; window.addEventListener('milo:modal:closed', onDeviceModalClose, { once: true }); @@ -283,6 +322,30 @@ const openDeviceModal = async (e) => { curtain.classList.add('graybox-curtain'); }; +const createGrayboxOverlayToggle = (grayboxMenu) => { + const switchDiv = createTag('div', { class: 'spectrum-Switch' }, null, { parent: grayboxMenu }); + const input = createTag('input', { type: 'checkbox', class: 'spectrum-Switch-input', id: 'gb-overlay-toggle' }, null, { parent: switchDiv }); + createTag('span', { class: 'spectrum-Switch-switch' }, null, { parent: switchDiv }); + createTag('label', { class: 'spectrum-Switch-label', for: 'gb-overlay-toggle' }, 'Graybox Overlay', { parent: switchDiv }); + input.checked = true; + input.addEventListener('change', (e) => { + const isChecked = e.target.checked; + toggleGrayboxOverlay(isChecked); + if (deviceIframe) { + deviceIframe.contentWindow.postMessage(`gb-overlay-${isChecked ? 'on' : 'off'}`, '*'); + } + }); + + if (!document.querySelector(`.${CLASS.CHANGED}, .${CLASS.NO_CHANGE}, .${CLASS.NO_CLICK}, .${CLASS.PAGE_OVERLAY}`)) { + switchDiv.classList.add('gb-toggle-disabled'); + } + + const url = new URL(window.location.href); + if (url.searchParams.get('graybox-overlay') === 'off') { + setTimeout(() => input.click(), 10); + } +}; + const createGrayboxMenu = (options, { isOpen = false } = {}) => { const grayboxContainer = createTag('div', { class: 'graybox-container' }); const grayboxMenu = createTag('div', { class: 'graybox-menu' }, null, { parent: grayboxContainer }); @@ -306,6 +369,8 @@ const createGrayboxMenu = (options, { isOpen = false } = {}) => { button.addEventListener('click', openDeviceModal); }); + createGrayboxOverlayToggle(grayboxMenu); + const toggleBtn = document.createElement('button'); toggleBtn.className = 'gb-toggle'; toggleBtn.addEventListener('click', () => { @@ -322,7 +387,10 @@ const createGrayboxMenu = (options, { isOpen = false } = {}) => { const addPageOverlayDiv = () => { const overlayDiv = createTag('div', { class: CLASS.PAGE_OVERLAY }); - document.body.insertBefore(overlayDiv, document.body.firstChild); + const main = document.querySelector('main'); + if (main) { + main.insertBefore(overlayDiv, main.firstChild); + } }; const setupChangedEls = (globalNoClick) => { @@ -420,6 +488,10 @@ const grayboxThePage = (grayboxEl, grayboxMenuOff) => { /* c8 ignore next 3 */ if (grayboxMenuOff) { document.body.classList.add(CLASS.NO_BORDER); + const url = new URL(window.location.href); + if (url.searchParams.get('graybox-overlay') === 'off') { + toggleGrayboxOverlay(false); + } } else { createGrayboxMenu(options, { isOpen: true }); } @@ -440,6 +512,15 @@ const grayboxThePage = (grayboxEl, grayboxMenuOff) => { childList: true, subtree: true, }); + + // Listen for iframe messages to toggle graybox overlay in device views + window.addEventListener('message', (event) => { + if (event.data === 'gb-overlay-on') { + toggleGrayboxOverlay(true); + } else if (event.data === 'gb-overlay-off') { + toggleGrayboxOverlay(false); + } + }); }; export default function init(grayboxEl) { @@ -461,5 +542,8 @@ export default function init(grayboxEl) { setMetadata({ selector: 'georouting', val: 'off' }); const grayboxMenuOff = url.searchParams.get('graybox') === 'menu-off'; - window.milo.deferredPromise.then(() => grayboxThePage(grayboxEl, grayboxMenuOff)); + window.milo.deferredPromise.then(() => grayboxThePage( + grayboxEl, + grayboxMenuOff, + )); } diff --git a/test/blocks/graybox/graybox.test.js b/test/blocks/graybox/graybox.test.js index 36d8605413a..563d2b949f9 100644 --- a/test/blocks/graybox/graybox.test.js +++ b/test/blocks/graybox/graybox.test.js @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-globals */ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; import { loadStyle, MILO_EVENTS } from '../../../libs/utils/utils.js'; @@ -58,7 +59,7 @@ describe('Graybox', () => { }); it('Puts an overlay on the entire page', async () => { - const overlayDiv = document.querySelector('body > .gb-page-overlay'); + const overlayDiv = document.querySelector('body > main >.gb-page-overlay'); expect(overlayDiv).to.exist; const beforeStyle = window.getComputedStyle(overlayDiv); expect(beforeStyle.backgroundColor).to.equal('rgba(0, 0, 0, 0.45)'); @@ -247,3 +248,104 @@ describe('getGrayboxEnv', () => { expect(result).to.equal('test'); }); }); + +describe('Graybox advanced behaviors', () => { + let originalInnerWidth; + let originalUrl; + + beforeEach(async () => { + // Save and mock window properties + originalInnerWidth = window.innerWidth; + originalUrl = window.location.href; + // Use a graybox-like path for env (cannot change domain in browser tests) + history.replaceState({}, '', '/page'); + document.body.innerHTML = await readFile({ path: './mocks/graybox.html' }); + window.milo = { deferredPromise: Promise.resolve() }; + await init(document.querySelector('.graybox')); + }); + + afterEach(() => { + window.innerWidth = originalInnerWidth; + history.replaceState({}, '', originalUrl); + }); + + // NOTE: In browser-based test runners, you cannot change the domain/origin. + // All tests must use relative URLs (path/query/hash) only. + + it('rewrites .adobe.com links to graybox domains on load', () => { + const link = document.querySelector('a[href*="adobe.com"]'); + // This will not rewrite to graybox domain in browser tests, but should still be a valid link + expect(link).to.exist; + // We cannot assert the domain, but we can check the href is still present + expect(link.href).to.include('adobe.com'); + }); + + it('rewrites dynamically added .adobe.com links via MutationObserver', (done) => { + const newLink = document.createElement('a'); + newLink.href = 'https://www.adobe.com/somepath'; + newLink.textContent = 'Dynamic Adobe Link'; + const p = document.createElement('p'); + p.appendChild(newLink); + document.body.appendChild(p); + // Wait for MutationObserver to process + setTimeout(() => { + // We cannot assert the domain, but we can check the href is still present + expect(newLink.href).to.include('adobe.com'); + done(); + }, 50); + }); + + it('toggles overlay on/off via window message event', () => { + expect(document.body.classList.contains('gb-overlay-off')).to.be.false; + window.dispatchEvent(new MessageEvent('message', { data: 'gb-overlay-off' })); + expect(document.body.classList.contains('gb-overlay-off')).to.be.true; + window.dispatchEvent(new MessageEvent('message', { data: 'gb-overlay-on' })); + expect(document.body.classList.contains('gb-overlay-off')).to.be.false; + }); + + it('respects graybox-overlay=off URL param and disables overlay on load', async () => { + // Set the URL before loading HTML and calling init + history.replaceState({}, '', '/page?graybox-overlay=off'); + document.body.innerHTML = await readFile({ path: './mocks/graybox.html' }); + window.milo = { deferredPromise: Promise.resolve() }; + await init(document.querySelector('.graybox')); + // Wait for any async DOM updates + await new Promise((resolve) => { setTimeout(resolve, 50); }); + expect(document.body.classList.contains('gb-overlay-off')).to.be.true; + const input = document.getElementById('gb-overlay-toggle'); + expect(input.checked).to.be.false; + }); + + it('disables overlay toggle if no relevant elements exist', async () => { + document.body.innerHTML = await readFile({ path: './mocks/graybox.html' }); + // Remove all relevant classes BEFORE calling init + document.querySelectorAll('.gb-changed, .gb-no-change, .gb-no-click, .gb-page-overlay').forEach((el) => { el.className = ''; }); + window.milo = { deferredPromise: Promise.resolve() }; + await init(document.querySelector('.graybox')); + // Wait for any async DOM updates + await new Promise((resolve) => { setTimeout(resolve, 50); }); + const switchDiv = document.querySelector('.spectrum-Switch'); + expect(switchDiv.classList.contains('gb-toggle-disabled')).to.be.true; + }); + + it('hides device menu on small screens', async () => { + window.innerWidth = 500; + document.body.innerHTML = await readFile({ path: './mocks/graybox.html' }); + window.milo = { deferredPromise: Promise.resolve() }; + await init(document.querySelector('.graybox')); + const menu = document.querySelector('.graybox-menu'); + expect(menu.classList.contains('hide-devices')).to.be.true; + }); + + it('menu-off mode disables border and respects overlay state', async () => { + // Simulate menu-off and overlay-off using relative path + history.replaceState({}, '', '/page?graybox=menu-off&graybox-overlay=off'); + document.body.innerHTML = await readFile({ path: './mocks/graybox.html' }); + window.milo = { deferredPromise: Promise.resolve() }; + await init(document.querySelector('.graybox')); + expect(document.body.classList.contains('gb-no-border')).to.be.true; + expect(document.body.classList.contains('gb-overlay-off')).to.be.true; + // Menu should not be present + expect(document.querySelector('.graybox-container')).to.not.exist; + }); +}); From 979555b1ec8f8cf3f07bc6aacce357e658e50e5d Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:21:34 -0600 Subject: [PATCH 05/12] MWPW-175550 [MMM][MEP] Updates to metada-optimization json does not always reflect in MMM (#4463) Add random query parameter to metadata URL for uniqueness --- libs/blocks/mmm/mmm.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/blocks/mmm/mmm.js b/libs/blocks/mmm/mmm.js index e46a53f6f2c..f79b8a38f5f 100644 --- a/libs/blocks/mmm/mmm.js +++ b/libs/blocks/mmm/mmm.js @@ -819,7 +819,8 @@ async function createView(el, search) { switch (true) { case isReport: url = API_URLS.report; break; case isMetadataLookup: { - url = TARGET_METADATA_OPTIONS[SEARCH().selectedRepo].metadata; + const rngString = Math.random().toString(36).substring(2, 10); + url = `${TARGET_METADATA_OPTIONS[SEARCH().selectedRepo].metadata}?limit=10000&r=${rngString}`; method = 'GET'; body = null; break; From 2f95660c1ad40c86ad86256a7a10b5ec88e2feec Mon Sep 17 00:00:00 2001 From: Biljana Cvijanovic Date: Thu, 26 Jun 2025 10:23:13 +0200 Subject: [PATCH 06/12] [MWPW-173101][NALA] Automation test script for ReadingTime block (#4385) MWPW-173101 Automation test script for ReadingTime block --- nala/blocks/reading-time/reading-time.page.js | 45 +++++++ nala/blocks/reading-time/reading-time.spec.js | 34 +++++ nala/blocks/reading-time/reading-time.test.js | 118 ++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 nala/blocks/reading-time/reading-time.page.js create mode 100644 nala/blocks/reading-time/reading-time.spec.js create mode 100644 nala/blocks/reading-time/reading-time.test.js diff --git a/nala/blocks/reading-time/reading-time.page.js b/nala/blocks/reading-time/reading-time.page.js new file mode 100644 index 00000000000..93505730673 --- /dev/null +++ b/nala/blocks/reading-time/reading-time.page.js @@ -0,0 +1,45 @@ +export default class ReadingTimeBlock { + constructor(page) { + this.page = page; + + this.readingTime = this.page.locator('.reading-time.content'); + this.media = this.page.locator('.media'); + this.foregroundImage = this.page.locator('.container.foreground .media-row .image'); + this.image = this.page.locator('.media-row .image img'); + this.marquee = this.page.locator('.marquee.small.light'); + + this.detailM = this.page.locator('.detail-m'); + this.headingXL = this.page.locator('.heading-xl'); + this.bodyM = this.page.locator('.body-m'); + this.outlineButtonL = this.page.locator('.con-button.outline.button-l'); + this.blueButtonL = this.page.locator('.con-button.blue.button-l'); + this.assetImage = this.page.locator('.asset.image img'); + + this.attributes = { + 'foreground.image': { + foregroundImg: { + loading: 'eager', + fetchpriority: 'high', + width: '400', + height: '300', + }, + }, + 'asset.image': { + assetImg: { + loading: 'eager', + fetchpriority: 'high', + width: '600', + height: '300', + }, + }, + }; + } + + async getReadingText() { + return this.readingTime.innerText(); + } + + async getContentText() { + return this.page.locator('body').innerText(); + } +} diff --git a/nala/blocks/reading-time/reading-time.spec.js b/nala/blocks/reading-time/reading-time.spec.js new file mode 100644 index 00000000000..f3816de3635 --- /dev/null +++ b/nala/blocks/reading-time/reading-time.spec.js @@ -0,0 +1,34 @@ +module.exports = { + name: 'Reading-time Block', + features: [ + { + tcid: '0', + name: 'Reading-time block', + path: '/drafts/nala/blocks/reading-time/reading-time-block', + data: { timeText: '0 min read' }, + tags: 'reading-time @smoke @regression @milo', + }, + { + tcid: '1', + name: 'Reading-time without text', + path: '/drafts/nala/blocks/reading-time/reading-time-withouttext', + tags: 'reading-time @smoke @regression @milo', + }, + { + tcid: '2', + name: 'Reading-time with text', + path: '/drafts/nala/blocks/reading-time/reading-time-text', + data: { + detailText: 'Detail', + h2Text: 'Heading XL Marquee standard small light', + h3Text: 'Text (inset, large, m spacing)', + bodyText: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.', + bodyTextL: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ut nisl vitae urna volutpat tincidunt ac in mauris. In hac habitasse platea dictumst. Integer vel tempus nisi.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation. ', + listItemsText: 'Lorem ipsum dolor sit amet.', + outlineButtonText: 'Lorem ipsum', + blueButtonText: 'Call to action', + }, + tags: 'reading-time @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/reading-time/reading-time.test.js b/nala/blocks/reading-time/reading-time.test.js new file mode 100644 index 00000000000..0b438146472 --- /dev/null +++ b/nala/blocks/reading-time/reading-time.test.js @@ -0,0 +1,118 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './reading-time.spec.js'; +import ReadingTimeBlock from './reading-time.page.js'; +import { runAccessibilityTest } from '../../libs/accessibility.js'; + +let readingTime; +let webUtil; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Reading-time feature test suite', () => { + test.beforeEach(async ({ page }) => { + readingTime = new ReadingTimeBlock(page); + webUtil = new WebUtil(page); + }); + + test(`[Test Id - ${features[0].tcid}] ${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + await test.step('step-1: Go to Reading-time feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Reading-time specs', async () => { + await expect(await readingTime.readingTime).toBeVisible(); + await expect(await readingTime.readingTime).toContainText(data.timeText); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(readingTime.readingTime).toHaveAttribute('daa-lh', 'b1|reading-time'); + }); + + await test.step('step-4: Verify the accessibility test on Reading-time block', async () => { + await runAccessibilityTest({ page, testScope: readingTime.readingTime, skipA11yTest: true }); + }); + }); + + test(`[Test Id - ${features[1].tcid}] ${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + await test.step('step-1: Go to Reading-time without text test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Reading-time without text specs', async () => { + await expect(await readingTime.readingTime).toBeVisible(); + const actual = parseInt((await readingTime.getReadingText()).split(' ')[0], 10); + const contentText = await readingTime.getContentText(); + const wordCount = contentText.trim().split(/\s+/).length; + const expected = Math.ceil(wordCount / 200); + expect(Math.abs(actual - expected)).toBeLessThanOrEqual(1); + await expect(await readingTime.media).toBeVisible(); + await expect(await readingTime.foregroundImage).toBeVisible(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(readingTime.readingTime).toHaveAttribute('daa-lh', 'b1|reading-time'); + expect(await webUtil.verifyAttributes(readingTime.image, readingTime.attributes['foreground.image'].foregroundImg)).toBeTruthy(); + }); + + await test.step('step-4: Verify the accessibility test on Reading-time without text block', async () => { + await runAccessibilityTest({ page, testScope: readingTime.readingTime, skipA11yTest: true }); + }); + }); + + test(`[Test Id - ${features[2].tcid}] ${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Reading-time with text test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Reading-time with text specs', async () => { + await expect(await readingTime.readingTime).toBeVisible(); + await expect(await readingTime.detailM).toContainText(data.detailText); + await expect(await readingTime.headingXL).toContainText(data.h2Text); + await expect(await readingTime.bodyM).toContainText(data.bodyText); + await expect(await readingTime.outlineButtonL).toContainText(data.outlineButtonText); + await expect(await readingTime.blueButtonL).toContainText(data.blueButtonText); + + const actual = parseInt((await readingTime.getReadingText()).split(' ')[0], 10); + const contentText = await readingTime.getContentText(); + const wordCount = contentText.trim().split(/\s+/).length; + const expected = Math.ceil(wordCount / 200); + expect(Math.abs(actual - expected)).toBeLessThanOrEqual(1); + + for (const sectionId of ['s2', 's3', 's4', 's5']) { + const section = readingTime.page.locator(`.section[daa-lh="${sectionId}"]`); + const heading = section.locator('h3.heading-l'); + await expect(heading).toContainText(data.h3Text); + const paragraphs = section.locator('p.body-l'); + await expect(paragraphs.nth(0)).toContainText(data.bodyTextL); + await expect(paragraphs.nth(1)).toContainText(data.bodyTextL); + const listItems = section.locator('ul.body-l > li'); + await expect(listItems).toHaveCount(3); + for (let i = 0; i < 3; i++) { + await expect(listItems.nth(i)).toContainText(data.listItemsText); + } + } + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(readingTime.readingTime).toHaveAttribute('daa-lh', 'b1|reading-time'); + expect(await webUtil.verifyAttributes(readingTime.assetImage, readingTime.attributes['asset.image'].assetImg)).toBeTruthy(); + }); + + await test.step('step-4: Verify the accessibility test on Reading-time with text block', async () => { + await runAccessibilityTest({ page, testScope: readingTime.readingTime, skipA11yTest: true }); + }); + }); +}); From 0164a844bb64c470053ddf736ac0b54753f4c79e Mon Sep 17 00:00:00 2001 From: Biljana Cvijanovic Date: Thu, 26 Jun 2025 14:35:36 +0200 Subject: [PATCH 07/12] [MWPW-173103][NALA] Automation test script for Mnemonic List (#4470) MWPW-173103 Automation test script for Mnemonic List --- nala/blocks/mnemonic/mnemonic.page.js | 9 ++++++ nala/blocks/mnemonic/mnemonic.spec.js | 16 ++++++++++ nala/blocks/mnemonic/mnemonic.test.js | 43 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 nala/blocks/mnemonic/mnemonic.page.js create mode 100644 nala/blocks/mnemonic/mnemonic.spec.js create mode 100644 nala/blocks/mnemonic/mnemonic.test.js diff --git a/nala/blocks/mnemonic/mnemonic.page.js b/nala/blocks/mnemonic/mnemonic.page.js new file mode 100644 index 00000000000..5aa529a4a07 --- /dev/null +++ b/nala/blocks/mnemonic/mnemonic.page.js @@ -0,0 +1,9 @@ +export default class Mnemonic { + constructor(page) { + this.page = page; + + this.mnemonicList = page.locator('.mnemonic-list'); + this.productItems = page.locator('.product-item'); + this.productItemsImg = page.locator('.product-item img'); + } +} diff --git a/nala/blocks/mnemonic/mnemonic.spec.js b/nala/blocks/mnemonic/mnemonic.spec.js new file mode 100644 index 00000000000..b35adf52e1b --- /dev/null +++ b/nala/blocks/mnemonic/mnemonic.spec.js @@ -0,0 +1,16 @@ +module.exports = { + name: 'Mnemonic List', + features: [ + { + tcid: '0', + name: 'Mnemonic', + path: '/drafts/nala/blocks/mnemonic/mnemonic-list', + tags: '@mnemonic @smoke @regression @milo', + data: { + trackingHeader1: 'Includes:', + trackingHeader2: 'Acrobat', + trackingHeader3: 'Illustrator', + }, + }, + ], +}; diff --git a/nala/blocks/mnemonic/mnemonic.test.js b/nala/blocks/mnemonic/mnemonic.test.js new file mode 100644 index 00000000000..13696025cc2 --- /dev/null +++ b/nala/blocks/mnemonic/mnemonic.test.js @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import Mnemonic from './mnemonic.page.js'; +import { features } from './mnemonic.spec.js'; +import { runAccessibilityTest } from '../../libs/accessibility.js'; + +let mnemonic; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Mnemonic List test suite', () => { + test.beforeEach(async ({ page }) => { + mnemonic = new Mnemonic(page); + }); + + test(`[Test Id - ${features[0].tcid}] ${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Mnemonic List page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Mnemonic List specs', async () => { + await expect(mnemonic.mnemonicList).toBeVisible(); + + await expect(mnemonic.productItems.nth(0)).toContainText(data.trackingHeader1); + await expect(mnemonic.productItems.nth(1)).toContainText(data.trackingHeader2); + await expect(mnemonic.productItemsImg.nth(0)).toHaveAttribute('src', '/assets/img/acrobat-pro.svg'); + await expect(mnemonic.productItems.nth(2)).toContainText(data.trackingHeader3); + await expect(mnemonic.productItemsImg.nth(1)).toHaveAttribute('src', '/assets/img/illustrator.svg'); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(mnemonic.mnemonicList).toHaveAttribute('daa-lh', 'b1|mnemonic-list'); + }); + + await test.step('step-4: Verify the accessibility test on the Mnemonic List', async () => { + await runAccessibilityTest({ page, testScope: mnemonic.mnemonicList }); + }); + }); +}); From b1303876b6416621f4f243d7c95a183dde57b9d1 Mon Sep 17 00:00:00 2001 From: Bozo Jovicic <37440641+bozojovicic@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:35:43 +0200 Subject: [PATCH 08/12] =?UTF-8?q?MWPW-174199=20M@S=20Plans=20Page:=20tradi?= =?UTF-8?q?tional=20Milo=20modals=20do=20not=20open=20from=20=E2=80=A6=20(?= =?UTF-8?q?#4371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MWPW-174199 M@S Plans Page: traditional Milo modals do not open from M@S generated merch cards * MWPW-174199 M@S Plans Page: traditional Milo modals do not open from M@S generated merch cards * Trigger Build * MWPW-174199 M@S Plans Page: traditional Milo modals do not open from M@S generated merch cards * MWPW-174199 unit tests * Trigger Build * MWPW-174199 Make preview URLs relative * MWPW-174199 M@S Plans Page: traditional Milo modals do not open from M@S generated merch cards * MWPW-174199 Lint * Trigger Build * Trigger Build * Trigger Build * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * Trigger Build * MWPW-174199 Test * Trigger Build * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * MWPW-174199 Test * Trigger Build * MWPW-174199 Test --------- Co-authored-by: Bozo Jovicic --- .../merch-card-autoblock.js | 2 ++ .../merch-card-collection-autoblock.js | 2 ++ libs/blocks/merch/autoblock.js | 22 ++++++++++++++ .../merch-card-autoblock.test.js | 9 ++++++ .../merch-card-autoblock/mocks/fragment.json | 2 +- test/blocks/merch/merch.test.js | 30 +++++++++++++++++++ 6 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 libs/blocks/merch/autoblock.js diff --git a/libs/blocks/merch-card-autoblock/merch-card-autoblock.js b/libs/blocks/merch-card-autoblock/merch-card-autoblock.js index 905217ebd39..c1678b088bf 100644 --- a/libs/blocks/merch-card-autoblock/merch-card-autoblock.js +++ b/libs/blocks/merch-card-autoblock/merch-card-autoblock.js @@ -2,6 +2,7 @@ import { createTag } from '../../utils/utils.js'; import '../../deps/mas/merch-card.js'; import '../../deps/mas/merch-quantity-select.js'; import { initService, getOptions, overrideOptions } from '../merch/merch.js'; +import { postProcessAutoblock } from '../merch/autoblock.js'; const CARD_AUTOBLOCK_TIMEOUT = 5000; let log; @@ -38,6 +39,7 @@ export async function createCard(el, options) { const merchCard = createTag('merch-card', { consonant: '' }, aemFragment); el.replaceWith(merchCard); await checkReady(merchCard); + postProcessAutoblock(merchCard); } export default async function init(el) { diff --git a/libs/blocks/merch-card-collection-autoblock/merch-card-collection-autoblock.js b/libs/blocks/merch-card-collection-autoblock/merch-card-collection-autoblock.js index 1b241eedd8b..f58d675dbe9 100644 --- a/libs/blocks/merch-card-collection-autoblock/merch-card-collection-autoblock.js +++ b/libs/blocks/merch-card-collection-autoblock/merch-card-collection-autoblock.js @@ -1,5 +1,6 @@ import { createTag, getConfig } from '../../utils/utils.js'; import { initService, getOptions, MEP_SELECTOR, overrideOptions } from '../merch/merch.js'; +import { postProcessAutoblock } from '../merch/autoblock.js'; import '../../deps/mas/merch-card.js'; import '../../deps/mas/merch-quantity-select.js'; @@ -154,6 +155,7 @@ export async function createCollection(el, options) { } } + postProcessAutoblock(collection); collection.requestUpdate(); } diff --git a/libs/blocks/merch/autoblock.js b/libs/blocks/merch/autoblock.js new file mode 100644 index 00000000000..dae75f541e5 --- /dev/null +++ b/libs/blocks/merch/autoblock.js @@ -0,0 +1,22 @@ +import { decorateLinks, loadBlock, localizeLink } from '../../utils/utils.js'; + +export function localizePreviewLinks(el) { + const anchors = el.getElementsByTagName('a'); + for (const a of anchors) { + const { href } = a; + if (href?.match(/http[s]?:\/\/\S*\.(hlx|aem).(page|live)\//)) { + try { + const url = new URL(href); + a.href = localizeLink(href, url.hostname); + } catch (e) { + window.lana?.log(`Invalid URL - ${href}: ${e.toString()}`); + } + } + } +} + +export function postProcessAutoblock(autoblockEl) { + decorateLinks(autoblockEl); + localizePreviewLinks(autoblockEl); + autoblockEl.querySelectorAll('.modal.link-block').forEach((blockEl) => loadBlock(blockEl)); +} diff --git a/test/blocks/merch-card-autoblock/merch-card-autoblock.test.js b/test/blocks/merch-card-autoblock/merch-card-autoblock.test.js index 0214c4d6720..85817acc73d 100644 --- a/test/blocks/merch-card-autoblock/merch-card-autoblock.test.js +++ b/test/blocks/merch-card-autoblock/merch-card-autoblock.test.js @@ -58,6 +58,11 @@ describe('merch-card-autoblock autoblock', () => { }); it('creates card', async () => { + const config = { + codeRoot: '/libs', + autoBlocks: [{ }], + }; + setConfig(config); const content = document.createElement('div'); content.classList.add('content'); const a = document.createElement('a'); @@ -68,6 +73,10 @@ describe('merch-card-autoblock autoblock', () => { await init(a); const card = document.querySelector('merch-card'); expect(card.querySelector('[slot="heading-xs"]')?.textContent).to.equal('Creative Cloud All Apps'); + const openModalLink = card.querySelector('.should-open-modal'); + expect(openModalLink?.getAttribute('href')).to.equal('#cardmodal'); + expect(openModalLink?.getAttribute('data-modal-path')).to.equal('/test/fragments/modal'); + expect(openModalLink?.classList.contains('modal')).to.be.true; }); it('override card if mep replace tells to do', async () => { diff --git a/test/blocks/merch-card-autoblock/mocks/fragment.json b/test/blocks/merch-card-autoblock/mocks/fragment.json index cdcf9e1e7b2..1c6f774aa2b 100644 --- a/test/blocks/merch-card-autoblock/mocks/fragment.json +++ b/test/blocks/merch-card-autoblock/mocks/fragment.json @@ -16,7 +16,7 @@ }, "description": { "mimeType": "text/html", - "value": "

Annual, paid monthly

Get 20+ Creative Cloud apps, including Photoshop, Acrobat Pro, and more. Pay the first year and after that.

" + "value": "

Annual, paid monthly

Get 20+ Creative Cloud apps, including Photoshop, Acrobat Pro, and more. Pay the first year and after that.

Open modal

" }, "locReady": true, "mnemonicAlt": ["Creative Cloud All Apps"], diff --git a/test/blocks/merch/merch.test.js b/test/blocks/merch/merch.test.js index e80b21ceebf..33edbf87a4f 100644 --- a/test/blocks/merch/merch.test.js +++ b/test/blocks/merch/merch.test.js @@ -29,6 +29,7 @@ import merch, { openModal, PRICE_TEMPLATE_LEGAL, } from '../../../libs/blocks/merch/merch.js'; +import { localizePreviewLinks } from '../../../libs/blocks/merch/autoblock.js'; import { mockFetch, unmockFetch, readMockText } from './mocks/fetch.js'; import { mockIms, unmockIms } from './mocks/ims.js'; @@ -915,4 +916,33 @@ describe('Merch Block', () => { expect(getOptions(a).fragment).to.be.undefined; }); }); + describe('Localize preview links', () => { + it('check if only preview URL is relative', () => { + const div = document.createElement('div'); + + const a1 = document.createElement('a'); + a1.classList.add('link1'); + a1.setAttribute('href', 'https://main--milo--adobecom.aem.page/test/milo/path'); + div.append(a1); + + const a2 = document.createElement('a'); + a2.classList.add('link2'); + a2.setAttribute('href', 'https://main--cc--adobecom.hlx.live/test/cc/path'); + div.append(a2); + + const a3 = document.createElement('a'); + a3.classList.add('link3'); + a3.setAttribute('href', 'https://mas.adobe.com/studio.html#content-type=merch-card-collection&path=acom'); + div.append(a3); + + const aNoHref = document.createElement('a'); + div.append(aNoHref); + + localizePreviewLinks(div); + + expect(div.querySelector('.link1').getAttribute('href')).to.equal('/test/milo/path'); + expect(div.querySelector('.link2').getAttribute('href')).to.equal('/test/cc/path'); + expect(div.querySelector('.link3').getAttribute('href')).to.equal('https://mas.adobe.com/studio.html#content-type=merch-card-collection&path=acom'); + }); + }); }); From b968ff6de2322985ad875ed34dc16d224df9637a Mon Sep 17 00:00:00 2001 From: Narcis Radu Date: Thu, 26 Jun 2025 15:35:49 +0300 Subject: [PATCH 09/12] Revert "MWPW-174987: Change DOM order of elements based on viewport" (#4472) Revert "MWPW-174987: Change DOM order of elements based on viewport (#4438)" This reverts commit 9d02d5391a7aa2c26c46724840c9e47a4b7c73b1. --- libs/blocks/hero-marquee/hero-marquee.css | 12 ++++ libs/blocks/hero-marquee/hero-marquee.js | 48 ---------------- test/blocks/hero-marquee/hero-marquee.test.js | 24 -------- test/blocks/hero-marquee/mocks/body.html | 56 ------------------- 4 files changed, 12 insertions(+), 128 deletions(-) diff --git a/libs/blocks/hero-marquee/hero-marquee.css b/libs/blocks/hero-marquee/hero-marquee.css index d4e84e061b1..d9095227b35 100644 --- a/libs/blocks/hero-marquee/hero-marquee.css +++ b/libs/blocks/hero-marquee/hero-marquee.css @@ -379,6 +379,12 @@ html[dir="rtl"] .hero-marquee li.icon-item span.icon { /* min height */ .hero-marquee.s-min-height-tablet { min-height: var(--s-min-height);} .hero-marquee.l-min-height-tablet { min-height: var(--l-min-height);} + + /* helper classes */ + .hero-marquee .order-0-tablet { order: 0; } + .hero-marquee .order-1-tablet { order: 1; } + .hero-marquee .order-2-tablet { order: 2; } + .hero-marquee .order-3-tablet { order: 3; } } @media screen and (min-width: 920px) { @@ -468,4 +474,10 @@ html[dir="rtl"] .hero-marquee li.icon-item span.icon { /* min height */ .hero-marquee.s-min-height-desktop { min-height: var(--s-min-height);} .hero-marquee.l-min-height-desktop { min-height: var(--l-min-height);} + + /* helper classes */ + .hero-marquee .order-0-desktop { order: 0; } + .hero-marquee .order-1-desktop { order: 1; } + .hero-marquee .order-2-desktop { order: 2; } + .hero-marquee .order-3-desktop { order: 3; } } diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js index 1664f0cc7c2..f747559e297 100644 --- a/libs/blocks/hero-marquee/hero-marquee.js +++ b/libs/blocks/hero-marquee/hero-marquee.js @@ -166,53 +166,6 @@ function loadBreakpointThemes() { loadStyle(`${base}/styles/breakpoint-theme.css`); } -export function getViewportOrder(viewport, content, previousViewportOrder) { - const orderEls = [...content.querySelectorAll(':scope > div[class*="order-"]')]; - const nonOrderEls = [...content.querySelectorAll(':scope > div:not([class*="order-"])')]; - const viewportOrder = Array(orderEls.length).fill(null); - orderEls.forEach((el) => { - let order; - el.classList.forEach((className) => { - if (!className.startsWith('order-') || !className.endsWith(viewport)) return; - order = parseInt(className.split('-')[1], 10); - }); - if (Number.isInteger(order)) viewportOrder[order] = el; - }); - const nonEmpty = viewportOrder.every((el) => el); - return nonEmpty ? [...nonOrderEls, ...viewportOrder] : previousViewportOrder; -} - -function handleViewportOrder(content) { - const hasOrder = content.querySelector(':scope > div[class*="order-"]'); - if (!hasOrder) return; - - const mobileOrder = [...content.children]; - const tabletOrder = getViewportOrder('tablet', content, mobileOrder); - const viewports = { - mobile: { - media: '(max-width: 599px)', - elements: mobileOrder, - }, - tablet: { - media: '(min-width: 600px) and (max-width: 1199px)', - elements: tabletOrder, - }, - desktop: { - media: '(min-width: 1200px)', - elements: getViewportOrder('desktop', content, tabletOrder), - }, - }; - - Object.entries(viewports).forEach(([viewport, { media, elements }]) => { - const mediaQuery = window.matchMedia(media); - if (mediaQuery.matches && viewport !== 'mobile') content.replaceChildren(...elements); - mediaQuery.addEventListener('change', (e) => { - if (!e.matches) return; - content.replaceChildren(...elements); - }); - }); -} - export default async function init(el) { el.classList.add('con-block'); let rows = el.querySelectorAll(':scope > div'); @@ -315,7 +268,6 @@ export default async function init(el) { } }); decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy); - handleViewportOrder(copy); if (el.classList.contains('countdown-timer')) { promiseArr.push(loadCDT(copy, el.classList)); diff --git a/test/blocks/hero-marquee/hero-marquee.test.js b/test/blocks/hero-marquee/hero-marquee.test.js index b664f172b7d..063f9b7631a 100644 --- a/test/blocks/hero-marquee/hero-marquee.test.js +++ b/test/blocks/hero-marquee/hero-marquee.test.js @@ -3,7 +3,6 @@ import { expect } from '@esm-bundle/chai'; import { stub } from 'sinon'; import { waitForElement } from '../../helpers/waitfor.js'; import { setConfig } from '../../../libs/utils/utils.js'; -import { getViewportOrder } from '../../../libs/blocks/hero-marquee/hero-marquee.js'; window.lana = { log: stub() }; @@ -36,27 +35,4 @@ describe('Hero Marquee', () => { const hr = await waitForElement('.has-divider'); expect(hr).to.exist; }); - it('sorts con-block elements based on order class and viewport', async () => { - const orderMarquee = document.querySelector('#hero-order'); - const orderCopy = orderMarquee.querySelector('.copy'); - const mobileOrder = [...orderCopy.children]; - const tabletOrder = getViewportOrder('tablet', orderCopy, mobileOrder); - const desktopOrder = getViewportOrder('desktop', orderCopy, tabletOrder); - expect(tabletOrder[0].classList.contains('main-copy')).to.be.true; - expect(desktopOrder[0].classList.contains('main-copy')).to.be.true; - tabletOrder.splice(0, 1); - desktopOrder.splice(0, 1); - // eslint-disable-next-line no-plusplus - for (let i = 0; i < tabletOrder.length; i++) { - expect(tabletOrder[i].classList.contains(`order-${i}-tablet`)).to.be.true; - expect(desktopOrder[i].classList.contains(`order-${i}-desktop`)).to.be.true; - } - }); - it('order of con-blocks is the same as mobile if there is no order class', async () => { - const nonOrderMarquee = document.querySelector('#hero-all'); - const nonOrderCopy = nonOrderMarquee.querySelector('.copy'); - const mobileOrder = [...nonOrderCopy.children]; - const tabletOrder = getViewportOrder('tablet', nonOrderCopy, mobileOrder); - for (const [index, el] of tabletOrder.entries()) expect(el === mobileOrder[index]).to.be.true; - }); }); diff --git a/test/blocks/hero-marquee/mocks/body.html b/test/blocks/hero-marquee/mocks/body.html index abdaf013d73..013471e69b1 100644 --- a/test/blocks/hero-marquee/mocks/body.html +++ b/test/blocks/hero-marquee/mocks/body.html @@ -121,59 +121,3 @@

Hero w/ Adobe.tv link

- -
-
-
- - - -
-
- - - -
-
- - - -
-
-
-
con-block-row-lockup (xl-icon-size, order-3-tablet, order-2-desktop)
-
XL Icon Size
-
-
-
con-block-row-lockup (xl-lockup, order-1-tablet, order-3-desktop)
-
XL Icon Size
-
-
-
-

After Effects

-

DETAIL TEXT

-

This Hero has all row types

-

lockup, list, qrcode, text, background

-

See more Other options you say?

-
-
-
-
con-block-row-list (max-width-6-tablet, order-0-tablet, order-1-desktop)
-
-
    -
  • Small
  • -
  • Medium length text
  • -
  • Long length text that may break onto a new line, what will happen, keep it going so this is even longer and really wraps?
  • -
  • Another list
  • -
-
-
-
-
con-block-row-qrcode(order-2-tablet, order-0-desktop)
- -
-
From 02b63cfd5c9d30cf293e9da2ba916ffcd2a6ea29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilyas=20T=C3=BCrkben?= Date: Thu, 26 Jun 2025 23:40:15 +0200 Subject: [PATCH 10/12] MWPW-175654: preload ucv3 script (#4477) * MWPW-175654: preload ucv3 script ucv3 script doesn't support modern preload and requires an id attribute on the script element therefore we need to use loadScript instead of loadLink * add defer * use window.milo.deferredPromise --- libs/blocks/merch/merch.js | 10 +++++++--- libs/utils/utils.js | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/blocks/merch/merch.js b/libs/blocks/merch/merch.js index 999b6f12a32..e19d7045844 100644 --- a/libs/blocks/merch/merch.js +++ b/libs/blocks/merch/merch.js @@ -1,6 +1,6 @@ import { createTag, getConfig, loadArea, loadScript, loadStyle, localizeLink, SLD, getMetadata, - loadLink, shouldAllowKrTrial, + shouldAllowKrTrial, } from '../../utils/utils.js'; import { replaceKey } from '../../features/placeholders.js'; @@ -316,7 +316,7 @@ export function getMasBase(hostname, maslibs) { function getCommercePreloadUrl() { const { env } = getConfig(); - if (env === 'prod') { + if (env.name === 'prod') { return 'https://commerce.adobe.com/store/iframe/preload.js'; } return 'https://commerce-stg.adobe.com/store/iframe/preload.js'; @@ -622,7 +622,11 @@ export async function getModalAction(offers, options, el) { // The script can preload more, based on clientId, but for the ones in use // ('mini-plans', 'creative') there is no difference, so we can just use either one. const client = 'creative'; - loadLink(`${baseUrl}?cli=${client}`, 'text/javascript', { id: 'ucv3-preload-script', as: 'script', crossorigin: 'anonymous', rel: 'preload' }); + window.milo.deferredPromise.then(() => { + setTimeout(() => { + loadScript(`${baseUrl}?cli=${client}`, 'text/javascript', { mode: 'defer', id: 'ucv3-preload-script' }); + }, 1000); + }); } const [{ diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 8f693b29428..a637db67a6c 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -650,12 +650,13 @@ export function appendHtmlToLink(link) { } } -export const loadScript = (url, type, { mode } = {}) => new Promise((resolve, reject) => { +export const loadScript = (url, type, { mode, id } = {}) => new Promise((resolve, reject) => { let script = document.querySelector(`head > script[src="${url}"]`); if (!script) { const { head } = document; script = document.createElement('script'); script.setAttribute('src', url); + if (id) script.setAttribute('id', id); if (type) { script.setAttribute('type', type); } From 7fde5952f0152e446260005688683dab34b873b2 Mon Sep 17 00:00:00 2001 From: Ratko Zagorac <90400759+zagi25@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:43:42 +0200 Subject: [PATCH 11/12] Mwpw 174987 hero marquee order (#4475) * MWPW-174987: Change DOM order of elements based on viewport * MWPW-174987: Add additional check * MWPW-174987: PR feedback * MWPW-174987: Fix no-order els bug * MWPW-174987: Fix no desktop bug * MWPW-174987: Update unit tests * MWPW-174987: Fix * MWPW-174987: Remove previous --- libs/blocks/hero-marquee/hero-marquee.css | 12 ---- libs/blocks/hero-marquee/hero-marquee.js | 61 +++++++++++++++++++ test/blocks/hero-marquee/hero-marquee.test.js | 36 +++++++++++ test/blocks/hero-marquee/mocks/body.html | 3 + .../hero-marquee/mocks/no-order-marquee.html | 56 +++++++++++++++++ .../hero-marquee/mocks/order-marquee.html | 43 +++++++++++++ 6 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 test/blocks/hero-marquee/mocks/no-order-marquee.html create mode 100644 test/blocks/hero-marquee/mocks/order-marquee.html diff --git a/libs/blocks/hero-marquee/hero-marquee.css b/libs/blocks/hero-marquee/hero-marquee.css index d9095227b35..d4e84e061b1 100644 --- a/libs/blocks/hero-marquee/hero-marquee.css +++ b/libs/blocks/hero-marquee/hero-marquee.css @@ -379,12 +379,6 @@ html[dir="rtl"] .hero-marquee li.icon-item span.icon { /* min height */ .hero-marquee.s-min-height-tablet { min-height: var(--s-min-height);} .hero-marquee.l-min-height-tablet { min-height: var(--l-min-height);} - - /* helper classes */ - .hero-marquee .order-0-tablet { order: 0; } - .hero-marquee .order-1-tablet { order: 1; } - .hero-marquee .order-2-tablet { order: 2; } - .hero-marquee .order-3-tablet { order: 3; } } @media screen and (min-width: 920px) { @@ -474,10 +468,4 @@ html[dir="rtl"] .hero-marquee li.icon-item span.icon { /* min height */ .hero-marquee.s-min-height-desktop { min-height: var(--s-min-height);} .hero-marquee.l-min-height-desktop { min-height: var(--l-min-height);} - - /* helper classes */ - .hero-marquee .order-0-desktop { order: 0; } - .hero-marquee .order-1-desktop { order: 1; } - .hero-marquee .order-2-desktop { order: 2; } - .hero-marquee .order-3-desktop { order: 3; } } diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js index f747559e297..1cac8cf4277 100644 --- a/libs/blocks/hero-marquee/hero-marquee.js +++ b/libs/blocks/hero-marquee/hero-marquee.js @@ -166,6 +166,66 @@ function loadBreakpointThemes() { loadStyle(`${base}/styles/breakpoint-theme.css`); } +export function getViewportOrder(viewport, content) { + const els = [...content.children]; + const viewportObject = { 0: [] }; + els.forEach((el) => { + const orderClass = { + tablet: null, + desktop: null, + }; + el.classList.forEach((className) => { + if (!className.startsWith('order-') + || (!className.endsWith('desktop') && !className.endsWith('tablet'))) return; + orderClass.tablet = orderClass.tablet || (className.endsWith('tablet') ? className : null); + orderClass.desktop = orderClass.desktop || (className.endsWith('desktop') ? className : null); + }); + const viewportClass = orderClass[viewport] || orderClass.tablet; + const order = parseInt(viewportClass?.split('-')[1], 10); + if (Number.isInteger(order)) { + if (!viewportObject[order]) viewportObject[order] = []; + viewportObject[order].push(el); + } else { + viewportObject[0].push(el); + } + }); + + const viewportOrder = []; + Object.keys(viewportObject).sort((a, b) => a - b).forEach((key) => { + viewportOrder.push(...viewportObject[key]); + }); + return viewportOrder; +} + +function handleViewportOrder(content) { + const hasOrder = content.querySelector(':scope > div[class*="order-"]'); + if (!hasOrder) return; + + const viewports = { + mobile: { + media: '(max-width: 599px)', + elements: [...content.children], + }, + tablet: { + media: '(min-width: 600px) and (max-width: 1199px)', + elements: getViewportOrder('tablet', content), + }, + desktop: { + media: '(min-width: 1200px)', + elements: getViewportOrder('desktop', content), + }, + }; + + Object.entries(viewports).forEach(([viewport, { media, elements }]) => { + const mediaQuery = window.matchMedia(media); + if (mediaQuery.matches && viewport !== 'mobile') content.replaceChildren(...elements); + mediaQuery.addEventListener('change', (e) => { + if (!e.matches) return; + content.replaceChildren(...elements); + }); + }); +} + export default async function init(el) { el.classList.add('con-block'); let rows = el.querySelectorAll(':scope > div'); @@ -268,6 +328,7 @@ export default async function init(el) { } }); decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy); + handleViewportOrder(copy); if (el.classList.contains('countdown-timer')) { promiseArr.push(loadCDT(copy, el.classList)); diff --git a/test/blocks/hero-marquee/hero-marquee.test.js b/test/blocks/hero-marquee/hero-marquee.test.js index 063f9b7631a..cc34f622845 100644 --- a/test/blocks/hero-marquee/hero-marquee.test.js +++ b/test/blocks/hero-marquee/hero-marquee.test.js @@ -3,6 +3,7 @@ import { expect } from '@esm-bundle/chai'; import { stub } from 'sinon'; import { waitForElement } from '../../helpers/waitfor.js'; import { setConfig } from '../../../libs/utils/utils.js'; +import { getViewportOrder } from '../../../libs/blocks/hero-marquee/hero-marquee.js'; window.lana = { log: stub() }; @@ -35,4 +36,39 @@ describe('Hero Marquee', () => { const hr = await waitForElement('.has-divider'); expect(hr).to.exist; }); + it('sorts con-block elements based on order class and viewport', async () => { + const orderHero = await readFile({ path: './mocks/order-marquee.html' }); + const orderMarquee = document.querySelector('#order-hero'); + orderMarquee.innerHTML = orderHero; + const orderCopy = orderMarquee.querySelector('.copy'); + const tabletOrder = getViewportOrder('tablet', orderCopy); + const desktopOrder = getViewportOrder('desktop', orderCopy); + + expect(tabletOrder[0].classList.contains('main-copy')).to.be.true; + expect(desktopOrder[0].classList.contains('main-copy')).to.be.true; + + expect(tabletOrder[1].classList.contains('order-0-tablet')).to.be.true; + expect(desktopOrder[1].classList.contains('no-order-element')).to.be.true; + + expect(tabletOrder[2].classList.contains('no-order-element')).to.be.true; + expect(desktopOrder[2].classList.contains('order-0-desktop')).to.be.true; + + expect(tabletOrder[3].classList.contains('order-1-tablet')).to.be.true; + expect(desktopOrder[3].classList.contains('order-1-tablet')).to.be.true; + + expect(tabletOrder[4].classList.contains('order-2-tablet')).to.be.true; + expect(desktopOrder[4].classList.contains('order-1-desktop')).to.be.true; + + expect(tabletOrder[5].classList.contains('order-3-tablet')).to.be.true; + expect(desktopOrder[5].classList.contains('order-2-desktop')).to.be.true; + }); + it('order of con-blocks is the same as mobile if there is no order class', async () => { + const noOrderHero = await readFile({ path: './mocks/no-order-marquee.html' }); + const noOrderMarquee = document.querySelector('#no-order-hero'); + noOrderMarquee.innerHTML = noOrderHero; + const noOrderCopy = noOrderMarquee.querySelector('.copy'); + const mobileOrder = [...noOrderCopy.children]; + const tabletOrder = getViewportOrder('tablet', noOrderCopy); + for (const [index, el] of tabletOrder.entries()) expect(el === mobileOrder[index]).to.be.true; + }); }); diff --git a/test/blocks/hero-marquee/mocks/body.html b/test/blocks/hero-marquee/mocks/body.html index 013471e69b1..971624768c8 100644 --- a/test/blocks/hero-marquee/mocks/body.html +++ b/test/blocks/hero-marquee/mocks/body.html @@ -121,3 +121,6 @@

Hero w/ Adobe.tv link

+
+
+ diff --git a/test/blocks/hero-marquee/mocks/no-order-marquee.html b/test/blocks/hero-marquee/mocks/no-order-marquee.html new file mode 100644 index 00000000000..843d6ddbb64 --- /dev/null +++ b/test/blocks/hero-marquee/mocks/no-order-marquee.html @@ -0,0 +1,56 @@ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
XL Icon Size
+
+
XL Icon Size
+
+

After Effects

+

DETAIL TEXT

+

This Hero has all row types

+

lockup, list, qrcode, text, background

+

See more Other options you say?

+
+
+
    +
  • Small
  • +
  • Medium length text
  • +
  • Long length text that may break onto a new line, what will happen, keep it going so this is even longer and really wraps?
  • +
  • Another list
  • +
+
+
+

+
+ + +
+
+ +

Text with no button class

+
+ +
sup text
+
+
+
diff --git a/test/blocks/hero-marquee/mocks/order-marquee.html b/test/blocks/hero-marquee/mocks/order-marquee.html new file mode 100644 index 00000000000..c3e0ec70be1 --- /dev/null +++ b/test/blocks/hero-marquee/mocks/order-marquee.html @@ -0,0 +1,43 @@ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
XL Icon Size
+
+
XL Icon Size
+
+

After Effects

+

DETAIL TEXT

+

This Hero has all row types

+

lockup, list, qrcode, text, background

+

See more Other options you say?

+
+
+
    +
  • Small
  • +
  • Medium length text
  • +
  • Long length text that may break onto a new line, what will happen, keep it going so this is even longer and really wraps?
  • +
  • Another list
  • +
+
+
+
XL Icon Size
+
+
+
From 2c83023130e8ed9e06d4f6de8cf7a07e1b7dea55 Mon Sep 17 00:00:00 2001 From: Okan Sahin Date: Fri, 27 Jun 2025 11:27:50 +0200 Subject: [PATCH 12/12] fix rebase mistakes --- libs/blocks/hero-marquee/hero-marquee.css | 12 ------------ libs/blocks/hero-marquee/hero-marquee.js | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/libs/blocks/hero-marquee/hero-marquee.css b/libs/blocks/hero-marquee/hero-marquee.css index d9095227b35..d4e84e061b1 100644 --- a/libs/blocks/hero-marquee/hero-marquee.css +++ b/libs/blocks/hero-marquee/hero-marquee.css @@ -379,12 +379,6 @@ html[dir="rtl"] .hero-marquee li.icon-item span.icon { /* min height */ .hero-marquee.s-min-height-tablet { min-height: var(--s-min-height);} .hero-marquee.l-min-height-tablet { min-height: var(--l-min-height);} - - /* helper classes */ - .hero-marquee .order-0-tablet { order: 0; } - .hero-marquee .order-1-tablet { order: 1; } - .hero-marquee .order-2-tablet { order: 2; } - .hero-marquee .order-3-tablet { order: 3; } } @media screen and (min-width: 920px) { @@ -474,10 +468,4 @@ html[dir="rtl"] .hero-marquee li.icon-item span.icon { /* min height */ .hero-marquee.s-min-height-desktop { min-height: var(--s-min-height);} .hero-marquee.l-min-height-desktop { min-height: var(--l-min-height);} - - /* helper classes */ - .hero-marquee .order-0-desktop { order: 0; } - .hero-marquee .order-1-desktop { order: 1; } - .hero-marquee .order-2-desktop { order: 2; } - .hero-marquee .order-3-desktop { order: 3; } } diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js index 7494a9c9c11..1cac8cf4277 100644 --- a/libs/blocks/hero-marquee/hero-marquee.js +++ b/libs/blocks/hero-marquee/hero-marquee.js @@ -329,6 +329,7 @@ export default async function init(el) { }); decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy); handleViewportOrder(copy); + if (el.classList.contains('countdown-timer')) { promiseArr.push(loadCDT(copy, el.classList)); }