From 2be5754c346d6fe809edb1789028d5dd761ae060 Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Wed, 11 Dec 2024 02:17:50 -0700 Subject: [PATCH 01/11] MWPW-159180 [MILO][MEP] MEP Manifest Manager (#3344) * initial publish * first commit * cleaned up * fixed id dupes * tighten up accordion height, increase width * Add source * Remove shown * Fix freshManifest source * committing better version not neat * added divs to dropdowns * stab at data parse for db * refine parseMepConfig * align to db, improve form look, add event listeners * stop using block name constant * update to aws api * Unfinished code for Viv * Fix undefined manifest * Add back properties and fix small DOM issue * Add get getMepPopup * stash * save working * Confine listeners to current meplist * add url param and hash method for sharing * just omit promos out of date range * minor fixes and checks * add condition for mepHighlight * pulling in manifest info * don't show arrow on mep panel in mmm * refactor functions * Fix radio and preview buttons * Remove comment * Fix typo * rework share logic fix spacing color * fix merge conflict bug * fix merge conflict created bug * Add another pageId * Fix label click issue * Fix pencil issue * bunch of fixes * deep link * add conditional chaining * refactor and streamline * update now that pathname is sent by db * fix page search for new db structure * switch to full field name not first letter * change all to empty * top geos and top pages * remove line * fix breaking from merge * loops through filters instead of hard code * keep share button on right in mobile * no longer using form.html * default all doesn't need to be in Word * share button on top * create shareButton listeners at creation * rename functions * remove unneeded files * restore fix to update button * change served to found * add change listener to input * unit tests working * button back but not visible * button fixed * add sharebutton unit test * fix share button test * repair existing unit tests * update mmm test after other fixes * collapse all when new one opened * update to new db column names * fix issue with preview link including entire manifest url instead of just path * fix activites filter in saveToMmm * add source for mep param, fix filter for save * fix radio buttons to only use path * duplicate schedule info * mmmremovemepsource * fix page save and analyticsTitle * tweak placeholder helpful info * Fix code scanning alert no. 235: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * remove unused * tweak to suggested security fix * unit test update * curious what the security check says * revert * remove authorization * lint issue * simplify milo check * add source to default promo shell... again * update css rule for add manifest input * hide scheduled info in mmm * remove unused import and return error message --------- Co-authored-by: viloria Co-authored-by: markpadbe Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- libs/blocks/mmm/mmm.css | 467 ++++++++++++++++++ libs/blocks/mmm/mmm.js | 239 +++++++++ .../personalization/personalization.js | 37 +- libs/features/personalization/preview.css | 37 +- libs/features/personalization/preview.js | 408 ++++++++------- libs/features/personalization/promo-utils.js | 4 +- libs/utils/utils.js | 1 + test/blocks/mmm/mmm.test.js | 196 ++++++++ test/blocks/mmm/mocks/body.html | 130 +++++ test/blocks/mmm/mocks/get-page.json | 48 ++ test/blocks/mmm/mocks/get-pages.json | 37 ++ ...o-duplicate-manifests-one-from-target.json | 2 + ...icate-manifests-other-one-from-target.json | 4 +- .../features/personalization/mocks/preview.js | 8 +- test/features/personalization/preview.test.js | 66 ++- .../personalization/promo-utils.test.js | 21 +- 16 files changed, 1491 insertions(+), 214 deletions(-) create mode 100644 libs/blocks/mmm/mmm.css create mode 100644 libs/blocks/mmm/mmm.js create mode 100644 test/blocks/mmm/mmm.test.js create mode 100644 test/blocks/mmm/mocks/body.html create mode 100644 test/blocks/mmm/mocks/get-page.json create mode 100644 test/blocks/mmm/mocks/get-pages.json diff --git a/libs/blocks/mmm/mmm.css b/libs/blocks/mmm/mmm.css new file mode 100644 index 0000000000..9ee9086170 --- /dev/null +++ b/libs/blocks/mmm/mmm.css @@ -0,0 +1,467 @@ +@import '../../styles/inline.css'; + +.mmm-container { + padding: var(--spacing-m) 0; +} + +div.mmm { + display: none; +} + +dl.mmm { + margin: 0 auto; + max-width: var(--grid-container-width); + border-bottom: 1px solid var(--color-gray-500); +} + +.mmm-container.no-borders dl.mmm { + border: none; +} + +.mmm dd { + margin: 0; + padding: var(--spacing-xs); + font-size: var(--type-body-s-size); + line-height: var(--type-body-s-lh); + background: #eee; +} + +.mmm dt button { + align-items: center; + background: none; + border: 1px solid var(--color-gray-500); + border-width: 1px 0 0; + color: var(--text-color); + display: flex; + font-family: var(--body-font-family); + font-size: var(--type-heading-xs-size); + font-weight: 700; + line-height: var(--type-heading-s-lh); + padding: var(--spacing-s) var(--spacing-m) var(--spacing-s) var(--spacing-xs); + position: relative; + text-align: start; + width: 100%; + -webkit-text-size-adjust: 100%; +} + +.mmm dt button:hover { + cursor: pointer; + color: var(--color-black); +} + +.mmm-container dt button h5 { + word-wrap: break-word; + display: inline-block; + width: calc(100% - 30px); +} + +.mmm-container.no-borders .mmm dt button { + border: none; +} + +.mmm dt .mmm-heading { + margin: 0; +} + +.mmm dt .mmm-heading h5 { + margin: 0; +} + +.mmm-icon { + position: absolute; + right: var(--spacing-xs); + top: 50%; + margin-top: -6px; + width: 12px; + height: 12px; + pointer-events: none; +} + +html[dir="rtl"] .mmm-icon { + right: unset; + left: var(--spacing-xs); +} + +.mmm-icon::before, +.mmm-icon::after { + content: ""; + display: block; + position: absolute; + width: 2px; + height: 12px; + background: var(--color-gray-600); + border-radius: 2px; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + transition: all .2s ease; +} + +.mmm-icon::after { + width: 12px; + height: 2px; +} + +.mmm dt button::before { + content: ""; + display: block; + position: absolute; + width: 2px; + height: 100%; + left: 0; + top: 0; + background-color: transparent; +} + +.mmm dt button[aria-expanded="true"]::before { + background-color: var(--link-color-dark); +} + +.quiet .mmm dt button[aria-expanded="true"]::before { + display: none; +} + +.mmm dt button[aria-expanded="true"] .mmm-icon::before, +.mmm dt button[aria-expanded="true"] .mmm-icon::after { + transform: rotate(90deg); +} + +.mmm dt button[aria-expanded="true"] .mmm-icon::after { + opacity: 0; +} + +.mmm dt button:hover .mmm-icon::before, +.mmm dt button:hover .mmm-icon::after { + background: var(--color-black); +} + +.mmm dt button:focus, +.mmm dt button:hover, +.mmm dt button[aria-expanded="true"] { + background: #00000005; +} + +.mmm dd p:first-child { + margin-top: 0; +} + +/* dark */ +.dark .mmm dt button, +.darkest .mmm dt button { + color: #fff; +} + +.dark dl.mmm { + border-bottom-color: var(--color-gray-600); +} + +.dark .mmm dt button { + border-top-color: var(--color-gray-600); +} + +.dark .mmm dt button:focus, +.dark .mmm dt button:hover, +.dark .mmm dt button[aria-expanded="true"] { + background: #00000060; +} + +.dark .mmm dt button:hover .mmm-icon::before, +.dark .mmm dt button:hover .mmm-icon::after { + background: var(--color-gray-100); +} + +html[dir="rtl"] .mmm dt button { + padding: var(--spacing-s) var(--spacing-xs) var(--spacing-s) var(--spacing-m); +} + +html[dir="rtl"] .mmm dt button::before { + right: 0; +} + +.section[class*='-up'] dl.mmm { + max-width: unset; +} + +/* Editorial Variation */ +.mmm-media { + display: none; +} + +.mmm-media > div { + position: relative; + display: none; + animation-duration: 1s; + animation-name: fade-in; +} + + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.mmm-media > div.expanded, +.mmm-media > div.expanded > img { + display: inline; + position: relative; + height: 525px; + max-height: 525px; + max-width: 700px; + width: auto; +} + +div.media-p { + width: 268px; + padding: 0; +} + +dd .mep-popup-header .mep-close { + display: none; +} +@media screen and (min-width: 1200px) { + .editorial { + display: flex; + gap: 54px; + align-items: center; + justify-content: center; + } + + .editorial .mmm { + width: 50%; + display: inline-block; + margin: 0; + } + + .media-p { + display: none; + } + + .mmm-media { + width: 700px; + height: 525px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + margin: 0; + } +} + +.mmm-container .filter-hide { + display: none; +} + +.mmm-form-container { + display: flex; + justify-content: center; + gap: var(--spacing-s); + margin: var(--spacing-m) auto 0; + width: 800px; + flex-flow: row nowrap; + max-width: 80vw; +} +.mmm-form-container > div { + flex: 1; +} +.mmm-form-container label { + margin-right: var(--spacing-xxs); + display: block; + font-size: var(--type-body-xs-size); +} +.mmm-form-container input, +.mmm-form-container select { + width: 100%; + padding: var(--spacing-xxs); + font-size: var(--type-body-xs-size); + padding-inline-start: 12px; + box-sizing: border-box; +} + +.loading { + width: 100px; + margin: 0 auto; +} +#mmm-shareButtonContainer { + position: absolute; + top: 0; + right: 0; +} +@media screen and (min-width: 1199px) { + .mmm-form-container { + flex-flow: row nowrap; + } + #mmm-dropdown-sub-container { + display: flex; + gap: var(--spacing-s); + } + #mmm-dropdown-container { + flex: 0 0 50%; + } + #mmm-search-query-container { + max-width: 700px; + } + #mmm-shareButtonContainer { + right: 100px; + } + #mmm-dropdown-container > div > div { + flex: 1; + } +} + + +.mmm-form-container > div.share-mmm { + margin-left: auto; + margin-right: auto; + text-align: center; + padding-top: 10px; + flex: 0; + display: flex; + align-items: flex-start; +} + +.share-mmm svg.icon { + height: 28px; + transition: filter 0.3s; + display: inline-block; + width: 28px; + margin: 0; + color: #adadad; +} + +.share-mmm a svg.icon:hover { + filter: brightness(0.7); +} + +.share-mmm p.icon-container, +.share-mmm p.icon-container a { + display: flex; + align-items: center; + padding-bottom: 0; + justify-content: center; +} + +.share-mmm p { + padding-bottom: 8px; +} + +.share-mmm p.icon-container svg { + color: #adadad; +} + +.share-mmm p.icon-container > a:not(:first-child) { + margin-inline-start: 18px; +} + +.share-mmm .copy-to-clipboard { + padding: 0; + background: none; + margin: 0; + line-height: 0; + cursor: pointer; + position: relative; + transition: filter 0.3s; + border-radius: 4px; + border: 2px solid transparent; + outline: none; + margin-inline-start: 16px; +} + +.share-mmm .copy-to-clipboard:focus-visible { + border: 2px solid black; +} + +.share-mmm .copy-to-clipboard:hover svg { + filter: brightness(0.7); +} + +.share-mmm .copy-to-clipboard::before { + content: attr(data-copy-to-clipboard); + display: block; + pointer-events: none; + position : absolute; + bottom: 100%; + left: 50%; + transform: translate(-50%, -4px); + opacity: 0; + visibility: hidden; + transition: transform 130ms ease-in-out, opacity 130ms ease-in-out, visibility 0ms linear 130ms; + background-color: #6d6d6d; + color: #fff; + padding: 4px 9px 5px; + font-size: 12px; + line-height: 1.3; + border-radius: 4px; + white-space: nowrap; +} + +.share-mmm .copy-to-clipboard::after { + content: ''; + display: block; + pointer-events: none; + position : absolute; + bottom: 100%; + left: 50%; + transform: translate(-50%, 0); + opacity: 0; + visibility: hidden; + transition: transform 130ms ease-in-out, opacity 130ms ease-in-out, visibility 0ms linear 130ms; + height: 4px; + width: 8px; + background-color: #6d6d6d; + clip-path: polygon(0 -5%, 50% 100%, 100% -5%); +} + +.share-mmm .copy-to-clipboard:hover::before, +.share-mmm .copy-to-clipboard:focus::before, +.share-mmm .copy-to-clipboard:hover::after, +.share-mmm .copy-to-clipboard:focus::after { + visibility: visible; + opacity: 1; + transition-delay: 0ms; +} + +.share-mmm .copy-to-clipboard:hover::before, +.share-mmm .copy-to-clipboard:focus::before { + transform: translate(-50%, -6px); +} + +.share-mmm .copy-to-clipboard:hover::after, +.share-mmm .copy-to-clipboard:focus::after { + transform: translate(-50%, -2px); +} + +.share-mmm .copy-to-clipboard.copy-to-clipboard-copied::before { + content: attr(data-copied); +} + +.share-mmm.inline { + margin: 0 auto; + text-align: center; + padding: 8px 0 16px; +} + +.share-mmm.inline p.icon-container { + justify-content: flex-start; + margin: 0; +} + +main > .section > .share-mmm.inline { + margin: 0 auto; +} + +main > .section > .inline-wrapper > .share-mmm.inline { + margin-top: 0; + margin-bottom: 0; +} + +main > .section.center .share-mmm.inline p.icon-container { + margin: 0 auto; + justify-content: center; +} diff --git a/libs/blocks/mmm/mmm.js b/libs/blocks/mmm/mmm.js new file mode 100644 index 0000000000..f1f0e3fbf9 --- /dev/null +++ b/libs/blocks/mmm/mmm.js @@ -0,0 +1,239 @@ +import { createTag, loadStyle } from '../../utils/utils.js'; +import { fetchData, DATA_TYPE } from '../../features/personalization/personalization.js'; +import { getMepPopup, API_URLS } from '../../features/personalization/preview.js'; + +async function toggleDrawer(target, dd) { + const el = target.closest('button'); + const expanded = el.getAttribute('aria-expanded') === 'true'; + if (expanded) { + el.setAttribute('aria-expanded', 'false'); + dd.setAttribute('hidden', ''); + } else { + document.querySelectorAll('dd:not([hidden])').forEach((openDd) => { + openDd.setAttribute('hidden', ''); + }); + document.querySelectorAll('dt button[aria-expanded="true"]').forEach((openButton) => { + openButton.setAttribute('aria-expanded', 'false'); + }); + el.setAttribute('aria-expanded', 'true'); + dd.removeAttribute('hidden'); + const loading = dd.querySelector('.loading'); + if (dd.classList.contains('placeholder-resolved') || !loading) return; + const { pageId } = dd.dataset; + const pageData = await fetchData(`${API_URLS.pageDetails}${pageId}`, DATA_TYPE.JSON); + loading.replaceWith(getMepPopup(pageData, true)); + dd.classList.add('placeholder-resolved'); + } +} +function createButtonDetailsPair(mmmEl, page) { + const { url, pageId } = page; + const triggerId = `mmm-trigger-${pageId}`; + const panelId = `mmm-content-${pageId}`; + const icon = createTag('span', { class: 'mmm-icon' }); + const hTag = createTag('h5', false, url); + const button = createTag('button', { + type: 'button', + id: triggerId, + class: 'mmm-trigger tracking-header', + 'aria-expanded': 'false', + 'aria-controls': panelId, + }, hTag); + button.append(icon); + + const dtHtml = hTag ? createTag(hTag.tagName, { class: 'mmm-heading' }, button) : button; + const dt = createTag('dt', false, dtHtml); + const loading = createTag( + 'div', + { class: 'loading' }, + ` + + + + + + + + + + `, + ); + const dd = createTag('dd', { id: panelId, hidden: true }, loading); + Object.keys(page).forEach((key) => { + const val = page[key] || 'us'; + dt.dataset[key] = val; + dd.dataset[key] = val; + }); + button.addEventListener('click', (e) => { toggleDrawer(e.target, dd, pageId, 'mmm'); }); + mmmEl.append(dt, dd); +} +function filterPageList() { + const mmmEntries = document.querySelectorAll('div.mmm-container > dl > *'); + const shareUrl = new URL(`${window.location.origin}${window.location.pathname}`); + const searchValues = {}; + document.querySelectorAll('.tabs input, .tabs select').forEach((field) => { + const id = field.getAttribute('id').split('-').pop(); + const { value, tagName } = field; + searchValues[id] = { + value, + tagName, + }; + if (value) shareUrl.searchParams.set(id, value); + }); + const selectedRadio = document.querySelector('.tab-list-container button[aria-selected="true"]'); + const filterType = selectedRadio?.getAttribute('id') === 'tab-mmm-options-2' ? 'search' : 'filter'; + if (filterType === 'search') shareUrl.searchParams.set('tab', 'mmm-options-2'); + document.querySelectorAll('button.copy-to-clipboard').forEach((button) => { + button.dataset.destination = shareUrl.href; + }); + + mmmEntries.forEach((entry) => { + const data = entry.dataset; + entry.classList.remove('filter-hide'); + if (filterType === 'search') { + if (!data.url.includes(searchValues.query.value)) entry.classList.add('filter-hide'); + return; + } + Object.keys(searchValues).forEach((key) => { + const { value, tagName } = searchValues[key]; + if (tagName !== 'SELECT') return; + const inputVal = data[key]; + if (value && !value.split(',').some((val) => inputVal === val)) { + entry.classList.add('filter-hide'); + } + }); + }); +} +function parseData(el) { + const data = {}; + const rows = el.querySelectorAll('div'); + let currentKey = ''; + rows.forEach((row) => { + const cols = row.querySelectorAll('div'); + if (cols.length < 2) return; + const val = cols[1].innerText.trim(); + let key = cols[0].innerText.toLowerCase().replace(/\s+/g, ''); + if (key.startsWith('menu')) { + key = key.split(':')[1].trim(); + currentKey = key; + data[key] = { + label: val, + options: {}, + }; + return; + } + if (data[currentKey]) data[currentKey].options[key] = val; + }); + return data; +} +function createShareButton() { + const div = createTag( + 'div', + { class: 'share-mmm' }, + ); + const p = createTag('p', { class: 'icon-container' }); + div.append(p); + const buttonLabel = 'Copy link to these search settings'; + const button = createTag( + 'button', + { + type: 'button', + class: 'copy-to-clipboard', + 'aria-label': buttonLabel, + 'data-copy-to-clipboard': buttonLabel, + 'data-copied': 'Copied!', + }, + ` + + `, + ); + p.append(button); + button.addEventListener('click', (e) => { + /* c8 ignore start */ + e.preventDefault(); + navigator.clipboard.writeText(button.dataset.destination).then(() => { + button.classList.add('copy-to-clipboard-copied'); + setTimeout(() => document.activeElement.blur(), 500); + setTimeout( + () => button.classList.remove('copy-to-clipboard-copied'), + 2000, + ); + }); + /* c8 ignore end */ + }); + return div; +} +function createDropdowns(data, sharedUrlSettings) { + const dropdownTab = document.querySelector('.section-metadata.dropdowns'); + const dropdownForm = createTag( + 'div', + { id: 'mmm-dropdown-container', class: 'mmm-form-container' }, + ); + dropdownTab.parentNode.append(dropdownForm); + const dropdownSubContainer = createTag('div', { id: 'mmm-dropdown-sub-container' }); + dropdownForm.append(dropdownSubContainer); + dropdownForm.append(createShareButton()); + Object.keys(data).forEach((key) => { + const { label, options } = data[key]; + const container = createTag('div'); + dropdownSubContainer.append(container); + container.append(createTag('label', { for: `mmm-dropdown-${key}` }, `${label}:`)); + const select = createTag('select', { id: `mmm-dropdown-${key}` }); + container.append(select); + select.append(createTag('option', { value: '' }, 'Show all')); + Object.keys(options).forEach((option) => { + const optionEl = createTag('option', { value: option }, options[option]); + select.append(optionEl); + const startingVal = sharedUrlSettings[key]; + if (startingVal === option) optionEl.setAttribute('selected', 'selected'); + }); + select.addEventListener('change', filterPageList); + }); +} +function createSearchField(data, sharedUrlSettings) { + const searchTab = document.querySelector('.section-metadata.search'); + const searchForm = createTag( + 'div', + { id: 'mmm-search-query-container', class: 'mmm-form-container' }, + `
+ + +
`, + ); + searchForm.append(createShareButton()); + searchTab.parentNode.insertBefore(searchForm, searchTab); + const searchField = searchForm.querySelector('input'); + if (sharedUrlSettings.query) searchField.value = sharedUrlSettings.query; + searchField.addEventListener('keyup', filterPageList); + searchField.addEventListener('change', filterPageList); +} +async function createForm(el) { + const data = parseData(el); + const urlParams = new URLSearchParams(window.location.search); + const sharedUrlSettings = Object.fromEntries(urlParams.entries()); + createSearchField(data, sharedUrlSettings); + createDropdowns(data, sharedUrlSettings); + document.querySelectorAll('.tab-list-container button').forEach((button) => { + button.addEventListener('click', filterPageList); + }); +} +async function createPageList(el) { + const mmmElContainer = createTag('div', { class: 'mmm-container max-width-12-desktop' }); + const mmmEl = createTag('dl', { + class: 'mmm foreground', + id: 'mmm', + role: 'presentation', + }); + mmmElContainer.append(mmmEl); + const pageList = await fetchData(API_URLS.pageList, DATA_TYPE.JSON); + pageList.map((page) => createButtonDetailsPair(mmmEl, page)); + const section = createTag('div', { id: 'mep-section', class: 'section' }); + const main = document.querySelector('main'); + el.replaceWith(mmmElContainer); + main.append(section); + filterPageList(); + loadStyle('/libs/features/personalization/preview.css'); +} +export default async function init(el) { + createForm(el); + createPageList(el); +} diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index cbb87ba508..78d43035ef 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -9,6 +9,7 @@ import { getFederatedUrl } from '../../utils/federated.js'; /* c8 ignore start */ const PHONE_SIZE = window.screen.width < 550 || window.screen.height < 550; const safariIpad = navigator.userAgent.includes('Macintosh') && navigator.maxTouchPoints > 1; +export const US_GEO = 'en-us'; export const PERSONALIZATION_TAGS = { all: () => true, chrome: () => navigator.userAgent.includes('Chrome') && !navigator.userAgent.includes('Edg'), @@ -57,7 +58,7 @@ const MANIFEST_KEYS = [ 'page filter optional', ]; -const DATA_TYPE = { +export const DATA_TYPE = { JSON: 'json', TEXT: 'text', }; @@ -229,7 +230,7 @@ const log = (...msg) => { if (config.mep?.preview) console.log(...msg); }; -const fetchData = async (url, type = DATA_TYPE.JSON) => { +export const fetchData = async (url, type = DATA_TYPE.JSON) => { try { const resp = await fetch(normalizePath(url)); if (!resp.ok) { @@ -661,7 +662,7 @@ export async function createMartechMetadata(placeholders, config, column) { placeholders.forEach((item, i) => { const firstRow = placeholders[i]; - let usValue = firstRow['en-us'] || firstRow.us || firstRow.en || firstRow.key; + let usValue = firstRow[US_GEO] || firstRow.us || firstRow.en || firstRow.key; if (!usValue) return; @@ -677,7 +678,7 @@ export function parsePlaceholders(placeholders, config, selectedVariantName = '' if (!placeholders?.length || selectedVariantName === 'default') return config; const valueNames = [ selectedVariantName.toLowerCase(), - config.mep?.geoPrefix, + config.mep?.prefix, config.locale.region.toLowerCase(), config.locale.ietf.toLowerCase(), ...config.locale.ietf.toLowerCase().split('-'), @@ -813,6 +814,7 @@ const createDefaultExperiment = (manifest) => ({ selectedVariantName: 'default', variantNames: ['all'], variants: {}, + source: ['promo'], }); export const addMepAnalytics = (config, header) => { @@ -843,6 +845,7 @@ export async function getManifestConfig(info = {}, variantOverride = false) { variantLabel, disabled, event, + source, } = info; if (disabled && (!variantOverride || !Object.keys(variantOverride).length)) { return createDefaultExperiment(info); @@ -875,8 +878,10 @@ export async function getManifestConfig(info = {}, variantOverride = false) { }; if (infoTab) { manifestConfig.manifestType = infoObj?.['manifest-type']?.toLowerCase(); - if (manifestOverrideName && manifestConfig.manifestType === TRACKED_MANIFEST_TYPE) { + if (manifestConfig.manifestType === TRACKED_MANIFEST_TYPE) { manifestConfig.manifestOverrideName = manifestOverrideName; + const analytics = manifestOverrideName || getFileName(manifestPath).replace('.json', ''); + manifestConfig.analyticsTitle = analytics.trim().slice(0, 15); } const executionOrder = { 'manifest-type': 1, @@ -907,6 +912,7 @@ export async function getManifestConfig(info = {}, variantOverride = false) { manifestConfig.manifestUrl = manifestUrl; manifestConfig.disabled = disabled; manifestConfig.event = event; + if (source?.length) manifestConfig.source = source; return manifestConfig; } @@ -977,6 +983,7 @@ export function cleanAndSortManifestList(manifests) { fullManifest = manifest; freshManifest = manifestObj[manifest.manifestPath]; } + freshManifest.source = freshManifest.source.concat(fullManifest.source); freshManifest.name = fullManifest.name; freshManifest.selectedVariantName = fullManifest.selectedVariantName; freshManifest.selectedVariant = freshManifest.variants[freshManifest.selectedVariantName]; @@ -1073,10 +1080,7 @@ export async function applyPers(manifests) { const val = r.experiment.selectedVariantName.replace(TARGET_EXP_PREFIX, '').trim().slice(0, 15); return val === 'default' ? 'nopzn' : val; }); - const pznManifests = pznList.map((r) => { - const val = r.experiment?.manifestOverrideName || r.experiment?.manifest; - return getFileName(val).replace('.json', '').trim().slice(0, 15); - }); + const pznManifests = pznList.map((r) => r.experiment.analyticsTitle); config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; } @@ -1087,7 +1091,7 @@ export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => persManifests = persEnabled.toLowerCase() .split(/,|(\s+)|(\\n)/g) .filter((path) => path?.trim()) - .map((manifestPath) => ({ manifestPath })); + .map((manifestPath) => ({ manifestPath, source: ['pzn'] })); } if (promoEnabled) { @@ -1110,14 +1114,17 @@ export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => mepParam.split('---').forEach((manifestPair) => { const manifestPath = manifestPair.trim().toLowerCase().split('--')[0]; if (!persManifestPaths.includes(manifestPath)) { - persManifests.push({ manifestPath }); + persManifests.push({ manifestPath, source: ['mep param'] }); } }); } return persManifests; }; -function updateManifestsAndPropositions({ config, targetManifests, targetPropositions }) { +async function updateManifestsAndPropositions({ config, targetManifests, targetPropositions }) { + targetManifests.forEach((manifest) => { + manifest.source = ['target']; + }); config.mep.targetManifests = targetManifests; if (targetPropositions?.length && window._satellite) { window._satellite.track('propositionDisplay', targetPropositions); @@ -1242,7 +1249,7 @@ export async function init(enablements = {}) { highlight: (mepHighlight !== undefined && mepHighlight !== 'false'), targetEnabled: target, experiments: [], - geoPrefix: config.locale?.prefix.split('/')[1]?.toLowerCase() || 'en-us', + prefix: config.locale?.prefix.split('/')[1]?.toLowerCase() || US_GEO, }; manifests = manifests.concat(await combineMepSources(pzn, promo, mepParam)); manifests?.forEach((manifest) => { @@ -1268,6 +1275,10 @@ export async function init(enablements = {}) { if (!manifests || !manifests.length) return; try { await applyPers(manifests); + if (config.mep.preview) { + const { saveToMmm } = await import('./preview.js'); + saveToMmm(); + } } catch (e) { log(`MEP Error: ${e.toString()}`); window.lana?.log(`MEP Error: ${e.toString()}`); diff --git a/libs/features/personalization/preview.css b/libs/features/personalization/preview.css index ddfbb9630e..9dd127a6f1 100644 --- a/libs/features/personalization/preview.css +++ b/libs/features/personalization/preview.css @@ -6,7 +6,6 @@ color: #eee; font-weight: 600; font-size: 24px; - counter-reset: preview; display: block !important; } @@ -122,6 +121,9 @@ } .mep-popup { + font-size: 14px; +} +.mep-popup.in-page { position: absolute; bottom: 64px; left: -8px; @@ -133,22 +135,24 @@ } .mep-popup a.mep-edit-manifest { - position: absolute; - display: block; - right: 0; - top: 0; + position: relative; + margin-left: 6px; cursor: pointer; + color: inherit; } .mep-popup .mep-popup-header { display: flex; justify-content: space-between; padding: 0 16px; - background-color: #222; border-radius: 16px 16px 0 0; position: relative; } +.mep-popup.in-page .mep-popup-header { + background-color: #222; +} + .mep-popup .mep-popup-header > div { margin: 8px 0; } @@ -175,7 +179,7 @@ margin: 0; } -.mep-popup::before { +.mep-popup.in-page::before { content: ''; width: 0; height: 0; @@ -220,7 +224,7 @@ padding: 0 12px 0 26px; } -.mep-popup .dark { +.mep-popup .advanced-options { display: flex; justify-content: flex-end; padding: 15px; @@ -230,6 +234,10 @@ font-weight: 500; } +.mep-popup .target-activity-name { + font-style: italic; +} + label.mep-manifest-selected-variant::after { content: ' *'; } @@ -244,17 +252,16 @@ label.mep-manifest-selected-variant::after { padding-right: 20px; } -.mep-manifest-title::before { - counter-increment: preview; - content: counter(preview) '. '; -} - -.mep-manifest-list { +.mep-popup.in-page .mep-manifest-list { max-height: calc(100vh - 300px); overflow-y: auto; } -input#new-manifest { +.mep-popup:not(.in-page) .promo-schedule-info { + display: none; +} + +.mep-popup input.new-manifest { height: 28px; width: 98%; margin-bottom: 5px; diff --git a/libs/features/personalization/preview.js b/libs/features/personalization/preview.js index 39199e0a8d..ba2084d31d 100644 --- a/libs/features/personalization/preview.js +++ b/libs/features/personalization/preview.js @@ -1,15 +1,22 @@ import { createTag, getConfig, getMetadata, loadStyle } from '../../utils/utils.js'; -import { TRACKED_MANIFEST_TYPE, getFileName } from './personalization.js'; +import { US_GEO, getFileName, normalizePath } from './personalization.js'; -function updatePreviewButton() { - const selectedInputs = document.querySelectorAll( - '.mep-popup input[type="radio"]:checked, .mep-popup input[type="text"]', +const API_DOMAIN = 'https://jvdtssh5lkvwwi4y3kbletjmvu0qctxj.lambda-url.us-west-2.on.aws'; +export const API_URLS = { + pageList: `${API_DOMAIN}/get-pages`, + pageDetails: `${API_DOMAIN}/get-page?id=`, + save: `${API_DOMAIN}/save-mep-call`, +}; + +function updatePreviewButton(popup, pageId) { + const selectedInputs = popup.querySelectorAll( + 'input[type="radio"]:checked, input[type="text"]', ); const manifestParameter = []; selectedInputs.forEach((selected) => { let { value } = selected; - if (selected.getAttribute('id') === 'new-manifest') { + if (selected.classList.contains('new-manifest')) { if (selected.value !== '') { try { const newManifest = new URL(value); @@ -22,17 +29,20 @@ function updatePreviewButton() { manifestParameter.push(value); } } else { - value = `${selected.getAttribute('name')}--${value}`; + value = `${selected.dataset.manifest}--${value}`; manifestParameter.push(value); } }); - const simulateHref = new URL(window.location.href); + const isMmm = pageId.length; + const page = isMmm ? popup.closest('[data-url]').dataset.url : window.location.href; + const simulateHref = new URL(page); simulateHref.searchParams.set('mep', manifestParameter.join('---')); - const mepHighlightCheckbox = document.querySelector( - '.mep-popup input[type="checkbox"]#mepHighlightCheckbox', + const mepHighlightCheckbox = popup.querySelector( + `input[type="checkbox"]#mepHighlightCheckbox${pageId}`, ); + document.body.dataset.mepHighlight = mepHighlightCheckbox.checked; if (mepHighlightCheckbox.checked) { simulateHref.searchParams.set('mepHighlight', true); @@ -40,8 +50,8 @@ function updatePreviewButton() { simulateHref.searchParams.delete('mepHighlight'); } - const mepPreviewButtonCheckbox = document.querySelector( - '.mep-popup input[type="checkbox"]#mepPreviewButtonCheckbox', + const mepPreviewButtonCheckbox = popup.querySelector( + `input[type="checkbox"]#mepPreviewButtonCheckbox${pageId}`, ); if (mepPreviewButtonCheckbox.checked) { simulateHref.searchParams.set('mepButton', 'off'); @@ -49,103 +59,109 @@ function updatePreviewButton() { simulateHref.searchParams.delete('mepButton'); } - document - .querySelector('.mep-popup a.con-button') + popup + .querySelector('a.con-button') .setAttribute('href', simulateHref.href); } - -function getRepo() { - const [, repo] = new URL(window.location.href).hostname.split('--'); - if (repo) return repo; - try { - const sidekick = document.querySelector('aem-sidekick, helix-sidekick'); - if (sidekick) { - const [, sidekickRepo] = new URL(JSON.parse(sidekick.getAttribute('status'))?.live.url).hostname.split('--'); - return sidekickRepo; - } - } catch (e) { - // eslint-disable-next-line no-console - console.log('Error getting repo from sidekick', e); - } - return false; -} - -async function getEditManifestUrl(a, w) { - const repo = getRepo(); - if (!repo) { - w.location = a.dataset.manifestPath; - return false; - } - const response = await fetch(`https://admin.hlx.page/status/adobecom/${repo}/main${a.dataset.manifestPath}?editUrl=auto`); - const body = await response.json(); - const editUrl = body?.edit?.url; - if (editUrl) { - w.location = editUrl; - a.href = editUrl; - return true; - } - w.location = a.dataset.manifestUrl; - return false; -} - function addPillEventListeners(div) { - div.querySelectorAll('.mep-popup input[type="radio"], .mep-popup input[type="checkbox"]').forEach((input) => { - input.addEventListener('change', updatePreviewButton); - }); - - div.querySelectorAll('.mep-popup input[type="text"]').forEach((input) => { - input.addEventListener('keyup', updatePreviewButton); - }); - div.querySelector('.mep-manifest.mep-badge').addEventListener('click', () => { div.classList.toggle('mep-hidden'); }); - div.querySelector('.mep-close').addEventListener('click', () => { document.body.removeChild(document.querySelector('.mep-preview-overlay')); }); - - div.querySelector('.mep-toggle-advanced').addEventListener('click', () => { - document.querySelector('.mep-advanced-container').classList.toggle('mep-advanced-open'); +} +function parseMepConfig() { + const config = getConfig(); + const { mep, locale, stageDomainsMap, env } = config; + const { experiments, targetEnabled, prefix, highlight } = mep; + const activities = experiments.map((experiment) => { + const { + name, event, manifest, variantNames, selectedVariantName, disabled, analyticsTitle, source, + } = experiment; + let pathname = manifest; + try { pathname = new URL(manifest).pathname; } catch (e) { /* do nothing */ } + return { + targetActivityName: name, + variantNames, + selectedVariantName, + url: manifest, + disabled, + source, + eventStart: event?.startUtc, + eventEnd: event?.endUtc, + pathname, + analyticsTitle, + }; }); - - div.querySelectorAll('a[data-manifest-path]').forEach((a) => { - a.addEventListener('click', () => { - if (a.getAttribute('href')) return false; - const w = window.open('', '_blank'); - w.document.write(` - Please wait while we redirect you. - If you are not redirected, please check that you are signed into the AEM sidekick - and try again. - `); - w.document.close(); - w.focus(); - getEditManifestUrl(a, w); - return true; - }); + const { pathname, origin } = window.location; + let page = pathname; + let domain = origin; + if (env?.name !== 'prod' && stageDomainsMap) { + const allowedHosts = [ + 'business.stage.adobe.com', + 'www.stage.adobe.com', + 'milo.stage.adobe.com', + ]; + const domainCheck = Object.keys(stageDomainsMap) + .find((key) => { + try { + const { host } = new URL(`https://${key}`); + return allowedHosts.includes(host); + } catch (e) { + return false; + } + }); + if (domainCheck) domain = `https://${domainCheck}`; + page = page.replace('/homepage/index-loggedout', '/'); + if (!page.endsWith('/') && !domain.includes('milo')) page += '.html'; + } + domain = domain.replace('stage.adobe.com', 'adobe.com'); + const url = `${domain}${page}`; + return { + page: { + url, + page, + target: targetEnabled ? 'on' : 'off', + personalization: (getMetadata('personalization')) ? 'on' : 'off', + geo: prefix === US_GEO ? '' : prefix, + locale: locale.ietf, + region: locale.region, + highlight, + }, + activities, + }; +} +function formatDate(dateTime) { + if (!dateTime) return ''; + const date = dateTime.toLocaleDateString(false, { + year: 'numeric', + month: 'short', + day: 'numeric', }); + const time = dateTime.toLocaleTimeString(false, { timeStyle: 'short' }); + return `${date} ${time}`; } - -function createPreviewPill(manifests) { - const overlay = createTag('div', { class: 'mep-preview-overlay static-links', style: 'display: none;' }); - document.body.append(overlay); - const div = document.createElement('div'); - div.classList.add('mep-hidden'); +function getManifestListDomAndParameter(mepConfig) { + const { activities, page } = mepConfig; + const { pageId = 1 } = page; let manifestList = ''; const manifestParameter = []; - manifests?.forEach((manifest) => { + activities?.forEach((manifest, mIdx) => { const { variantNames, - manifestPath = manifest.manifest, - selectedVariantName, - name, - manifestType, + manifestPath = manifest.url, + selectedVariantName = manifest.selectedVariantName || 'default', manifestUrl, - manifestOverrideName, + targetActivityName, + source, + analyticsTitle, } = manifest; const editUrl = manifestUrl || manifestPath; + const editPath = normalizePath(editUrl); let radio = ''; - variantNames.forEach((variant) => { + const variantNamesArray = typeof variantNames === 'string' ? variantNames.split('||') : variantNames; + variantNamesArray.forEach((variant) => { const checked = { attribute: '', class: '', @@ -156,8 +172,9 @@ function createPreviewPill(manifests) { manifestParameter.push(`${manifestPath}--${variant}`); } radio += `
- - + +
`; }); const checked = { @@ -167,110 +184,139 @@ function createPreviewPill(manifests) { if (!variantNames.includes(selectedVariantName)) { checked.attribute = 'checked="checked"'; checked.class = 'class="mep-manifest-selected-variant"'; - manifestParameter.push(`${manifestPath}--default`); + manifestParameter.push(`${editUrl}--default`); } radio += `
- - + +
`; - const manifestFileName = getFileName(manifestPath); - const targetTitle = name ? `${name}
${manifestFileName}` : manifestFileName; - const scheduled = manifest.event - ? `

Scheduled - ${manifest.disabled ? 'inactive' : 'active'}

-

On: ${manifest.event.start?.toLocaleString()} - instant

-

Off: ${manifest.event.end?.toLocaleString()}

` : ''; - let analyticsTitle = ''; - if (manifestType !== TRACKED_MANIFEST_TYPE) { - analyticsTitle = 'N/A for this manifest type'; - } else if (manifestOverrideName) { - analyticsTitle = manifestOverrideName; - } else { - analyticsTitle = manifestFileName.replace('.json', '').slice(0, 20); + if (manifest.eventStart) { + manifest.event = { + start: new Date(manifest.eventStart), + end: new Date(manifest.eventEnd), + }; } - manifestList += `
+ const scheduled = manifest.event + ? `

Scheduled - ${manifest.disabled ? 'inactive' : 'active'}

+

On: ${formatDate(manifest.event.start)} - instant

+

Off: ${formatDate(manifest.event.end)}

` : ''; + manifestList += `
- ${targetTitle} - - - + ${mIdx + 1}. ${getFileName(manifestPath)} + + +
${targetActivityName || ''}
+
Source: ${source}
+ ${manifest.lastSeen ? `
Last seen: ${formatDate(new Date(manifest.lastSeen))}
` : ''} ${scheduled}
${radio}
`; }); - const config = getConfig(); - let targetOnText = config.mep.targetEnabled ? 'on' : 'off'; - if (config.mep.targetEnabled === 'postlcp') targetOnText = 'on post LCP'; - const personalizationOn = getMetadata('personalization'); - const personalizationOnText = personalizationOn && personalizationOn !== '' ? 'on' : 'off'; - const simulateHref = new URL(window.location.href); - simulateHref.searchParams.set('mep', manifestParameter.join('---')); - + return { manifestList, manifestParameter }; +} +function addMepPopupListeners(popup, pageId) { + popup.querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach((input) => { + input.addEventListener('change', updatePreviewButton.bind(null, popup, pageId)); + }); + popup.querySelectorAll('input[type="text"]').forEach((input) => { + input.addEventListener('keyup', updatePreviewButton.bind(null, popup, pageId)); + input.addEventListener('change', updatePreviewButton.bind(null, popup, pageId)); + }); + popup.querySelector('.mep-toggle-advanced').addEventListener('click', (e) => { + e.target.closest('.mep-popup')?.querySelector('.mep-advanced-container')?.classList.toggle('mep-advanced-open'); + }); +} +export function getMepPopup(mepConfig, isMmm = false) { + const { activities, page } = mepConfig; + const pageId = page?.pageId ? `-${page.pageId}` : ''; + const { manifestList } = getManifestListDomAndParameter(mepConfig); let mepHighlightChecked = ''; - if (config.mep?.highlight) { + if (page?.highlight) { mepHighlightChecked = 'checked="checked"'; document.body.dataset.mepHighlight = true; } - const PREVIEW_BUTTON_ID = 'preview-button'; + const pageUrl = isMmm ? page.url : new URL(window.location.href).href; + const mepPopup = createTag('div', { + class: `mep-popup${isMmm ? '' : ' in-page'}`, + 'data-url': pageUrl, + }); + const mepPopupHeader = createTag('div', { class: 'mep-popup-header' }); + const listInfo = createTag('div', { class: 'mep-manifest-info' }); + const advancedOptions = createTag('div', { class: 'mep-advanced-container' }); + const mepManifestList = createTag('div', { class: 'mep-manifest-list' }); - div.innerHTML = ` -
- -
${manifests?.length || 0} Manifest(s) served
-
-
-
-
-

${manifests?.length || 0} Manifest(s) served

- -
Page Info:
-
Target integration feature is ${targetOnText}
-
Personalization feature is ${personalizationOnText}
-
Page's Prefix/Region/Locale are ${config.mep.geoPrefix} / ${config.locale.region} / ${config.locale.ietf}
-
-
-
-
-
- -
-
- ${manifestList} -
-
Advanced options
-
-
- Optional: new manifest location or path -
+ listInfo.innerHTML = ` +
+ + +
`; + advancedOptions.innerHTML = ` +
Advanced options
+
+
Optional: new manifest location or path
- +
- + +
-
-
-
- Preview -
-
`; +
`; - const previewButton = div.querySelector(`a[data-id="${PREVIEW_BUTTON_ID}"]`); - - if (previewButton) previewButton.href = simulateHref.href; - - overlay.append(div); - addPillEventListeners(div); + const mepManifestPreviewButton = createTag('div', { class: `advanced-options${isMmm ? '' : ' dark'}` }); + mepManifestPreviewButton.innerHTML = ` + Preview`; + const targetOnText = page.target === 'postlcp' ? 'on post LCP' : page.target; + mepPopupHeader.innerHTML = ` +
+

${activities?.length || 0} Manifest(s) found

+ +
Page Info:
+
Target integration feature is ${targetOnText}
+
Personalization feature is ${page.personalization}
+
Page's Geo Folder is ${page.geo || 'nothing (US)'}
+
Page's Locale is ${page.locale?.toLowerCase()}
+ ${page.lastSeen ? `
Last seen: ${formatDate(new Date(page.lastSeen))}
` : ''} +
`; + mepManifestList.innerHTML = manifestList; + if (listInfo) mepManifestList.prepend(listInfo); + if (advancedOptions) mepManifestList.append(advancedOptions); + mepPopup.append(mepPopupHeader); + mepPopup.append(mepManifestList); + mepPopup.append(mepManifestPreviewButton); + const previewButton = mepPopup.querySelector(`a[data-id="${PREVIEW_BUTTON_ID}"]`); + if (previewButton) updatePreviewButton(mepPopup, pageId); + addMepPopupListeners(mepPopup, pageId); + return mepPopup; +} +function createPreviewPill() { + const mepConfig = parseMepConfig(); + const { activities } = mepConfig; + const overlay = createTag('div', { class: 'mep-preview-overlay static-links', style: 'display: none;' }); + const pill = document.createElement('div'); + pill.classList.add('mep-hidden'); + const mepBadge = createTag('div', { class: 'mep-manifest mep-badge' }); + mepBadge.innerHTML = ` + +
${activities?.length || 0} Manifest(s) found
`; + pill.append(mepBadge); + pill.append(getMepPopup(mepConfig)); + overlay.append(pill); + document.body.append(overlay); + addPillEventListeners(pill); } - function addHighlightData(manifests) { manifests.forEach(({ selectedVariant, manifest }) => { const manifestName = getFileName(manifest); @@ -296,10 +342,38 @@ function addHighlightData(manifests) { .forEach((el) => (el.dataset.manifestId = manifestName)); }); } - +export async function saveToMmm() { + const data = parseMepConfig(); + if (data.page.url.includes('/drafts/')) return false; + data.activities = data.activities.filter((activity) => { + const { url, source } = activity; + activity.source = source.filter((item) => item !== 'mep param'); + return (!!(activity.source?.length && !url.includes('/drafts/'))); + }); + data.activities = data.activities.map((activity) => { + activity.variantNames = activity.variantNames?.join('||') || ''; + activity.source = activity.source?.join(',') || ''; + delete activity.selectedVariantName; + return activity; + }); + if (data.page.prefix === US_GEO) data.page.prefix = ''; + data.page.target = getMetadata('target') || 'off'; + delete data.page.highlight; + return fetch(API_URLS.save, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + .then(async (response) => { + const res = await response.json(); + if (response.ok) return res; + /* c8 ignore next 1 */ + throw new Error(res.message || 'Network response failed'); + }); +} export default async function decoratePreviewMode() { const { miloLibs, codeRoot, mep } = getConfig(); loadStyle(`${miloLibs || codeRoot}/features/personalization/preview.css`); - createPreviewPill(mep?.experiments); + createPreviewPill(); if (mep?.experiments) addHighlightData(mep.experiments); } diff --git a/libs/features/personalization/promo-utils.js b/libs/features/personalization/promo-utils.js index 46bd624595..999f868683 100644 --- a/libs/features/personalization/promo-utils.js +++ b/libs/features/personalization/promo-utils.js @@ -42,11 +42,13 @@ const getRegionalPromoManifests = (manifestNames, region, searchParams) => { name, start: GMTStringToLocalDate(start), end: GMTStringToLocalDate(end), + startUtc: start, + endUtc: end, cdtStart, cdtEnd, }; const disabled = isDisabled(event, searchParams); - return { manifestPath, disabled, event }; + return { manifestPath, disabled, event, source: ['promo'] }; } return null; }) diff --git a/libs/utils/utils.js b/libs/utils/utils.js index c0c6ddefbb..c3f2881f38 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -53,6 +53,7 @@ const MILO_BLOCKS = [ 'merch-card', 'merch-card-collection', 'merch-offers', + 'mmm', 'mnemonic-list', 'mobile-app-banner', 'modal', diff --git a/test/blocks/mmm/mmm.test.js b/test/blocks/mmm/mmm.test.js new file mode 100644 index 0000000000..56b51c4531 --- /dev/null +++ b/test/blocks/mmm/mmm.test.js @@ -0,0 +1,196 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import { stub } from 'sinon'; + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); +const getFetchPromise = (data, type = 'json') => new Promise((resolve) => { + resolve({ + ok: true, + [type]: () => data, + }); +}); + +const setFetchResponse = (data, type = 'json') => { + window.fetch = stub().returns(getFetchPromise(data, type)); +}; + +async function loadJsonAndSetResponse(jsonPath) { + let json = await readFile({ path: jsonPath }); + json = JSON.parse(json); + setFetchResponse(json); +} + +describe('MMM', () => { + before(async () => { + await loadJsonAndSetResponse('./mocks/get-pages.json'); + document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + const module = await import('../../../libs/blocks/mmm/mmm.js'); + await module.default(document.querySelector('.mmm')); + }); + + it('Renders with mmm class', async () => { + const mmmDl = document.querySelector('dl.mmm'); + expect(mmmDl).to.exist; + const mmmDt = mmmDl.querySelectorAll('dt'); + expect(mmmDt.length).to.equal(5); + expect(mmmDt[0].textContent).to.equal('https://www.adobe.com/'); + const mmmDd = mmmDl.querySelectorAll('dd'); + expect(mmmDd.length).to.equal(5); + const loading = mmmDd[0].querySelector('.loading'); + expect(loading).to.exist; + }); + + it('Expand collapse', async () => { + await loadJsonAndSetResponse('./mocks/get-page.json'); + const [firstMmmButton, secondMmmButton] = document.body.querySelectorAll('dt button'); + expect(firstMmmButton.getAttribute('aria-expanded')).to.equal('false'); + expect(secondMmmButton.getAttribute('aria-expanded')).to.equal('false'); + + // open 1st + firstMmmButton.click(); + expect(firstMmmButton.getAttribute('aria-expanded')).to.equal('true'); + expect(secondMmmButton.getAttribute('aria-expanded')).to.equal('false'); + + // open 2nd + secondMmmButton.click(); + expect(firstMmmButton.getAttribute('aria-expanded')).to.equal('false'); + expect(secondMmmButton.getAttribute('aria-expanded')).to.equal('true'); + + // close 2nd + secondMmmButton.click(); + expect(firstMmmButton.getAttribute('aria-expanded')).to.equal('false'); + expect(secondMmmButton.getAttribute('aria-expanded')).to.equal('false'); + + // re-open 1st + firstMmmButton.click(); + expect(firstMmmButton.getAttribute('aria-expanded')).to.equal('true'); + expect(secondMmmButton.getAttribute('aria-expanded')).to.equal('false'); + }); + + it('Loads page details', async () => { + const firstMmmButton = document.body.querySelector('dt button'); + await loadJsonAndSetResponse('./mocks/get-page.json'); + firstMmmButton.click(); + const firstMmmDd = document.body.querySelector('dd'); + const loading = firstMmmDd.querySelector('.loading'); + expect(loading).to.not.exist; + const mmmPopup = firstMmmDd.querySelector('.mep-popup'); + expect(mmmPopup).to.exist; + const mmmPopupHeader = mmmPopup.querySelector('.mep-popup-header'); + const h4 = mmmPopupHeader.querySelector('h4'); + expect(h4.textContent).to.equal('3 Manifest(s) found'); + expect(mmmPopupHeader.textContent).to.include('Target integration feature is on'); + expect(mmmPopupHeader.textContent).to.include('Personalization feature is on'); + expect(mmmPopupHeader.textContent).to.include('Page\'s Geo Folder is nothing (US)'); + expect(mmmPopupHeader.textContent).to.include('Page\'s Locale is en-us'); + expect(mmmPopupHeader.textContent).to.include('Last seen:'); + const mepManifestList = mmmPopup.querySelector('.mep-manifest-list'); + expect(mepManifestList).to.exist; + const mepManifestListItems = mmmPopup.querySelectorAll('.mep-manifest-list > .mep-manifest-info'); + expect(mepManifestListItems.length).to.equal(4); + const radios = mepManifestList.querySelectorAll('input[type="radio"]'); + expect(radios.length).to.equal(19); + const checkboxes = mepManifestList.querySelectorAll('input[type="checkbox"]'); + expect(checkboxes.length).to.equal(2); + const inputs = mepManifestList.querySelectorAll('input[type="text"]'); + expect(inputs.length).to.equal(1); + const manifestTitle = mepManifestList.querySelector('.mep-manifest-title'); + expect(manifestTitle.textContent).to.include('1. hp-11-15-black-friday.json'); + expect(manifestTitle.textContent).to.include('PZN | US | Homepage | Logged Out - 11/15/24 - Blk-Frdy-Cybr-Mndy'); + expect(manifestTitle.textContent).to.include('Source: target'); + expect(manifestTitle.textContent).to.include('Last seen:'); + const editButton = manifestTitle.querySelector('.mep-edit-manifest'); + expect(editButton).to.exist; + expect(editButton.href).to.equal('https://main--homepage--adobecom.hlx.page/homepage/fragments/mep/hp-11-15-black-friday.json'); + const previewButton = mmmPopup.querySelector('a[data-id="preview-button"]'); + expect(previewButton).to.exist; + }); + + it('Test preview button', async () => { + const firstMmmButton = document.body.querySelector('dt button'); + await loadJsonAndSetResponse('./mocks/get-page.json'); + firstMmmButton.click(); + const firstMmmDd = document.body.querySelector('dd'); + const mmmPopup = firstMmmDd.querySelector('.mep-popup'); + const previewButton = mmmPopup.querySelector('a[data-id="preview-button"]'); + expect(previewButton).to.exist; + expect(previewButton.href).to.include('https%3A%2F%2Fmain--homepage--adobecom.hlx.page%2Fhomepage%2Ffragments%2Fmep%2Fhp-11-15-black-friday.json--default'); + const radio = mmmPopup.querySelector('input[type="radio"][name="https://main--homepage--adobecom.hlx.page/homepage/fragments/mep/hp-11-15-black-friday.json4"][value="target-apro-twp-abdn"]'); + expect(radio).to.exist; + radio.click(); + expect(previewButton.href).to.include('https%3A%2F%2Fmain--homepage--adobecom.hlx.page%2Fhomepage%2Ffragments%2Fmep%2Fhp-11-15-black-friday.json--target-apro-twp-abdn'); + const addHighlight = mmmPopup.querySelector('#mepHighlightCheckbox-4'); + expect(addHighlight).to.exist; + addHighlight.click(); + expect(previewButton.href).to.include('mepHighlight=true'); + const toggleAdvancedContainer = mmmPopup.querySelector('.mep-advanced-container'); + expect(toggleAdvancedContainer).to.exist; + expect(toggleAdvancedContainer.className).to.equal('mep-advanced-container'); + const toggleAdvanced = mmmPopup.querySelector('.mep-toggle-advanced'); + expect(toggleAdvanced).to.exist; + toggleAdvanced.click(); + expect(toggleAdvancedContainer.className).to.equal('mep-advanced-container mep-advanced-open'); + const addButtonOff = document.querySelector('#mepPreviewButtonCheckbox-4'); + expect(addButtonOff).to.exist; + addButtonOff.click(); + expect(previewButton.href).to.include('mepButton=off'); + const newManifest = mmmPopup.querySelector('.new-manifest'); + expect(newManifest).to.exist; + newManifest.value = '/added-manifest.json'; + const event = new Event('change'); + newManifest.dispatchEvent(event); + expect(previewButton.href).to.include('%2Fadded-manifest.json'); + }); + + it('Test filters', async () => { + const showSelector = 'dt:not(.filter-hide),dd:not(.filter-hide)'; + const copyButton = document.querySelector('.copy-to-clipboard'); + expect(copyButton).to.exist; + const event = new Event('change'); + expect(document.querySelectorAll(showSelector).length).to.equal(10); + expect(copyButton.dataset.destination).to.not.include('geo'); + expect(copyButton.dataset.destination).to.not.include('page'); + expect(copyButton.dataset.destination).to.not.include('tab'); + expect(copyButton.dataset.destination).to.not.include('query'); + const geoDropdown = document.querySelector('#mmm-dropdown-geo'); + geoDropdown.options[1].selected = true; + geoDropdown.dispatchEvent(event); + expect(document.querySelectorAll(showSelector).length).to.equal(8); + expect(copyButton.dataset.destination).to.include('geo'); + expect(copyButton.dataset.destination).to.not.include('page'); + expect(copyButton.dataset.destination).to.not.include('tab'); + expect(copyButton.dataset.destination).to.not.include('query'); + const pageDropdown = document.querySelector('#mmm-dropdown-page'); + pageDropdown.options[2].selected = true; + pageDropdown.dispatchEvent(event); + expect(document.querySelectorAll(showSelector).length).to.equal(4); + expect(copyButton.dataset.destination).to.include('geo'); + expect(copyButton.dataset.destination).to.include('page'); + expect(copyButton.dataset.destination).to.not.include('tab'); + expect(copyButton.dataset.destination).to.not.include('query'); + geoDropdown.options[0].selected = true; + geoDropdown.dispatchEvent(event); + expect(document.querySelectorAll(showSelector).length).to.equal(6); + expect(copyButton.dataset.destination).to.not.include('geo'); + expect(copyButton.dataset.destination).to.include('page'); + expect(copyButton.dataset.destination).to.not.include('tab'); + expect(copyButton.dataset.destination).to.not.include('query'); + const filterTabs = document.querySelectorAll('.tab-list-container button'); + filterTabs[0].setAttribute('aria-selected', 'false'); + filterTabs[1].setAttribute('aria-selected', 'true'); + filterTabs[1].click(); + expect(document.querySelectorAll(showSelector).length).to.equal(10); + expect(copyButton.dataset.destination).to.not.include('geo'); + expect(copyButton.dataset.destination).to.include('page'); + expect(copyButton.dataset.destination).to.include('tab'); + expect(copyButton.dataset.destination).to.not.include('query'); + const mmmSearchQuery = document.querySelector('#mmm-search-query'); + mmmSearchQuery.value = 'pricing'; + mmmSearchQuery.dispatchEvent(event); + expect(document.querySelectorAll(showSelector).length).to.equal(2); + expect(copyButton.dataset.destination).to.not.include('geo'); + expect(copyButton.dataset.destination).to.include('page'); + expect(copyButton.dataset.destination).to.include('tab'); + expect(copyButton.dataset.destination).to.include('query'); + }); +}); diff --git a/test/blocks/mmm/mocks/body.html b/test/blocks/mmm/mocks/body.html new file mode 100644 index 0000000000..e8bd9bb266 --- /dev/null +++ b/test/blocks/mmm/mocks/body.html @@ -0,0 +1,130 @@ +
+
+ +
+
+
+ +
+
+ + + +
+ +
+ +
+ +
+
+
+
Menu: geo
+
Top Regions/Geos
+
+
+
us,ca,ca_fr
+
NA region: US, CA, CA_FR
+
+
+
au,nz,kr
+
APAC region: AU, NZ, KR
+
+
+
mx,br
+
LATAM region: MX, BR
+
+
+
jp
+
Japan
+
+
+
us
+
US: United States
+
+
+
ca
+
CA-EN: Canada - English
+
+
+
ca_fr
+
CA-FR: Canada - Français
+
+
+
au
+
AU: Australia
+
+
+
nz
+
NZ: New Zealand
+
+
+
kr
+
KR: Korea
+
+
+
mx
+
MX: México
+
+
+
br
+
BR: Brasil
+
+
+
Menu: page
+
Top Pages
+
+
+
+

/,

+

/creativecloud.html,

+

/products/photoshop.html,

+

/products/illustrator.html

+
+
Only top pages
+
+
+
/
+
Homepage
+
+
+
/creativecloud.html
+
CCOV
+
+
+
/products/photoshop.html
+
Photoshop product page
+
+
+
/products/illustrator.html
+
Illustrator product page
+
+
+
+
diff --git a/test/blocks/mmm/mocks/get-page.json b/test/blocks/mmm/mocks/get-page.json new file mode 100644 index 0000000000..fc4884f1e1 --- /dev/null +++ b/test/blocks/mmm/mocks/get-page.json @@ -0,0 +1,48 @@ +{ + "activities": [ + { + "eventEnd": null, + "lastSeen": "2024-12-06T01:22:42.000Z", + "variantNames": "target-apro-twp-abdn||target-cc-lapsed||target-smb||target-edu||target-dc||target-return||phone||all", + "targetActivityName": "PZN | US | Homepage | Logged Out - 11/15/24 - Blk-Frdy-Cybr-Mndy", + "eventStart": null, + "source": "target", + "manifestId": 6, + "url": "https://main--homepage--adobecom.hlx.page/homepage/fragments/mep/hp-11-15-black-friday.json", + "pathname": "/homepage/fragments/mep/hp-11-15-black-friday.json" + }, + { + "eventEnd": null, + "lastSeen": "2024-12-06T00:23:37.000Z", + "variantNames": "target-apro-twp-abdn||target-cc-lapsed||target-smb||target-edu||target-dc||target-return||phone", + "targetActivityName": null, + "eventStart": null, + "source": "pzn", + "manifestId": 15, + "url": "https://main--homepage--adobecom.hlx.page/homepage/fragments/mep/hp-12-09-24-cybr-tkdwn-ste.json", + "pathname": "/homepage/fragments/mep/hp-12-09-24-cybr-tkdwn-ste.json" + }, + { + "eventEnd": "2024-12-09T15:00:00.000Z", + "lastSeen": "2024-12-06T18:54:14.000Z", + "variantNames": "all", + "targetActivityName": null, + "eventStart": "2024-11-15T15:45:00.000Z", + "source": "promo", + "manifestId": 7, + "url": "https://main--homepage--adobecom.hlx.page/homepage/fragments/loggedout/promos/2024/bfcm/global/bfcm-cct-row-133146.json", + "pathname": "/homepage/fragments/loggedout/promos/2024/bfcm/global/bfcm-cct-row-133146.json" + } + ], + "page": { + "personalization": "on", + "lastSeen": "2024-12-06T21:02:46.000Z", + "geo": "", + "pageId": 4, + "page": "/", + "locale": "en-US", + "region": "us", + "url": "https://www.adobe.com/", + "target": "on" + } +} diff --git a/test/blocks/mmm/mocks/get-pages.json b/test/blocks/mmm/mocks/get-pages.json new file mode 100644 index 0000000000..9c07e761c8 --- /dev/null +++ b/test/blocks/mmm/mocks/get-pages.json @@ -0,0 +1,37 @@ +[ + { + "geo": "", + "pageId": 4, + "page": "/", + "locale": "en-US", + "url": "https://www.adobe.com/" + }, + { + "geo": "", + "pageId": 7, + "page": "/acrobat/pricing.html", + "locale": "en-US", + "url": "https://www.adobe.com/acrobat/pricing.html" + }, + { + "geo": "", + "pageId": 3, + "page": "/creativecloud.html", + "locale": "en-US", + "url": "https://www.adobe.com/creativecloud.html" + }, + { + "geo": "ca_fr", + "pageId": 16, + "page": "/", + "locale": "fr-CA", + "url": "https://www.adobe.com/ca_fr/" + }, + { + "geo": "de", + "pageId": 61, + "page": "/", + "locale": "de-DE", + "url": "https://www.adobe.com/de/" + } +] diff --git a/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-one-from-target.json b/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-one-from-target.json index 2436880133..d78d5ad671 100644 --- a/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-one-from-target.json +++ b/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-one-from-target.json @@ -34,6 +34,7 @@ ], "fragments": [] }, + "source": "pzn", "name": "MILO0013b - use fresh manifest over saved", "manifest": "https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", "manifestUrl": "https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", @@ -74,6 +75,7 @@ ], "fragments": [] }, + "source": "target", "manifest": "/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", "manifestUrl": "https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", "manifestPath": "/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json" diff --git a/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-other-one-from-target.json b/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-other-one-from-target.json index 351c24790a..61e5c65049 100644 --- a/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-other-one-from-target.json +++ b/test/features/personalization/mocks/manifestLists/two-duplicate-manifests-other-one-from-target.json @@ -34,6 +34,7 @@ ], "fragments": [] }, + "source": "pzn", "manifest": "/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", "manifestUrl": "https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", "manifestPath": "/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json" @@ -76,6 +77,7 @@ "name": "MILO0013b - use fresh manifest over saved", "manifest": "https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", "manifestUrl": "https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", - "manifestPath": "/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json" + "manifestPath": "/drafts/vgoodrich/fragments/2024/q1/mepmanifestorder/manifests/outdated-manifest.json", + "source": "target" } ] diff --git a/test/features/personalization/mocks/preview.js b/test/features/personalization/mocks/preview.js index 502ddb34d1..e2de7a835e 100644 --- a/test/features/personalization/mocks/preview.js +++ b/test/features/personalization/mocks/preview.js @@ -46,7 +46,7 @@ export default [ }, ], }, - manifest: '/homepage/fragments/mep/selected-example.json', + manifest: 'https://main--cc--homepage.hlx.page/homepage/fragments/mep/selected-example.json', manifestUrl: 'https://main--milo--adobecom.hlx.page/drafts/vgoodrich/fragments/unit-tests/manifest.json', }, { @@ -69,7 +69,7 @@ export default [ manifestOverrideName: 'hp', selectedVariantName: 'default', selectedVariant: 'default', - manifest: '/homepage/fragments/mep/default-selected.json', + manifest: 'https://main--homepage--adobecom.hlx.live/homepage/fragments/mep/default-selected.json', manifestUrl: 'https://main--homepage--adobecom.hlx.live/homepage/fragments/mep/default-selected.json', }, { @@ -100,7 +100,7 @@ export default [ }, ], }, - manifest: '/promos/2023/global/black-friday/black-friday-global.json', + manifest: 'https://main--cc--adobecom.hlx.page/promos/2023/global/black-friday/black-friday-global.json', manifestUrl: 'https://main--cc--adobecom.hlx.page/promos/2023/global/black-friday/black-friday-global.json', disabled: false, event: { @@ -116,7 +116,7 @@ export default [ start: new Date('2024-11-24T00:00:00.000Z'), end: new Date('2024-11-24T00:00:00.000Z'), }, - manifest: '/promos/2023/emea/cyber-monday/cyber-monday-emea.json', + manifest: 'https://main--cc--adobecom.hlx.page/promos/2023/emea/cyber-monday/cyber-monday-emea.json', variantNames: [ 'all', ], diff --git a/test/features/personalization/preview.test.js b/test/features/personalization/preview.test.js index 99407541df..10424ba5a7 100644 --- a/test/features/personalization/preview.test.js +++ b/test/features/personalization/preview.test.js @@ -9,21 +9,67 @@ const { setConfig, MILO_EVENTS } = await import('../../../libs/utils/utils.js'); const config = { miloLibs: 'https://main--milo--adobecom.hlx.live/libs', codeRoot: 'https://main--homepage--adobecom.hlx.live/homepage', + locale: { + ietf: 'en-US', + tk: 'hah7vzn.css', + prefix: '', + region: 'us', + contentRoot: 'https://main--cc--adobecom.hlx.page/cc-shared', + }, mep: { preview: true, override: '', highlight: true, + experiments: [], + targetEnabled: true, + prefix: '', + }, + stageDomainsMap: { + 'www.stage.adobe.com': { + 'www.adobe.com': 'origin', + 'business.adobe.com': 'business.stage.adobe.com', + 'helpx.adobe.com': 'helpx.stage.adobe.com', + 'blog.adobe.com': 'blog.stage.adobe.com', + 'developer.adobe.com': 'developer-stage.adobe.com', + 'news.adobe.com': 'news.stage.adobe.com', + 'firefly.adobe.com': 'firefly-stage.corp.adobe.com', + 'creativecloud.adobe.com': 'stage.creativecloud.adobe.com', + 'projectneo.adobe.com': 'stg.projectneo.adobe.com', + }, + '--cc--adobecom.hlx.live': { + 'www.adobe.com': 'origin', + 'business.adobe.com': 'business.stage.adobe.com', + 'helpx.adobe.com': 'helpx.stage.adobe.com', + 'blog.adobe.com': 'blog.stage.adobe.com', + 'developer.adobe.com': 'developer-stage.adobe.com', + 'news.adobe.com': 'news.stage.adobe.com', + 'firefly.adobe.com': 'firefly-stage.corp.adobe.com', + 'creativecloud.adobe.com': 'stage.creativecloud.adobe.com', + 'projectneo.adobe.com': 'stg.projectneo.adobe.com', + }, + '--cc--adobecom.hlx.page': { + 'www.adobe.com': 'origin', + 'business.adobe.com': 'business.stage.adobe.com', + 'helpx.adobe.com': 'helpx.stage.adobe.com', + 'blog.adobe.com': 'blog.stage.adobe.com', + 'developer.adobe.com': 'developer-stage.adobe.com', + 'news.adobe.com': 'news.stage.adobe.com', + 'firefly.adobe.com': 'firefly-stage.corp.adobe.com', + 'creativecloud.adobe.com': 'stage.creativecloud.adobe.com', + 'projectneo.adobe.com': 'stg.projectneo.adobe.com', + }, }, + env: { name: 'stage' }, }; setConfig(config); describe('preview feature', () => { - it('builds with 0 manifests', () => { - decoratePreviewMode(); + it('builds with 0 manifests', async () => { + await decoratePreviewMode(); const event = new Event(MILO_EVENTS.DEFERRED); document.dispatchEvent(event); expect(document.querySelectorAll('.mep-preview-overlay').length).to.equal(1); - expect(document.querySelector('.mep-popup-header h4').textContent).to.equal('0 Manifest(s) served'); + expect(document.querySelector('.mep-popup-header h4').textContent).to.equal('0 Manifest(s) found'); }); it('expand and close panel, expand and close advance, remove button', () => { expect(document.querySelector('.mep-preview-overlay > div').className).to.equal('mep-hidden'); @@ -38,12 +84,12 @@ describe('preview feature', () => { document.querySelector('.mep-close').click(); expect(document.querySelectorAll('.mep-preview-overlay').length).to.equal(0); }); - it('builds with multiple manifests', () => { + it('builds with multiple manifests', async () => { config.mep.experiments = experiments; setConfig(config); - decoratePreviewMode(); + await decoratePreviewMode(); expect(document.querySelectorAll('.mep-preview-overlay').length).to.equal(1); - expect(document.querySelector('.mep-popup-header h4').textContent).to.equal(`${config.mep.experiments.length} Manifest(s) served`); + expect(document.querySelector('.mep-popup-header h4').textContent).to.equal(`${config.mep.experiments.length} Manifest(s) found`); }); it('adds highlights', () => { expect(document.querySelector('[data-path="/fragments/fragmentreplaced"]').getAttribute('data-manifest-id')).to.equal('selected-example.json'); @@ -54,14 +100,14 @@ describe('preview feature', () => { expect(document.querySelector('merch-card').getAttribute('data-manifest-id')).to.equal('selected-example.json'); }); it('preselects form inputs', () => { - expect(document.querySelector('input[name="/homepage/fragments/mep/selected-example.json"][value="target-smb"]').getAttribute('checked')).to.equal('checked'); - expect(document.querySelector('input[name="/homepage/fragments/mep/default-selected.json"][value="default"]').getAttribute('checked')).to.equal('checked'); + expect(document.querySelector('input[name*="/homepage/fragments/mep/selected-example.json"][value="target-smb"]').getAttribute('checked')).to.equal('checked'); + expect(document.querySelector('input[name*="/homepage/fragments/mep/default-selected.json"][value="default"]').getAttribute('checked')).to.equal('checked'); expect(document.querySelector('input#mepHighlightCheckbox').getAttribute('checked')).to.equal('checked'); }); it('updates preview button', () => { expect(document.querySelector('a[title="Preview above choices"]').getAttribute('href')).to.contain('---'); - document.querySelector('#new-manifest').value = 'https://main--homepage--adobecom.hlx.live/homepage/fragments/mep/new-manifest.json'; - document.querySelector('input[name="/homepage/fragments/mep/selected-example.json"][value="default"]').click(); + document.querySelector('.new-manifest').value = 'https://main--homepage--adobecom.hlx.live/homepage/fragments/mep/new-manifest.json'; + document.querySelector('input[name*="/homepage/fragments/mep/selected-example.json"][value="default"]').click(); expect(document.querySelector('a[title="Preview above choices"]').getAttribute('href')).to.contain('new-manifest.json'); expect(document.querySelector('a[title="Preview above choices"]').getAttribute('href')).to.contain('%2Fhomepage%2Ffragments%2Fmep%2Fselected-example.json--default'); document.querySelector('input#mepHighlightCheckbox').click(); diff --git a/test/features/personalization/promo-utils.test.js b/test/features/personalization/promo-utils.test.js index f9fb26dffc..bacf88138f 100644 --- a/test/features/personalization/promo-utils.test.js +++ b/test/features/personalization/promo-utils.test.js @@ -78,7 +78,12 @@ describe('getPromoManifests', () => { end: new Date('2300-12-15T00:00:00.000Z'), cdtEnd: undefined, cdtStart: undefined, + startUtc: '2000-11-01T00:00:00', + endUtc: '2300-12-15T00:00:00', }, + source: [ + 'promo', + ], }, { manifestPath: 'https://main--milo--adobecom.hlx.page/promos/2023/black-friday/pre-black-friday/manifest-global.json', @@ -89,7 +94,12 @@ describe('getPromoManifests', () => { end: new Date('2300-12-15T00:00:00.000Z'), cdtEnd: undefined, cdtStart: undefined, + startUtc: '2000-11-01T00:00:00', + endUtc: '2300-12-15T00:00:00', }, + source: [ + 'promo', + ], }, { manifestPath: 'https://main--milo--adobecom.hlx.page/promos/2023/black-friday/black-friday/manifest-global.json', @@ -100,22 +110,27 @@ describe('getPromoManifests', () => { end: new Date('2000-12-31T00:00:00.000Z'), cdtEnd: '2026-08-30T00:00:00', cdtStart: '2024-08-26T12:00:00', + startUtc: '2000-12-15T00:00:00', + endUtc: '2000-12-31T00:00:00', }, + source: [ + 'promo', + ], }, ]; document.head.innerHTML = await readFile({ path: './mocks/head-schedule.html' }); const manifestnames = 'pre-black-friday-global,black-friday-global,cyber-monday'; const emea = 'bf-de'; const americas = 'bf-us'; - expect(getPromoManifests( + const manifests = getPromoManifests( { manifestnames, emea_manifestnames: emea, americas_manifestnames: americas, }, new URLSearchParams(), - )) - .to.deep.eq(expectedManifests); + ); + expect(manifests).to.deep.eq(expectedManifests); }); it('should return an empty array if no schedule', async () => { From 4c2077446f66184655d8cb9ae7f6f67740b52cdf Mon Sep 17 00:00:00 2001 From: Jason Slavin Date: Wed, 11 Dec 2024 01:17:57 -0800 Subject: [PATCH 02/11] MWPW-155723 - Adds group metadata to ensure dynamic nav continuity - fix (#3318) * Adding dynamic-nav-group setting to allow distinction between dynamic-nav user journeys * Utils change * Ensuring status shows correctly dispite group * Making metadata case insensative * Does a check for status.js if storage item does not exist --------- Co-authored-by: Megan Thomas --- .../dynamic-navigation/dynamic-navigation.js | 14 ++++ libs/features/dynamic-navigation/status.js | 10 ++- test/features/dynamic-nav/dynamicNav.test.js | 30 ++++++++ test/features/dynamic-nav/status.test.js | 70 ++++++++++++++++++- 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/libs/features/dynamic-navigation/dynamic-navigation.js b/libs/features/dynamic-navigation/dynamic-navigation.js index 7354518a58..7d943cc597 100644 --- a/libs/features/dynamic-navigation/dynamic-navigation.js +++ b/libs/features/dynamic-navigation/dynamic-navigation.js @@ -14,16 +14,30 @@ export function foundDisableValues() { return foundValues.length ? foundValues : false; } +function dynamicNavGroupMatches(groupMetaData) { + const storedGroup = window.sessionStorage.getItem('dynamicNavGroup'); + if (groupMetaData && storedGroup) { + return storedGroup.toLowerCase() === groupMetaData.toLowerCase(); + } + return false; +} + export default function dynamicNav(url, key) { if (foundDisableValues()) return url; const metadataContent = getMetadata('dynamic-nav'); + const dynamicNavGroup = getMetadata('dynamic-nav-group'); if (metadataContent === 'entry') { window.sessionStorage.setItem('gnavSource', url); window.sessionStorage.setItem('dynamicNavKey', key); + if (dynamicNavGroup) window.sessionStorage.setItem('dynamicNavGroup', dynamicNavGroup); return url; } + if (metadataContent === 'on' && dynamicNavGroup) { + if (!dynamicNavGroupMatches(dynamicNavGroup)) return url; + } + if (metadataContent !== 'on' || key !== window.sessionStorage.getItem('dynamicNavKey')) return url; return window.sessionStorage.getItem('gnavSource') || url; diff --git a/libs/features/dynamic-navigation/status.js b/libs/features/dynamic-navigation/status.js index 792f586765..489f825bec 100644 --- a/libs/features/dynamic-navigation/status.js +++ b/libs/features/dynamic-navigation/status.js @@ -80,7 +80,13 @@ const createStatusWidget = (dynamicNavKey) => { const currentSource = getCurrentSource(dynamicNavSetting, storedSource, authoredSource); const dynamicNavDisableValues = getMetadata('dynamic-nav-disable'); const foundValues = foundDisableValues(); - const status = getStatus(dynamicNavSetting, foundValues.length >= 1, storedSource); + const groupMetaSetting = getMetadata('dynamic-nav-group') || 'Group not set'; + const storedDyanmicNavGroup = window.sessionStorage.getItem('dynamicNavGroup'); + const groupsMatch = storedDyanmicNavGroup + && groupMetaSetting.toLowerCase() === storedDyanmicNavGroup.toLowerCase(); + const groupsMatchMessage = groupsMatch ? 'Yes' : 'No'; + const isDisabled = foundValues.length >= 1 || (!groupsMatch && groupMetaSetting !== 'Group not set'); + const status = getStatus(dynamicNavSetting, isDisabled, storedSource); const statusWidget = createTag('div', { class: 'dynamic-nav-status' }); statusWidget.innerHTML = ` @@ -94,6 +100,8 @@ const createStatusWidget = (dynamicNavKey) => {

Status: ${status}

Setting: ${dynamicNavSetting}

+

Group: ${groupMetaSetting}

+

Group matches stored group: ${groupsMatchMessage}

Consumer key: ${dynamicNavKey}