From 4531781a8dc80ecc6c3846f20b624543dd1b534c Mon Sep 17 00:00:00 2001 From: Bandana Laishram Date: Wed, 24 Jul 2024 13:36:18 +0530 Subject: [PATCH 1/5] Bootstrapper script for milo feds blocks (#2560) * Adding feds block bootstraper for non milo * Added css to load footer * added css to load footer * Updating default branch to main * Taking configs and params and not making it iife * Lint fix * Adding configurable env names * Lint fix * Adding origin from milo config for non milo consumers * Line fix * Fixing code * Adding support for privacy * Adding support for federated urls in fragments block to centralisation * Dynamic importing federated file and including test case * Fix to accomodate gnav component from same script * Fix for eslint * Add test case for navigation js * lint fix * Adding support for localize link in region picker * Add test case for bootstrapper js --------- Co-authored-by: Snehal Sonawane --- libs/blocks/fragment/fragment.js | 11 +- libs/blocks/global-footer/global-footer.js | 2 + libs/navigation/bootstrapper.js | 22 ++++ libs/navigation/navigation.css | 21 ++++ libs/navigation/navigation.js | 34 ++++++ libs/scripts/scripts.js | 101 +----------------- libs/utils/federated.js | 5 +- libs/utils/locales.js | 99 +++++++++++++++++ test/blocks/fragment/mocks/body.html | 1 + .../mocks/federal/fragments/frag-a.plain.html | 4 + test/navigation/bootstrapper.test.js | 32 ++++++ test/navigation/mocks/body.html | 6 ++ test/navigation/navigation.test.js | 13 +++ 13 files changed, 246 insertions(+), 105 deletions(-) create mode 100644 libs/navigation/bootstrapper.js create mode 100644 libs/navigation/navigation.css create mode 100644 libs/navigation/navigation.js create mode 100644 libs/utils/locales.js create mode 100644 test/blocks/fragment/mocks/federal/fragments/frag-a.plain.html create mode 100644 test/navigation/bootstrapper.test.js create mode 100644 test/navigation/mocks/body.html create mode 100644 test/navigation/navigation.test.js diff --git a/libs/blocks/fragment/fragment.js b/libs/blocks/fragment/fragment.js index 4aaa558edf..2bca02da83 100644 --- a/libs/blocks/fragment/fragment.js +++ b/libs/blocks/fragment/fragment.js @@ -90,11 +90,16 @@ export default async function init(a) { } const { customFetch } = await import('../../utils/helpers.js'); - const resp = await customFetch({ resource: `${a.href}.plain.html`, withCacheRules: true }) + let resourcePath = a.href; + if (a.href.includes('/federal/')) { + const { getFederatedUrl } = await import('../../utils/federated.js'); + resourcePath = getFederatedUrl(a.href); + } + const resp = await customFetch({ resource: `${resourcePath}.plain.html`, withCacheRules: true }) .catch(() => ({})); if (!resp?.ok) { - window.lana?.log(`Could not get fragment: ${a.href}.plain.html`); + window.lana?.log(`Could not get fragment: ${resourcePath}.plain.html`); return; } @@ -106,7 +111,7 @@ export default async function init(a) { const sections = doc.querySelectorAll('body > div'); if (!sections.length) { - window.lana?.log(`Could not make fragment: ${a.href}.plain.html`); + window.lana?.log(`Could not make fragment: ${resourcePath}.plain.html`); return; } diff --git a/libs/blocks/global-footer/global-footer.js b/libs/blocks/global-footer/global-footer.js index b5cb8f9b3d..29a6a4b3c2 100644 --- a/libs/blocks/global-footer/global-footer.js +++ b/libs/blocks/global-footer/global-footer.js @@ -5,6 +5,7 @@ import { getMetadata, getConfig, loadBlock, + localizeLink, } from '../../utils/utils.js'; import { @@ -258,6 +259,7 @@ class Footer { } else { // No hash -> region selector expands a dropdown regionPickerElem.href = '#'; // reset href value to not get treated as a fragment + regionSelector.href = localizeLink(regionSelector.href); decorateAutoBlock(regionSelector); // add fragment-specific class(es) this.elements.regionPicker.append(regionSelector); // add fragment after regionPickerElem await loadBlock(regionSelector); // load fragment and replace original link diff --git a/libs/navigation/bootstrapper.js b/libs/navigation/bootstrapper.js new file mode 100644 index 0000000000..e98941060b --- /dev/null +++ b/libs/navigation/bootstrapper.js @@ -0,0 +1,22 @@ +export default async function bootstrapBlock(miloConfigs, blockConfig) { + const { miloLibs } = miloConfigs; + const { name, targetEl } = blockConfig; + const { getConfig, setConfig, createTag, loadLink, loadScript } = await import(`${miloLibs}/utils/utils.js`); + setConfig({ ...miloConfigs }); + const { default: initBlock } = await import(`${miloLibs}/blocks/${name}/${name}.js`); + + const styles = [`${miloLibs}/blocks/${name}/${name}.css`, `${miloLibs}/navigation/navigation.css`]; + styles.forEach((url) => loadLink(url, { rel: 'stylesheet' })); + + if (!document.querySelector(targetEl)) { + const block = createTag(targetEl, { class: name }); + document.body[blockConfig.appendType](block); + } + initBlock(document.querySelector(targetEl)); + if (blockConfig.targetEl === 'footer') { + const { loadPrivacy } = await import(`${miloLibs}/scripts/delayed.js`); + setTimeout(() => { + loadPrivacy(getConfig, loadScript); + }, blockConfig.delay); + } +} diff --git a/libs/navigation/navigation.css b/libs/navigation/navigation.css new file mode 100644 index 0000000000..e3ef7f1241 --- /dev/null +++ b/libs/navigation/navigation.css @@ -0,0 +1,21 @@ + /* Extracting the essential styles required for rendering the component independently */ + .global-footer, .dialog-modal { + font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif; + line-height: 27px; + color: #2c2c2c; + word-wrap: break-word; + -webkit-font-smoothing: antialiased; +} + +.global-footer a, .dialog-modal a { + text-decoration: none; +} + +.dialog-modal a { + color: #035FE6; +} + +.global-footer img { + max-width: 100%; + height: auto; +} diff --git a/libs/navigation/navigation.js b/libs/navigation/navigation.js new file mode 100644 index 0000000000..21992cbb33 --- /dev/null +++ b/libs/navigation/navigation.js @@ -0,0 +1,34 @@ +const blockConfig = { + footer: { + name: 'global-footer', + targetEl: 'footer', + appendType: 'appendChild', + }, +}; + +const envMap = { + prod: 'https://www.adobe.com', + stage: 'https://www.stage.adobe.com', + qa: 'https://feds--milo--adobecom.hlx.page', +}; + +export default async function loadBlock(configs = {}) { + const { footer, locale, env = 'prod' } = configs; + const branch = new URLSearchParams(window.location.search).get('navbranch'); + const miloLibs = branch ? `https://${branch}--milo--adobecom.hlx.page` : envMap[env]; + + // Relative path can't be used, as the script will run on consumer's app + const { default: bootstrapBlock } = await import(`${miloLibs}/libs/navigation/bootstrapper.js`); + const { default: locales } = await import(`${miloLibs}/libs/utils/locales.js`); + const clientConfig = { + origin: miloLibs, + miloLibs: `${miloLibs}/libs`, + pathname: `/${locale || ''}`, + locales: configs.locales || locales, + }; + if (footer) { + const { footer: { authoringPath, privacyId, privacyLoadDelay = 3000 } } = configs; + blockConfig.delay = privacyLoadDelay; + bootstrapBlock({ ...clientConfig, contentRoot: authoringPath, privacyId }, blockConfig.footer); + } +} diff --git a/libs/scripts/scripts.js b/libs/scripts/scripts.js index 3fce88cd92..4388eea325 100644 --- a/libs/scripts/scripts.js +++ b/libs/scripts/scripts.js @@ -16,6 +16,7 @@ import { setConfig, getMetadata, } from '../utils/utils.js'; +import locales from '../utils/locales.js'; // Production Domain const prodDomains = ['milo.adobe.com']; @@ -28,106 +29,6 @@ const stageDomainsMap = { 'news.adobe.com': 'news.stage.adobe.com', }; -const locales = { - '': { ietf: 'en-US', tk: 'hah7vzn.css' }, - ae_ar: { ietf: 'ar-AE', tk: 'lpk1hwn.css', dir: 'rtl' }, - ae_en: { ietf: 'en', tk: 'hah7vzn.css' }, - africa: { ietf: 'en', tk: 'hah7vzn.css' }, - ar: { ietf: 'ar', tk: 'lpk1hwn.css', dir: 'rtl' }, - ar_es: { ietf: 'es-AR', tk: 'hah7vzn.css' }, - at: { ietf: 'de-AT', tk: 'hah7vzn.css' }, - au: { ietf: 'en-AU', tk: 'hah7vzn.css' }, - be_en: { ietf: 'en-BE', tk: 'hah7vzn.css' }, - be_fr: { ietf: 'fr-BE', tk: 'hah7vzn.css' }, - be_nl: { ietf: 'nl-BE', tk: 'qxw8hzm.css' }, - bg: { ietf: 'bg-BG', tk: 'qxw8hzm.css' }, - br: { ietf: 'pt-BR', tk: 'hah7vzn.css' }, - ca_fr: { ietf: 'fr-CA', tk: 'hah7vzn.css' }, - ca: { ietf: 'en-CA', tk: 'hah7vzn.css' }, - ch_de: { ietf: 'de-CH', tk: 'hah7vzn.css' }, - ch_fr: { ietf: 'fr-CH', tk: 'hah7vzn.css' }, - ch_it: { ietf: 'it-CH', tk: 'hah7vzn.css' }, - cl: { ietf: 'es-CL', tk: 'hah7vzn.css' }, - cn: { ietf: 'zh-CN', tk: 'qxw8hzm' }, - co: { ietf: 'es-CO', tk: 'hah7vzn.css' }, - cr: { ietf: 'es-419', tk: 'hah7vzn.css' }, - cy_en: { ietf: 'en-CY', tk: 'hah7vzn.css' }, - cz: { ietf: 'cs-CZ', tk: 'qxw8hzm.css' }, - de: { ietf: 'de-DE', tk: 'hah7vzn.css' }, - dk: { ietf: 'da-DK', tk: 'qxw8hzm.css' }, - ec: { ietf: 'es-419', tk: 'hah7vzn.css' }, - ee: { ietf: 'et-EE', tk: 'qxw8hzm.css' }, - eg_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, - eg_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - el: { ietf: 'el', tk: 'qxw8hzm.css' }, - es: { ietf: 'es-ES', tk: 'hah7vzn.css' }, - fi: { ietf: 'fi-FI', tk: 'qxw8hzm.css' }, - fr: { ietf: 'fr-FR', tk: 'hah7vzn.css' }, - gr_el: { ietf: 'el', tk: 'qxw8hzm.css' }, - gr_en: { ietf: 'en-GR', tk: 'hah7vzn.css' }, - gt: { ietf: 'es-419', tk: 'hah7vzn.css' }, - hk_en: { ietf: 'en-HK', tk: 'hah7vzn.css' }, - hk_zh: { ietf: 'zh-HK', tk: 'jay0ecd' }, - hu: { ietf: 'hu-HU', tk: 'qxw8hzm.css' }, - id_en: { ietf: 'en', tk: 'hah7vzn.css' }, - id_id: { ietf: 'id', tk: 'qxw8hzm.css' }, - ie: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - il_en: { ietf: 'en-IL', tk: 'hah7vzn.css' }, - il_he: { ietf: 'he', tk: 'qxw8hzm.css', dir: 'rtl' }, - in_hi: { ietf: 'hi', tk: 'qxw8hzm.css' }, - in: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - it: { ietf: 'it-IT', tk: 'hah7vzn.css' }, - jp: { ietf: 'ja-JP', tk: 'dvg6awq' }, - kr: { ietf: 'ko-KR', tk: 'qjs5sfm' }, - kw_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, - kw_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - la: { ietf: 'es-LA', tk: 'hah7vzn.css' }, - langstore: { ietf: 'en-US', tk: 'hah7vzn.css' }, - lt: { ietf: 'lt-LT', tk: 'qxw8hzm.css' }, - lu_de: { ietf: 'de-LU', tk: 'hah7vzn.css' }, - lu_en: { ietf: 'en-LU', tk: 'hah7vzn.css' }, - lu_fr: { ietf: 'fr-LU', tk: 'hah7vzn.css' }, - lv: { ietf: 'lv-LV', tk: 'qxw8hzm.css' }, - mena_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, - mena_en: { ietf: 'en', tk: 'hah7vzn.css' }, - mt: { ietf: 'en-MT', tk: 'hah7vzn.css' }, - mx: { ietf: 'es-MX', tk: 'hah7vzn.css' }, - my_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - my_ms: { ietf: 'ms', tk: 'qxw8hzm.css' }, - ng: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - nl: { ietf: 'nl-NL', tk: 'qxw8hzm.css' }, - no: { ietf: 'no-NO', tk: 'qxw8hzm.css' }, - nz: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - pe: { ietf: 'es-PE', tk: 'hah7vzn.css' }, - ph_en: { ietf: 'en', tk: 'hah7vzn.css' }, - ph_fil: { ietf: 'fil-PH', tk: 'qxw8hzm.css' }, - pl: { ietf: 'pl-PL', tk: 'qxw8hzm.css' }, - pr: { ietf: 'es-419', tk: 'hah7vzn.css' }, - pt: { ietf: 'pt-PT', tk: 'hah7vzn.css' }, - qa_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, - qa_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - ro: { ietf: 'ro-RO', tk: 'qxw8hzm.css' }, - ru: { ietf: 'ru-RU', tk: 'qxw8hzm.css' }, - sa_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, - sa_en: { ietf: 'en', tk: 'hah7vzn.css' }, - se: { ietf: 'sv-SE', tk: 'qxw8hzm.css' }, - sg: { ietf: 'en-SG', tk: 'hah7vzn.css' }, - si: { ietf: 'sl-SI', tk: 'qxw8hzm.css' }, - sk: { ietf: 'sk-SK', tk: 'qxw8hzm.css' }, - th_en: { ietf: 'en', tk: 'hah7vzn.css' }, - th_th: { ietf: 'th', tk: 'lqo2bst.css' }, - tr: { ietf: 'tr-TR', tk: 'qxw8hzm.css' }, - tw: { ietf: 'zh-TW', tk: 'jay0ecd' }, - ua: { ietf: 'uk-UA', tk: 'qxw8hzm.css' }, - uk: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - vn_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - vn_vi: { ietf: 'vi', tk: 'qxw8hzm.css' }, - za: { ietf: 'en-GB', tk: 'hah7vzn.css' }, - cis_en: { ietf: 'en', tk: 'rks2kng.css' }, - cis_ru: { ietf: 'ru', tk: 'qxw8hzm.css' }, - sea: { ietf: 'en', tk: 'hah7vzn.css' }, -}; - const config = { geoRouting: 'on', fallbackRouting: 'on', diff --git a/libs/utils/federated.js b/libs/utils/federated.js index e4596c03a3..c32b4ac627 100644 --- a/libs/utils/federated.js +++ b/libs/utils/federated.js @@ -10,10 +10,11 @@ export const getFederatedContentRoot = () => { 'https://milo.adobe.com', 'https://news.adobe.com', ]; - const { allowedOrigins = [] } = getConfig(); + const { allowedOrigins = [], origin: configOrigin } = getConfig(); if (federatedContentRoot) return federatedContentRoot; + // Non milo consumers will have its origin from congig + const origin = configOrigin || window.location.origin; - const { origin } = window.location; federatedContentRoot = [...allowedOrigins, ...cdnWhitelistedOrigins].some((o) => origin.replace('.stage', '') === o) ? origin : 'https://www.adobe.com'; diff --git a/libs/utils/locales.js b/libs/utils/locales.js new file mode 100644 index 0000000000..7fd76e2bd7 --- /dev/null +++ b/libs/utils/locales.js @@ -0,0 +1,99 @@ +export default { + '': { ietf: 'en-US', tk: 'hah7vzn.css' }, + ae_ar: { ietf: 'ar-AE', tk: 'lpk1hwn.css', dir: 'rtl' }, + ae_en: { ietf: 'en', tk: 'hah7vzn.css' }, + africa: { ietf: 'en', tk: 'hah7vzn.css' }, + ar: { ietf: 'ar', tk: 'lpk1hwn.css', dir: 'rtl' }, + ar_es: { ietf: 'es-AR', tk: 'hah7vzn.css' }, + at: { ietf: 'de-AT', tk: 'hah7vzn.css' }, + au: { ietf: 'en-AU', tk: 'hah7vzn.css' }, + be_en: { ietf: 'en-BE', tk: 'hah7vzn.css' }, + be_fr: { ietf: 'fr-BE', tk: 'hah7vzn.css' }, + be_nl: { ietf: 'nl-BE', tk: 'qxw8hzm.css' }, + bg: { ietf: 'bg-BG', tk: 'qxw8hzm.css' }, + br: { ietf: 'pt-BR', tk: 'hah7vzn.css' }, + ca_fr: { ietf: 'fr-CA', tk: 'hah7vzn.css' }, + ca: { ietf: 'en-CA', tk: 'hah7vzn.css' }, + ch_de: { ietf: 'de-CH', tk: 'hah7vzn.css' }, + ch_fr: { ietf: 'fr-CH', tk: 'hah7vzn.css' }, + ch_it: { ietf: 'it-CH', tk: 'hah7vzn.css' }, + cl: { ietf: 'es-CL', tk: 'hah7vzn.css' }, + cn: { ietf: 'zh-CN', tk: 'qxw8hzm' }, + co: { ietf: 'es-CO', tk: 'hah7vzn.css' }, + cr: { ietf: 'es-419', tk: 'hah7vzn.css' }, + cy_en: { ietf: 'en-CY', tk: 'hah7vzn.css' }, + cz: { ietf: 'cs-CZ', tk: 'qxw8hzm.css' }, + de: { ietf: 'de-DE', tk: 'hah7vzn.css' }, + dk: { ietf: 'da-DK', tk: 'qxw8hzm.css' }, + ec: { ietf: 'es-419', tk: 'hah7vzn.css' }, + ee: { ietf: 'et-EE', tk: 'qxw8hzm.css' }, + eg_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, + eg_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + el: { ietf: 'el', tk: 'qxw8hzm.css' }, + es: { ietf: 'es-ES', tk: 'hah7vzn.css' }, + fi: { ietf: 'fi-FI', tk: 'qxw8hzm.css' }, + fr: { ietf: 'fr-FR', tk: 'hah7vzn.css' }, + gr_el: { ietf: 'el', tk: 'qxw8hzm.css' }, + gr_en: { ietf: 'en-GR', tk: 'hah7vzn.css' }, + gt: { ietf: 'es-419', tk: 'hah7vzn.css' }, + hk_en: { ietf: 'en-HK', tk: 'hah7vzn.css' }, + hk_zh: { ietf: 'zh-HK', tk: 'jay0ecd' }, + hu: { ietf: 'hu-HU', tk: 'qxw8hzm.css' }, + id_en: { ietf: 'en', tk: 'hah7vzn.css' }, + id_id: { ietf: 'id', tk: 'qxw8hzm.css' }, + ie: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + il_en: { ietf: 'en-IL', tk: 'hah7vzn.css' }, + il_he: { ietf: 'he', tk: 'qxw8hzm.css', dir: 'rtl' }, + in_hi: { ietf: 'hi', tk: 'qxw8hzm.css' }, + in: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + it: { ietf: 'it-IT', tk: 'hah7vzn.css' }, + jp: { ietf: 'ja-JP', tk: 'dvg6awq' }, + kr: { ietf: 'ko-KR', tk: 'qjs5sfm' }, + kw_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, + kw_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + la: { ietf: 'es-LA', tk: 'hah7vzn.css' }, + langstore: { ietf: 'en-US', tk: 'hah7vzn.css' }, + lt: { ietf: 'lt-LT', tk: 'qxw8hzm.css' }, + lu_de: { ietf: 'de-LU', tk: 'hah7vzn.css' }, + lu_en: { ietf: 'en-LU', tk: 'hah7vzn.css' }, + lu_fr: { ietf: 'fr-LU', tk: 'hah7vzn.css' }, + lv: { ietf: 'lv-LV', tk: 'qxw8hzm.css' }, + mena_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, + mena_en: { ietf: 'en', tk: 'hah7vzn.css' }, + mt: { ietf: 'en-MT', tk: 'hah7vzn.css' }, + mx: { ietf: 'es-MX', tk: 'hah7vzn.css' }, + my_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + my_ms: { ietf: 'ms', tk: 'qxw8hzm.css' }, + ng: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + nl: { ietf: 'nl-NL', tk: 'qxw8hzm.css' }, + no: { ietf: 'no-NO', tk: 'qxw8hzm.css' }, + nz: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + pe: { ietf: 'es-PE', tk: 'hah7vzn.css' }, + ph_en: { ietf: 'en', tk: 'hah7vzn.css' }, + ph_fil: { ietf: 'fil-PH', tk: 'qxw8hzm.css' }, + pl: { ietf: 'pl-PL', tk: 'qxw8hzm.css' }, + pr: { ietf: 'es-419', tk: 'hah7vzn.css' }, + pt: { ietf: 'pt-PT', tk: 'hah7vzn.css' }, + qa_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, + qa_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + ro: { ietf: 'ro-RO', tk: 'qxw8hzm.css' }, + ru: { ietf: 'ru-RU', tk: 'qxw8hzm.css' }, + sa_ar: { ietf: 'ar', tk: 'qxw8hzm.css', dir: 'rtl' }, + sa_en: { ietf: 'en', tk: 'hah7vzn.css' }, + se: { ietf: 'sv-SE', tk: 'qxw8hzm.css' }, + sg: { ietf: 'en-SG', tk: 'hah7vzn.css' }, + si: { ietf: 'sl-SI', tk: 'qxw8hzm.css' }, + sk: { ietf: 'sk-SK', tk: 'qxw8hzm.css' }, + th_en: { ietf: 'en', tk: 'hah7vzn.css' }, + th_th: { ietf: 'th', tk: 'lqo2bst.css' }, + tr: { ietf: 'tr-TR', tk: 'qxw8hzm.css' }, + tw: { ietf: 'zh-TW', tk: 'jay0ecd' }, + ua: { ietf: 'uk-UA', tk: 'qxw8hzm.css' }, + uk: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + vn_en: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + vn_vi: { ietf: 'vi', tk: 'qxw8hzm.css' }, + za: { ietf: 'en-GB', tk: 'hah7vzn.css' }, + cis_en: { ietf: 'en', tk: 'rks2kng.css' }, + cis_ru: { ietf: 'ru', tk: 'qxw8hzm.css' }, + sea: { ietf: 'en', tk: 'hah7vzn.css' }, +}; diff --git a/test/blocks/fragment/mocks/body.html b/test/blocks/fragment/mocks/body.html index df6bcce448..ecf4e2551e 100644 --- a/test/blocks/fragment/mocks/body.html +++ b/test/blocks/fragment/mocks/body.html @@ -6,6 +6,7 @@ Fragment Fragment Fragment +Fragment

Fragment

diff --git a/test/blocks/fragment/mocks/federal/fragments/frag-a.plain.html b/test/blocks/fragment/mocks/federal/fragments/frag-a.plain.html new file mode 100644 index 0000000000..f4afadc27b --- /dev/null +++ b/test/blocks/fragment/mocks/federal/fragments/frag-a.plain.html @@ -0,0 +1,4 @@ +
+

Frag A, Loads Frag B

+ Frag B link +
diff --git a/test/navigation/bootstrapper.test.js b/test/navigation/bootstrapper.test.js new file mode 100644 index 0000000000..8bfb286e82 --- /dev/null +++ b/test/navigation/bootstrapper.test.js @@ -0,0 +1,32 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import loadBlock from '../../libs/navigation/bootstrapper.js'; + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +const blockConfig = { + name: 'global-footer', + targetEl: 'footer', + appendType: 'appendChild', + footer: { authoringPath: '/federal/home', privacyLoadDelay: 0 }, +}; + +const miloConfigs = { + origin: 'https://feds--milo--adobecom.hlx.page', + miloLibs: 'https://feds--milo--adobecom.hlx.page/libs', + pathname: '/', +}; + +describe('Bootstrapper', async () => { + it('Renders the footer block', async () => { + await loadBlock(miloConfigs, blockConfig); + const clock = sinon.useFakeTimers({ + toFake: ['setTimeout'], + shouldAdvanceTime: true, + }); + clock.tick(3000); + const el = document.getElementsByTagName('footer'); + expect(el).to.exist; + }); +}); diff --git a/test/navigation/mocks/body.html b/test/navigation/mocks/body.html new file mode 100644 index 0000000000..99b63783d3 --- /dev/null +++ b/test/navigation/mocks/body.html @@ -0,0 +1,6 @@ + +
+
+
+
+ diff --git a/test/navigation/navigation.test.js b/test/navigation/navigation.test.js new file mode 100644 index 0000000000..a29a75ced1 --- /dev/null +++ b/test/navigation/navigation.test.js @@ -0,0 +1,13 @@ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import loadBlock from '../../libs/navigation/navigation.js'; + +document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + +describe('Bootstrapper', async () => { + it('Renders the footer block', async () => { + await loadBlock({ footer: { authoringPath: '/federal/home' }, env: 'qa' }); + const el = document.getElementsByTagName('footer'); + expect(el).to.exist; + }); +}); From c5fcc8e82016236eac7e1c81391bafa38a5cc0ab Mon Sep 17 00:00:00 2001 From: Mark Perry <124626043+markpadbe@users.noreply.github.com> Date: Wed, 24 Jul 2024 01:06:55 -0700 Subject: [PATCH 2/5] MWPW-146528[MILO][MEP][ANALYTICS] Add attribute to content changed by Target for analytics and MWPW-152274 (#2593) * replace overridePersonalizationVariant function * Fix chaining * Move parseMepParam * add additional optional chaining operator Co-authored-by: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> * Refactor personalization.js * Remove comment * Add TargetManifestId * Remove comment * Add targetManifestId test * Refactor addMepHighlightAndTargetManifestIdGnav * Remove console.log from test * Shorten function name * Limits targetManifestId to target exp and adds missing name to manifestId * Fix test * Handle default selectedVariant issue * Mostly done * Merging in prop display update * Unit tests fixed * Add targetmanifestid to useblockcode * refactor to remove unnecessary import * Remove commented code * Removing unused functions * Update tests, not including replacePage * Shorten Function Name * move insert inline back to fragment.js * additional cleanup and unit tests * Fix adobeTargetTestId issues * add optional chaining * Update condition for addAnalyticsForUseBlockCode * Refactor to handle analytics for updatemetadata * Fix useBlockCode test * Fix merged merged test * Remove applyPers from preload fragments test * Fix martech init post-merge * Fix tests and martech merge cleanup * Dynamically import personalization.js * add 2 ignore comments * add conditional chaining * Remove mwpw-146528 temp files * MWPW-152275 Move manifest.json request up while waiting on sstats / etc. (#2608) * add preload * shorten if and and conditional chaining * run loop even if Target is off * Add nullish coalescing assignment suggestion * MWPW-154998 [MEP][MILO] Manifests do not execute in the right order when there is a disabled manifest (#2616) * add default execution order * unit test --------- Co-authored-by: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Co-authored-by: vgoodric --- libs/blocks/fragment/fragment.js | 19 +- .../global-navigation/global-navigation.js | 12 +- .../global-navigation/utilities/menu/menu.js | 4 +- .../global-navigation/utilities/utilities.js | 12 +- .../personalization/personalization.js | 308 +++++++++++------- libs/martech/martech.js | 42 +-- libs/mep/mwpw-142224/marquee/marquee.js | 3 - .../marquee/marquee.css | 0 libs/mep/sample-block-code/marquee/marquee.js | 3 + libs/utils/utils.js | 92 ++---- test/features/personalization/actions.test.js | 55 +++- .../actionsTargetManifestId.test.js | 164 ++++++++++ .../personalization/deprecatedActions.test.js | 15 +- test/features/personalization/mepSettings.js | 10 + .../personalization/mepTargetSettings.js | 10 + .../actions/manifestAppendToSection.json | 1 + .../mocks/actions/manifestCustomAction.json | 4 + .../mocks/actions/manifestInsertAfter.json | 3 + .../mocks/actions/manifestInsertBefore.json | 2 + .../actions/manifestPrependToSection.json | 3 +- .../mocks/actions/manifestRemove.json | 2 + .../mocks/actions/manifestReplace.json | 4 + .../mocks/actions/manifestTargetReplace.json | 48 +++ .../mocks/actions/manifestUseBlockCode.json | 1 + .../mocks/actions/manifestUseBlockCode2.json | 1 + ...replace-content-target-howto-h2.plain.html | 1 + .../personalization/pageFilter.test.js | 7 +- .../personalization/personalization.test.js | 97 +++++- .../personalization/replacePage.test.js | 5 +- test/utils/utils-mep.test.js | 3 +- test/utils/utils.test.js | 4 +- 31 files changed, 637 insertions(+), 298 deletions(-) delete mode 100644 libs/mep/mwpw-142224/marquee/marquee.js rename libs/mep/{mwpw-142224 => sample-block-code}/marquee/marquee.css (100%) create mode 100644 libs/mep/sample-block-code/marquee/marquee.js create mode 100644 test/features/personalization/actionsTargetManifestId.test.js create mode 100644 test/features/personalization/mepSettings.js create mode 100644 test/features/personalization/mepTargetSettings.js create mode 100644 test/features/personalization/mocks/actions/manifestTargetReplace.json create mode 100644 test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html diff --git a/libs/blocks/fragment/fragment.js b/libs/blocks/fragment/fragment.js index 2bca02da83..84a922d6f5 100644 --- a/libs/blocks/fragment/fragment.js +++ b/libs/blocks/fragment/fragment.js @@ -32,12 +32,6 @@ const updateFragMap = (fragment, a, href) => { } }; -const setManifestIdOnChildren = (sections, manifestId) => { - [...sections[0].children].forEach( - (child) => (child.dataset.manifestId = manifestId), - ); -}; - const insertInlineFrag = (sections, a, relHref) => { // Inline fragments only support one section, other sections are ignored const fragChildren = [...sections[0].children]; @@ -79,7 +73,7 @@ export default async function init(a) { } const path = new URL(a.href).pathname; - if (mep?.fragments?.[path] && mep) { + if (mep?.fragments?.[path]) { relHref = mep.handleFragmentCommand(mep?.fragments[path], a); if (!relHref) return; } @@ -122,15 +116,10 @@ export default async function init(a) { } updateFragMap(fragment, a, relHref); - - if (a.dataset.manifestId) { - if (inline) { - setManifestIdOnChildren(sections, a.dataset.manifestId); - } else { - fragment.dataset.manifestId = a.dataset.manifestId; - } + if (a.dataset.manifestId || a.dataset.adobeTargetTestid) { + const { updateFragDataProps } = await import('../../features/personalization/personalization.js'); + updateFragDataProps(a, inline, sections, fragment); } - if (inline) { insertInlineFrag(sections, a, relHref); } else { diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index 4bdab1954a..8da869c76e 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -33,7 +33,7 @@ import { toFragment, trigger, yieldToMain, - addMepHighlight, + addMepHighlightAndTargetId, } from './utilities/utilities.js'; import { replaceKey, replaceKeyArray } from '../../features/placeholders.js'; @@ -919,14 +919,14 @@ class Gnav { observer.observe(dropdownTrigger, { attributeFilter: ['aria-expanded'] }); delayDropdownDecoration({ template: triggerTemplate }); - return addMepHighlight(triggerTemplate, item); + return addMepHighlightAndTargetId(triggerTemplate, item); } case 'primaryCta': case 'secondaryCta': // Remove its 'em' or 'strong' wrapper item.parentElement.replaceWith(item); - return addMepHighlight(toFragment`
+ return addMepHighlightAndTargetId(toFragment`
${decorateCta({ elem: item, type: itemType, index: index + 1 })}
`, item); case 'link': { @@ -945,15 +945,15 @@ class Gnav {
${linkElem}
`; - return addMepHighlight(linkTemplate, item); + return addMepHighlightAndTargetId(linkTemplate, item); } case 'text': - return addMepHighlight(toFragment`
+ return addMepHighlightAndTargetId(toFragment`
${item.textContent}
`, item); default: /* c8 ignore next 3 */ - return addMepHighlight(toFragment`
+ return addMepHighlightAndTargetId(toFragment`
${item}
`, item); } diff --git a/libs/blocks/global-navigation/utilities/menu/menu.js b/libs/blocks/global-navigation/utilities/menu/menu.js index d5f6d8fabb..aa2a21e704 100644 --- a/libs/blocks/global-navigation/utilities/menu/menu.js +++ b/libs/blocks/global-navigation/utilities/menu/menu.js @@ -11,7 +11,7 @@ import { toFragment, trigger, yieldToMain, - addMepHighlight, + addMepHighlightAndTargetId, } from '../utilities.js'; const decorateHeadline = (elem, index) => { @@ -317,7 +317,7 @@ const decorateMenu = (config) => logErrorFor(async () => { ${menuContent}
`; - addMepHighlight(menuTemplate, content); + addMepHighlightAndTargetId(menuTemplate, content); decorateCrossCloudMenu(menuTemplate); diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index fcc3c24f74..a4c73212f7 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -47,13 +47,12 @@ export const logErrorFor = async (fn, message, tags) => { } }; -export function addMepHighlight(el, source) { - let { manifestId } = source.dataset; - if (!manifestId) { - const closestManifestId = source?.closest('[data-manifest-id]'); - if (closestManifestId) manifestId = closestManifestId.dataset.manifestId; - } +export function addMepHighlightAndTargetId(el, source) { + let { manifestId, targetManifestId } = source.dataset; + manifestId ??= source?.closest('[data-manifest-id]')?.dataset?.manifestId; + targetManifestId ??= source?.closest('[data-adobe-target-testid]')?.dataset?.adobeTargetTestid; if (manifestId) el.dataset.manifestId = manifestId; + if (targetManifestId) el.dataset.adobeTargetTestid = targetManifestId; return el; } @@ -310,6 +309,7 @@ export async function fetchAndProcessPlainHtml({ url, shouldDecorateLinks = true const text = await res.text(); const { body } = new DOMParser().parseFromString(text, 'text/html'); if (mepFragment?.manifestId) body.dataset.manifestId = mepFragment.manifestId; + if (mepFragment?.targetManifestId) body.dataset.adobeTargetTestid = mepFragment.targetManifestId; const commands = mepGnav?.commands; if (commands?.length) { const { handleCommands, deleteMarkedEls } = await import('../../../features/personalization/personalization.js'); diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 30b993165b..c58b2c6953 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ /* eslint-disable no-console */ import { @@ -54,8 +55,6 @@ const DATA_TYPE = { const IN_BLOCK_SELECTOR_PREFIX = 'in-block:'; -export const appendJsonExt = (path) => (path.endsWith('.json') ? path : `${path}.json`); - export const normalizePath = (p, localize = true) => { let path = p; @@ -87,28 +86,6 @@ export const normalizePath = (p, localize = true) => { return path; }; -export const preloadManifests = ({ targetManifests = [], persManifests = [] }) => { - let manifests = targetManifests; - - manifests = manifests.concat( - persManifests.map((manifest) => ({ - ...manifest, - manifestPath: normalizePath(appendJsonExt(manifest.manifestPath)), - manifestUrl: manifest.manifestPath, - })), - ); - - for (const manifest of manifests) { - if (!manifest.manifestData && manifest.manifestPath && !manifest.disabled) { - loadLink( - manifest.manifestPath, - { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }, - ); - } - } - return manifests; -}; - export const getFileName = (path) => path?.split('/').pop(); const isInLcpSection = (el) => { @@ -116,7 +93,7 @@ const isInLcpSection = (el) => { return lcpSection === el || lcpSection?.contains(el); }; -export const createFrag = (el, url, manifestId) => { +export const createFrag = (el, url, manifestId, targetManifestId) => { let href = url; try { const { pathname, search, hash } = new URL(url); @@ -126,6 +103,7 @@ export const createFrag = (el, url, manifestId) => { } const a = createTag('a', { href }, url); if (manifestId) a.dataset.manifestId = manifestId; + if (targetManifestId) a.dataset.adobeTargetTestid = targetManifestId; let frag = createTag('p', undefined, a); const isDelayedModalAnchor = /#.*delay=/.test(href); if (isDelayedModalAnchor) frag.classList.add('hide-block'); @@ -162,9 +140,9 @@ const COMMANDS = { } el.classList.add(CLASS_EL_DELETE); }, - replace: (el, target, manifestId) => { + replace: (el, target, manifestId, targetManifestId) => { if (!el || el.classList.contains(CLASS_EL_REPLACE)) return; - el.insertAdjacentElement('beforebegin', createFrag(el, target, manifestId)); + el.insertAdjacentElement('beforebegin', createFrag(el, target, manifestId, targetManifestId)); el.classList.add(CLASS_EL_DELETE, CLASS_EL_REPLACE); }, }; @@ -227,6 +205,7 @@ const consolidateObjects = (arr, prop, existing = {}) => arr.reduce((propMap, it selector, manifestPath: item.manifestPath, manifestId: i.manifestId, + targetManifestId: i.targetManifestId, }; // eslint-disable-next-line no-restricted-syntax for (const key in propMap) { @@ -310,9 +289,9 @@ function getSection(rootEl, idx) { : rootEl.querySelector(`:scope > div:nth-child(${idx})`); } -function registerInBlockActions(cmd, manifestId) { +function registerInBlockActions(cmd, manifestId, targetManifestId) { const { action, target, selector } = cmd; - const command = { action, target, manifestId }; + const command = { action, target, manifestId, targetManifestId }; const blockAndSelector = selector.substring(IN_BLOCK_SELECTOR_PREFIX.length).trim().split(/\s+/); const [blockName] = blockAndSelector; @@ -424,24 +403,39 @@ const addHash = (url, newHash) => { } }; +const setDataIdOnChildren = (sections, id, value) => { + [...sections[0].children].forEach( + (child) => (child.dataset[id] = value), + ); +}; + +export const updateFragDataProps = (a, inline, sections, fragment) => { + const { manifestId, adobeTargetTestid } = a.dataset; + if (inline) { + if (manifestId) setDataIdOnChildren(sections, 'manifestId', manifestId); + if (adobeTargetTestid) setDataIdOnChildren(sections, 'adobeTargetTestid', adobeTargetTestid); + } else { + if (manifestId) fragment.dataset.manifestId = manifestId; + if (adobeTargetTestid) fragment.dataset.adobeTargetTestid = adobeTargetTestid; + } +}; export function handleCommands(commands, rootEl = document, forceInline = false) { commands.forEach((cmd) => { - const { manifestId } = cmd; - const { action, selector, target: trgt } = cmd; + const { manifestId, targetManifestId, action, selector, target: trgt } = cmd; const target = forceInline ? addHash(trgt, INLINE_HASH) : trgt; if (selector.startsWith(IN_BLOCK_SELECTOR_PREFIX)) { - registerInBlockActions(cmd, manifestId); + registerInBlockActions(cmd, manifestId, targetManifestId); return; } if (action in COMMANDS) { const el = getSelectedElement(selector, action, rootEl); - COMMANDS[action](el, target, manifestId); + COMMANDS[action](el, target, manifestId, targetManifestId); } else if (action in CREATE_CMDS) { const el = getSelectedElement(selector, action, rootEl); el?.insertAdjacentElement( CREATE_CMDS[action], - createFrag(el, target, manifestId), + createFrag(el, target, manifestId, targetManifestId), ); } else { /* c8 ignore next 2 */ @@ -450,14 +444,21 @@ export function handleCommands(commands, rootEl = document, forceInline = false) }); } -const getVariantInfo = (line, variantNames, variants, manifestId) => { +const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOverrideName) => { + const config = getConfig(); + let manifestId = getFileName(manifestPath); + let targetId = manifestId.replace('.json', ''); + if (manifestOverrideName) targetId = manifestOverrideName; + if (!config.mep?.preview) manifestId = false; const action = line.action?.toLowerCase().replace('content', '').replace('fragment', ''); const { selector } = line; const pageFilter = line['page filter'] || line['page filter optional']; if (pageFilter && !matchGlob(pageFilter, new URL(window.location).pathname)) return; + if (!config.mep?.preview) manifestId = false; variantNames.forEach((vn) => { + const targetManifestId = vn.startsWith(TARGET_EXP_PREFIX) ? targetId : false; if (!line[vn] || line[vn].toLowerCase() === 'false') return; const variantInfo = { @@ -467,6 +468,7 @@ const getVariantInfo = (line, variantNames, variants, manifestId) => { target: line[vn], selectorType: checkSelectorType(selector), manifestId, + targetManifestId, }; if (action in COMMANDS && variantInfo.selectorType === 'fragment') { @@ -475,6 +477,7 @@ const getVariantInfo = (line, variantNames, variants, manifestId) => { val: normalizePath(line[vn]), action, manifestId, + targetManifestId, }); } else if (GLOBAL_CMDS.includes(action)) { variants[vn][action] = variants[vn][action] || []; @@ -486,6 +489,7 @@ const getVariantInfo = (line, variantNames, variants, manifestId) => { val: blockTarget, pageFilter, manifestId, + targetManifestId, }); } else { variants[vn][action].push({ @@ -493,6 +497,7 @@ const getVariantInfo = (line, variantNames, variants, manifestId) => { val: normalizePath(line[vn]), pageFilter, manifestId, + targetManifestId, }); } } else if (action in COMMANDS || action in CREATE_CMDS) { @@ -504,7 +509,7 @@ const getVariantInfo = (line, variantNames, variants, manifestId) => { }); }; -export function parseManifestVariants(data, manifestId) { +export function parseManifestVariants(data, manifestPath, manifestOverrideName) { if (!data?.length) return null; const manifestConfig = {}; @@ -519,10 +524,15 @@ export function parseManifestVariants(data, manifestId) { variants[vn] = { commands: [], fragments: [] }; }); - experiences.forEach((line) => getVariantInfo(line, variantNames, variants, manifestId)); + experiences.forEach((line) => { + getVariantInfo(line, variantNames, variants, manifestPath, manifestOverrideName); + }); manifestConfig.variants = variants; manifestConfig.variantNames = variantNames; + const config = getConfig(); + if (!config.mep?.preview) manifestConfig.manifestId = false; + return manifestConfig; } catch (e) { /* c8 ignore next 3 */ @@ -615,12 +625,30 @@ const createDefaultExperiment = (manifest) => ({ disabled: manifest.disabled, event: manifest.event, manifest: manifest.manifestPath, + executionOrder: '1-1', selectedVariant: { commands: [], fragments: [] }, selectedVariantName: 'default', variantNames: ['all'], variants: {}, }); +export const addMepAnalytics = (config, header) => { + config.mep.experiments.forEach((experiment) => { + experiment?.selectedVariant?.useblockcode?.forEach(({ selector, targetManifestId }) => { + if (selector && targetManifestId) { + document.querySelectorAll(`.${selector}`) + .forEach((el) => (el.dataset.adobeTargetTestid = targetManifestId)); + } + }); + if (header) { + experiment?.selectedVariant?.updatemetadata?.forEach((updateMetaData) => { + if (updateMetaData?.selector === 'gnav-source' && updateMetaData.targetManifestId) { + header.dataset.adobeTargetTestid = updateMetaData.targetManifestId; + } + }); + } + }); +}; export async function getManifestConfig(info, variantOverride = false) { const { name, @@ -633,7 +661,7 @@ export async function getManifestConfig(info, variantOverride = false) { disabled, event, } = info; - if (disabled && !variantOverride) { + if (disabled && (!variantOverride || !Object.keys(variantOverride).length)) { return createDefaultExperiment(info); } let data = manifestData; @@ -644,33 +672,25 @@ export async function getManifestConfig(info, variantOverride = false) { const persData = data?.experiences?.data || data?.data || data; if (!persData) return null; - - let manifestId = getFileName(manifestPath); - const config = getConfig(); - if (!config.mep?.preview) { - manifestId = false; - } else if (name) { - manifestId = `${name}: ${manifestId}`; - } - const manifestConfig = parseManifestVariants(persData, manifestId); + const infoTab = manifestInfo || data?.info?.data; + const infoObj = infoTab?.reduce((acc, item) => { + acc[item.key] = item.value; + return acc; + }, {}); + const manifestOverrideName = name || infoObj?.['manifest-override-name']?.toLowerCase(); + const manifestConfig = parseManifestVariants(persData, manifestPath, manifestOverrideName); if (!manifestConfig) { /* c8 ignore next 3 */ console.log('Error loading personalization manifestConfig: ', name || manifestPath); return null; } - - const infoTab = manifestInfo || data?.info?.data; const infoKeyMap = { 'manifest-type': ['Personalization', 'Promo', 'Test'], 'manifest-execution-order': ['First', 'Normal', 'Last'], }; if (infoTab) { - const infoObj = infoTab?.reduce((acc, item) => { - acc[item.key] = item.value; - return acc; - }, {}); - manifestConfig.manifestOverrideName = infoObj?.['manifest-override-name']?.toLowerCase(); + manifestConfig.manifestOverrideName = manifestOverrideName; manifestConfig.manifestType = infoObj?.['manifest-type']?.toLowerCase(); const executionOrder = { 'manifest-type': 1, @@ -704,7 +724,6 @@ export async function getManifestConfig(info, variantOverride = false) { manifestConfig.selectedVariantName = 'default'; manifestConfig.selectedVariant = 'default'; } - const placeholders = manifestPlaceholders || data?.placeholders?.data; if (placeholders) { updateConfig( @@ -725,22 +744,22 @@ export const deleteMarkedEls = (rootEl = document) => { .forEach((el) => el.remove()); }; -const normalizeFragPaths = ({ selector, val, action }) => ({ +const normalizeFragPaths = ({ selector, val, action, manifestId, targetManifestId }) => ({ selector: normalizePath(selector), val: normalizePath(val), action, + manifestId, + targetManifestId, }); - -export async function categorizeActions(experiment) { +export async function categorizeActions(experiment, config) { if (!experiment) return null; const { manifestPath, selectedVariant } = experiment; if (!selectedVariant || selectedVariant === 'default') return { experiment }; - if (selectedVariant.replacepage) { - // only one replacepage can be defined - await replaceInner(selectedVariant.replacepage[0]?.val, document.querySelector('main')); - document.querySelector('main').dataset.manifestId = manifestPath; - } + // only one replacepage can be defined + const { replacepage } = selectedVariant; + // eslint-disable-next-line prefer-destructuring + if (selectedVariant.replacepage?.length) config.mep.replacepage = replacepage[0]; selectedVariant.insertscript?.map((script) => loadScript(script.val)); selectedVariant.updatemetadata?.map((metadata) => setMetadata(metadata)); @@ -804,14 +823,18 @@ export function cleanAndSortManifestList(manifests) { window.lana?.log(`MEP Error parsing manifests: ${e.toString()}`); } }); + Object.keys(manifestObj).forEach((key) => { + delete manifestObj[key].variants; + }); return Object.values(manifestObj).sort(compareExecutionOrder); } export function handleFragmentCommand(command, a) { - const { action, fragment, manifestId } = command; + const { action, fragment, manifestId, targetManifestId } = command; if (action === 'replace') { a.href = fragment; if (manifestId) a.dataset.manifestId = manifestId; + if (targetManifestId) a.dataset.adobeTargetTestid = targetManifestId; return fragment; } if (action === 'remove') { @@ -825,64 +848,125 @@ export function handleFragmentCommand(command, a) { } export async function applyPers(manifests, postLCP = false) { - try { - const config = getConfig(); - if (!postLCP) { - const { - mep: mepParam, - mepHighlight, - mepButton, - } = Object.fromEntries(PAGE_URL.searchParams); - config.mep = { - handleFragmentCommand, - preview: (mepButton !== 'off' - && (config.env?.name !== 'prod' || mepParam || mepParam === '' || mepButton)), - variantOverride: parseMepParam(mepParam), - highlight: (mepHighlight !== undefined && mepHighlight !== 'false'), - mepParam, - targetEnabled: config.mep?.targetEnabled, - }; - } + if (!manifests?.length) return; + let experiments = manifests; + const config = getConfig(); + for (let i = 0; i < experiments.length; i += 1) { + experiments[i] = await getManifestConfig(experiments[i], config.mep?.variantOverride); + } - if (!manifests?.length) return; - let experiments = manifests; - for (let i = 0; i < experiments.length; i += 1) { - experiments[i] = await getManifestConfig(experiments[i], config.mep?.variantOverride); - } + experiments = cleanAndSortManifestList(experiments); - experiments = cleanAndSortManifestList(experiments); + let results = []; - let results = []; + for (const experiment of experiments) { + const result = await categorizeActions(experiment, config); + if (result) results.push(result); + } + results = results.filter(Boolean); - for (const experiment of experiments) { - const result = await categorizeActions(experiment); - if (result) { - results.push(result); - } - } - results = results.filter(Boolean); + config.mep.experiments = [...config.mep.experiments, ...experiments]; + config.mep.blocks = consolidateObjects(results, 'blocks', config.mep.blocks); + config.mep.fragments = consolidateObjects(results, 'fragments', config.mep.fragments); + config.mep.commands = consolidateArray(results, 'commands', config.mep.commands); - config.mep.experiments ??= []; - config.mep.experiments = experiments; - config.mep.blocks = consolidateObjects(results, 'blocks', config.mep.blocks); - config.mep.fragments = consolidateObjects(results, 'fragments', config.mep.fragments); - config.mep.commands = consolidateArray(results, 'commands', config.mep.commands); + const main = document.querySelector('main'); + if (config.mep.replacepage && !postLCP && main) { + await replaceInner(config.mep.replacepage.val, main); + const { manifestId, targetManifestId } = config.mep.replacepage; + if (manifestId) main.dataset.manifestId = manifestId; + if (targetManifestId) main.dataset.adobeTargetTestid = targetManifestId; + } - if (!postLCP) handleCommands(config.mep.commands); - deleteMarkedEls(); + if (!postLCP) handleCommands(config.mep.commands); + deleteMarkedEls(); - const pznList = results.filter((r) => (r.experiment?.manifestType === TRACKED_MANIFEST_TYPE)); - if (!pznList.length) return; + const pznList = results.filter((r) => (r.experiment?.manifestType === TRACKED_MANIFEST_TYPE)); + if (!pznList.length) return; - const pznVariants = pznList.map((r) => { - const val = r.experiment.selectedVariantName.replace(TARGET_EXP_PREFIX, '').trim().slice(0, 15); - return val === 'default' ? 'nopzn' : val; + const pznVariants = pznList.map((r) => { + 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); + }); + config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; +} + +export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => { + let persManifests = []; + + if (persEnabled) { + persManifests = persEnabled.toLowerCase() + .split(/,|(\s+)|(\\n)/g) + .filter((path) => path?.trim()) + .map((manifestPath) => ({ manifestPath })); + } + + if (promoEnabled) { + const { default: getPromoManifests } = await import('./promo-utils.js'); + persManifests = persManifests.concat(getPromoManifests(promoEnabled, PAGE_URL.searchParams)); + } + + if (mepParam && mepParam !== 'off') { + const persManifestPaths = persManifests.map((manifest) => { + const { manifestPath } = manifest; + if (manifestPath.startsWith('/')) return manifestPath; + try { + const url = new URL(manifestPath); + return url.pathname; + } catch (e) { + return manifestPath; + } }); - const pznManifests = pznList.map((r) => { - const val = r.experiment?.manifestOverrideName || r.experiment?.manifest; - return getFileName(val).replace('.json', '').trim().slice(0, 15); + + mepParam.split('---').forEach((manifestPair) => { + const manifestPath = manifestPair.trim().toLowerCase().split('--')[0]; + if (!persManifestPaths.includes(manifestPath)) { + persManifests.push({ manifestPath }); + } + }); + } + return persManifests; +}; + +export async function init(enablements = {}) { + let manifests = []; + const { + mepParam, mepHighlight, mepButton, pzn, promo, target, postLCP, + } = enablements; + const config = getConfig(); + if (!postLCP) { + config.mep = { + handleFragmentCommand, + updateFragDataProps, + preview: (mepButton !== 'off' + && (config.env?.name !== 'prod' || mepParam || mepParam === '' || mepButton)), + variantOverride: parseMepParam(mepParam), + highlight: (mepHighlight !== undefined && mepHighlight !== 'false'), + targetEnabled: target, + experiments: [], + }; + manifests = manifests.concat(await combineMepSources(pzn, promo, mepParam)); + manifests?.forEach((manifest) => { + if (manifest.disabled) return; + const localizedURL = localizeLink(manifest.manifestPath); + loadLink(localizedURL, { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }); }); - config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; + } + + if (target === true || (target === 'gnav' && postLCP)) { + const { getTargetPersonalization } = await import('../../martech/martech.js'); + const { targetManifests, targetPropositions } = await getTargetPersonalization(); + manifests = manifests.concat(targetManifests); + if (targetPropositions?.length && window._satellite) { + window._satellite.track('propositionDisplay', targetPropositions); + } + } + try { + await applyPers(manifests, postLCP); } catch (e) { console.warn(e); window.lana?.log(`MEP Error: ${e.toString()}`); diff --git a/libs/martech/martech.js b/libs/martech/martech.js index bef35b6249..fac3ef32a8 100644 --- a/libs/martech/martech.js +++ b/libs/martech/martech.js @@ -1,4 +1,3 @@ -/* eslint-disable no-underscore-dangle */ import { getConfig, getMetadata, loadIms, loadLink, loadScript } from '../utils/utils.js'; const ALLOY_SEND_EVENT = 'alloy_sendEvent'; @@ -47,16 +46,6 @@ const waitForEventOrTimeout = (eventName, timeout, returnValIfTimeout) => new Pr window.addEventListener(ALLOY_SEND_EVENT_ERROR, errorListener, { once: true }); }); -const getExpFromParam = (expParam) => { - const lastSlash = expParam.lastIndexOf('/'); - return { - experiments: [{ - experimentPath: expParam.substring(0, lastSlash), - variantLabel: expParam.substring(lastSlash + 1), - }], - }; -}; - const handleAlloyResponse = (response) => { const items = ( (response.propositions?.length && response.propositions) @@ -116,12 +105,9 @@ function sendTargetResponseAnalytics(failure, responseStart, timeout, message) { }); } -const getTargetPersonalization = async () => { +export const getTargetPersonalization = async () => { const params = new URL(window.location.href).searchParams; - const experimentParam = params.get('experiment'); - if (experimentParam) return getExpFromParam(experimentParam); - const timeout = parseInt(params.get('target-timeout'), 10) || parseInt(getMetadata('target-timeout'), 10) || TARGET_TIMEOUT_MS; @@ -249,7 +235,7 @@ const loadMartechFiles = async (config) => { ? '/marketingtech/main.standard.min.js' : '/marketingtech/main.standard.qa.min.js' )); - + // eslint-disable-next-line no-underscore-dangle window._satellite.track('pageload'); }; @@ -257,30 +243,8 @@ const loadMartechFiles = async (config) => { return filesLoadedPromise; }; -export default async function init({ - persEnabled = false, - persManifests = [], - postLCP = false, -}) { +export default async function init() { const config = getConfig(); const martechPromise = loadMartechFiles(config); - - if (persEnabled) { - loadLink( - `${config.miloLibs || config.codeRoot}/features/personalization/personalization.js`, - { as: 'script', rel: 'modulepreload' }, - ); - - const { targetManifests, targetPropositions } = await getTargetPersonalization(); - if (targetManifests?.length || persManifests?.length) { - const { preloadManifests, applyPers } = await import('../features/personalization/personalization.js'); - const manifests = preloadManifests({ targetManifests, persManifests }); - await applyPers(manifests, postLCP); - if (targetPropositions?.length && window._satellite) { - window._satellite.track('propositionDisplay', targetPropositions); - } - } - } - return martechPromise; } diff --git a/libs/mep/mwpw-142224/marquee/marquee.js b/libs/mep/mwpw-142224/marquee/marquee.js deleted file mode 100644 index e7742cca13..0000000000 --- a/libs/mep/mwpw-142224/marquee/marquee.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function init(el) { - el.innerHTML = 'Marquee was replaced MEP and the content were overwritten.'; -} diff --git a/libs/mep/mwpw-142224/marquee/marquee.css b/libs/mep/sample-block-code/marquee/marquee.css similarity index 100% rename from libs/mep/mwpw-142224/marquee/marquee.css rename to libs/mep/sample-block-code/marquee/marquee.css diff --git a/libs/mep/sample-block-code/marquee/marquee.js b/libs/mep/sample-block-code/marquee/marquee.js new file mode 100644 index 0000000000..f322c6e100 --- /dev/null +++ b/libs/mep/sample-block-code/marquee/marquee.js @@ -0,0 +1,3 @@ +export default function init(el) { + el.innerHTML = 'Marquee code was replaced MEP and the content was overwritten.'; +} diff --git a/libs/utils/utils.js b/libs/utils/utils.js index c4f4a6f64b..28b5b4b8b5 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -922,81 +922,36 @@ export const getMepEnablement = (mdKey, paramKey = false) => { return getMdValue(mdKey); }; -export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => { - let persManifests = []; - - if (persEnabled) { - persManifests = persEnabled.toLowerCase() - .split(/,|(\s+)|(\\n)/g) - .filter((path) => path?.trim()) - .map((manifestPath) => ({ manifestPath })); - } - - if (promoEnabled) { - const { default: getPromoManifests } = await import('../features/personalization/promo-utils.js'); - persManifests = persManifests.concat(getPromoManifests(promoEnabled, PAGE_URL.searchParams)); - } - - if (mepParam && mepParam !== 'off') { - const persManifestPaths = persManifests.map((manifest) => { - const { manifestPath } = manifest; - if (manifestPath.startsWith('/')) return manifestPath; - try { - const url = new URL(manifestPath); - return url.pathname; - } catch (e) { - return manifestPath; - } - }); - - mepParam.split('---').forEach((manifestPair) => { - const manifestPath = manifestPair.trim().toLowerCase().split('--')[0]; - if (!persManifestPaths.includes(manifestPath)) { - persManifests.push({ manifestPath }); - } - }); - } - return persManifests; -}; - async function checkForPageMods() { - const { mep: mepParam } = Object.fromEntries(PAGE_URL.searchParams); + const { mep: mepParam, mepHighlight, mepButton } = Object.fromEntries(PAGE_URL.searchParams); if (mepParam === 'off') return; - const persEnabled = getMepEnablement('personalization'); - const promoEnabled = getMepEnablement('manifestnames', PROMO_PARAM); - const targetEnabled = getMepEnablement('target'); - const mepEnabled = persEnabled || targetEnabled || promoEnabled || mepParam; - if (!mepEnabled) return; + const pzn = getMepEnablement('personalization'); + const promo = getMepEnablement('manifestnames', PROMO_PARAM); + const target = getMepEnablement('target'); + if (!pzn && !target && !promo && !mepParam && !mepHighlight && !mepButton) return; - const config = getConfig(); - config.mep = { targetEnabled }; - loadLink( - `${config.base}/features/personalization/personalization.js`, - { as: 'script', rel: 'modulepreload' }, - ); - - const persManifests = await combineMepSources(persEnabled, promoEnabled, mepParam); - if (targetEnabled === true) { - await loadMartech({ persEnabled: true, persManifests, targetEnabled }); - return; + if (target) { + loadMartech(); + } else if (pzn) { + loadIms() + .then(() => { + /* c8 ignore next */ + if (window.adobeIMS?.isSignedInUser()) loadMartech(); + }) + .catch((e) => { console.log('Unable to load IMS:', e); }); } - if (!persManifests.length) return; - loadIms() - .then(() => { - if (window.adobeIMS.isSignedInUser()) loadMartech(); - }) - .catch((e) => { console.log('Unable to load IMS:', e); }); - - const { preloadManifests, applyPers } = await import('../features/personalization/personalization.js'); - const manifests = preloadManifests({ persManifests }, { getConfig, loadLink }); - - await applyPers(manifests); + const { init } = await import('../features/personalization/personalization.js'); + await init({ + mepParam, mepHighlight, mepButton, pzn, promo, target, + }); } async function loadPostLCP(config) { if (config.mep?.targetEnabled === 'gnav') { - await loadMartech({ persEnabled: true, postLCP: true }); + /* c8 ignore next 2 */ + const { init } = await import('../features/personalization/personalization.js'); + await init({ postLCP: true }); } else { loadMartech(); } @@ -1014,6 +969,11 @@ async function loadPostLCP(config) { loadTemplate(); const { default: loadFonts } = await import('./fonts.js'); loadFonts(config.locale, loadStyle); + + if (config?.mep) { + import('../features/personalization/personalization.js') + .then(({ addMepAnalytics }) => addMepAnalytics(config, header)); + } if (config.mep?.preview) { import('../features/personalization/preview.js') .then(({ default: decoratePreviewMode }) => decoratePreviewMode()); diff --git a/test/features/personalization/actions.test.js b/test/features/personalization/actions.test.js index e9bce4b9c0..fe9e935202 100644 --- a/test/features/personalization/actions.test.js +++ b/test/features/personalization/actions.test.js @@ -3,8 +3,8 @@ import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig, loadBlock } from '../../../libs/utils/utils.js'; import initFragments from '../../../libs/blocks/fragment/fragment.js'; -import { applyPers, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; -import spoofParams from './spoofParams.js'; +import { init, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; +import mepSettings from './mepSettings.js'; document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -35,10 +35,12 @@ describe('replace action', () => { expect(document.querySelector('.how-to')).to.not.be.null; const parentEl = document.querySelector('#features-of-milo-experimentation-platform')?.parentElement; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector('#features-of-milo-experimentation-platform')).to.be.null; - expect(parentEl.firstElementChild.firstElementChild.href) + const el = parentEl.firstElementChild.firstElementChild; + expect(el.href) .to.equal('http://localhost:2000/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2'); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); // .how-to should not be changed as it is targeted to firefox expect(document.querySelector('.how-to')).to.not.be.null; }); @@ -52,7 +54,8 @@ describe('replace action', () => { expect(document.querySelector('a[href="/fragments/replaceme"]')).to.exist; expect(document.querySelector('a[href="/fragments/inline-replaceme#_inline"]')).to.exist; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); const fragmentResp = await readFile({ path: './mocks/fragments/fragmentReplaced.plain.html' }); const inlineFragmentResp = await readFile({ path: './mocks/fragments/inlineFragReplaced.plain.html' }); @@ -83,7 +86,8 @@ describe('insertAfter action', async () => { expect(document.querySelector('a[href="/fragments/insertafter"]')).to.be.null; expect(document.querySelector('a[href="/fragments/insertafterfragment"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); let fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter"]'); expect(fragment).to.not.be.null; @@ -105,7 +109,8 @@ describe('insertBefore action', async () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/fragments/insertbefore"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); let fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertbefore"]'); expect(fragment).to.not.be.null; @@ -127,7 +132,8 @@ describe('prependToSection action', async () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/prependToSection"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); const fragment = document.querySelector('main > div:nth-child(2) > div:first-child a[href="/test/features/personalization/mocks/fragments/prependToSection"]'); expect(fragment).to.not.be.null; @@ -143,7 +149,8 @@ describe('appendToSection action', async () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/appendToSection"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); const fragment = document.querySelector('main > div:nth-child(2) > div:last-child a[href="/test/features/personalization/mocks/fragments/appendToSection"]'); expect(fragment).to.not.be.null; @@ -155,7 +162,9 @@ describe('remove action', () => { let manifestJson = await readFile({ path: './mocks/actions/manifestRemove.json' }); manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + mepSettings.mepButton = 'off'; + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); }); it('remove should remove content', async () => { expect(document.querySelector('.z-pattern')).to.be.null; @@ -168,7 +177,6 @@ describe('remove action', () => { }); it('removeContent should tag but not remove content in preview', async () => { - spoofParams({ mep: '' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); let manifestJson = await readFile({ path: './mocks/actions/manifestRemove.json' }); @@ -177,7 +185,10 @@ describe('remove action', () => { setTimeout(async () => { expect(document.querySelector('.z-pattern')).to.not.be.null; - await applyPers([{ manifestPath: '/mocks/manifestRemove.json' }]); + mepSettings.mepButton = false; + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + expect(document.querySelector('.z-pattern')).to.not.be.null; expect(document.querySelector('.z-pattern').dataset.removedManifestId).to.not.be.null; @@ -195,7 +206,9 @@ describe('useBlockCode action', async () => { manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0] + .targetManifestId).to.equal(false); expect(getConfig().mep.blocks).to.deep.equal({ promo: 'http://localhost:2000/test/features/personalization/mocks/promo' }); const promoBlock = document.querySelector('.promo'); @@ -209,7 +222,9 @@ describe('useBlockCode action', async () => { manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0] + .targetManifestId).to.equal(false); expect(getConfig().mep.blocks).to.deep.equal({ myblock: 'http://localhost:2000/test/features/personalization/mocks/myblock' }); const myBlock = document.querySelector('.myblock'); @@ -224,7 +239,8 @@ describe('custom actions', async () => { let manifestJson = await readFile({ path: './mocks/actions/manifestReplace.json' }); manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); expect(getConfig().mep.custom).to.be.undefined; }); @@ -233,31 +249,34 @@ describe('custom actions', async () => { manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); - console.log(getConfig().mep.inBlock); + await init(mepSettings); expect(getConfig().mep.inBlock).to.deep.equal({ 'my-block': { commands: [{ action: 'replace', target: '/fragments/fragmentreplaced', manifestId: false, + targetManifestId: false, }, { action: 'replace', target: '/fragments/new-large-menu', manifestId: false, selector: '.large-menu', + targetManifestId: false, }], fragments: { '/fragments/sub-menu': { action: 'replace', target: '/fragments/even-more-new-sub-menu', manifestId: false, + targetManifestId: false, }, '/fragments/new-sub-menu': { action: 'replace', target: '/fragments/even-more-new-sub-menu', manifestId: false, + targetManifestId: false, }, }, }, @@ -280,7 +299,7 @@ describe('custom actions', async () => { expect(document.querySelector(lcpLink)).not.to.exist; expect(document.querySelector(notLcpLink)).not.to.exist; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector(lcpLink)).to.exist; expect(document.querySelector(notLcpLink)).not.to.exist; diff --git a/test/features/personalization/actionsTargetManifestId.test.js b/test/features/personalization/actionsTargetManifestId.test.js new file mode 100644 index 0000000000..7345cabb8b --- /dev/null +++ b/test/features/personalization/actionsTargetManifestId.test.js @@ -0,0 +1,164 @@ +import { expect } from '@esm-bundle/chai'; +import { readFile } from '@web/test-runner-commands'; +import { stub } from 'sinon'; +import { getConfig } from '../../../libs/utils/utils.js'; +import initFragments from '../../../libs/blocks/fragment/fragment.js'; +import { init, handleFragmentCommand, addMepAnalytics } from '../../../libs/features/personalization/personalization.js'; +import mepSettings from './mepTargetSettings.js'; + +document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); +document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); + +// Add custom keys so tests doesn't rely on real data +const config = getConfig(); +config.env = { name: 'prod' }; + +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)); +}; + +// Note that the manifestPath doesn't matter as we stub the fetch +describe('replace action', () => { + it('with a CSS Selector, it should replace an element with a fragment', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestReplace.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); + const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2"]'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); + + it('with a fragment selector, it should replace a fragment in the document', async () => { + document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); + + let manifestJson = await readFile({ path: './mocks/actions/manifestReplace.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.fragments['/fragments/replaceme'].targetManifestId).to.equal('manifest'); + const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2"]'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + const fragmentResp = await readFile({ path: './mocks/fragments/fragmentReplaced.plain.html' }); + const inlineFragmentResp = await readFile({ path: './mocks/fragments/inlineFragReplaced.plain.html' }); + window.fetch = stub(); + window.fetch.withArgs('http://localhost:2000/test/features/personalization/mocks/fragments/fragmentReplaced.plain.html') + .returns(getFetchPromise(fragmentResp, 'text')); + window.fetch.withArgs('http://localhost:2000/test/features/personalization/mocks/fragments/inlineFragReplaced.plain.html') + .returns(getFetchPromise(inlineFragmentResp, 'text')); + const replacemeFrag = document.querySelector('a[href="/fragments/replaceme"]'); + await initFragments(replacemeFrag); + expect(document.querySelector('a[href="/fragments/replaceme"]')).to.be.null; + expect(document.querySelector('div[data-path="/test/features/personalization/mocks/fragments/fragmentReplaced"]')).to.exist; + + const inlineReplacemeFrag = document.querySelector('a[href="/fragments/inline-replaceme#_inline"]'); + await initFragments(inlineReplacemeFrag); + expect(document.querySelector('a[href="/fragments/inline-replaceme#_inline"]')).to.be.null; + expect(document.querySelector('.inlinefragmentreplaced')).to.exist; + }); +}); + +describe('insertAfter action', async () => { + it('insertContentAfter should add fragment after target content and fragment', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestInsertAfter.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); + const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter"]'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); +}); + +describe('insertBefore action', async () => { + it('insertContentBefore should add fragment before target content and fragment', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestInsertBefore.json' }); + + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); + const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertbefore"]'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); +}); + +describe('prependToSection action', async () => { + it('appendToSection should add fragment to beginning of section', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestPrependToSection.json' }); + + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/prependToSection"]')).to.be.null; + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); + const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/prependToSection"]'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); +}); + +describe('appendToSection action', async () => { + it('appendToSection should add fragment to end of section', async () => { + config.mep = { handleFragmentCommand }; + let manifestJson = await readFile({ path: './mocks/actions/manifestAppendToSection.json' }); + + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); + const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/appendToSection"]'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); +}); + +describe('useBlockCode action', async () => { + it('useBlockCode should override a current block with the custom block code provided', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestUseBlockCode.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0].targetManifestId).to.equal('manifest'); + await addMepAnalytics(config); + const el = document.querySelector('.promo'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); + + it('useBlockCode should be able to use a new type of block', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestUseBlockCode2.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0].targetManifestId).to.equal('manifest'); + await addMepAnalytics(config); + const el = document.querySelector('.myblock'); + expect(el.dataset.adobeTargetTestid).to.equal('manifest'); + }); +}); + +describe('custom actions', async () => { + it('should add a custom action configuration', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestCustomAction.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await init(mepSettings); + expect(getConfig().mep.inBlock['my-block'].commands[0].targetManifestId).to.equal('manifest'); + expect(getConfig().mep.inBlock['my-block'].commands[1].targetManifestId).to.equal('manifest'); + expect(getConfig().mep.inBlock['my-block'].fragments['/fragments/sub-menu'].targetManifestId).to.equal('manifest'); + expect(getConfig().mep.inBlock['my-block'].fragments['/fragments/new-sub-menu'].targetManifestId).to.equal('manifest'); + }); +}); diff --git a/test/features/personalization/deprecatedActions.test.js b/test/features/personalization/deprecatedActions.test.js index b6041034ad..30ac2cb931 100644 --- a/test/features/personalization/deprecatedActions.test.js +++ b/test/features/personalization/deprecatedActions.test.js @@ -3,8 +3,9 @@ import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig } from '../../../libs/utils/utils.js'; import initFragments from '../../../libs/blocks/fragment/fragment.js'; -import { applyPers, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; +import { init, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; import spoofParams from './spoofParams.js'; +import mepSettings from './mepSettings.js'; document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -38,7 +39,7 @@ describe('Functional Test', () => { expect(document.querySelector('.how-to')).to.not.be.null; const parentEl = document.querySelector('#features-of-milo-experimentation-platform')?.parentElement; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector('#features-of-milo-experimentation-platform')).to.be.null; expect(parentEl.firstElementChild.firstElementChild.href) .to.equal('http://localhost:2000/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2'); @@ -52,7 +53,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('.z-pattern')).to.not.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector('.z-pattern')).to.be.null; }); @@ -62,7 +63,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/fragments/insertafter"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter"]'); expect(fragment).to.not.be.null; @@ -77,7 +78,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/fragments/insertbefore"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertbefore"]'); expect(fragment).to.not.be.null; @@ -94,7 +95,7 @@ describe('Functional Test', () => { expect(document.querySelector('a[href="/fragments/replaceme"]')).to.exist; expect(document.querySelector('a[href="/fragments/inline-replaceme#_inline"]')).to.exist; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const fragmentResp = await readFile({ path: './mocks/fragments/fragmentReplaced.plain.html' }); const inlineFragmentResp = await readFile({ path: './mocks/fragments/inlineFragReplaced.plain.html' }); @@ -126,7 +127,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('.z-pattern')).to.not.be.null; - await applyPers([{ manifestPath: '/mocks/manifestRemove.json' }]); + await init(mepSettings); expect(document.querySelector('.z-pattern')).to.not.be.null; expect(document.querySelector('.z-pattern').dataset.removedManifestId).to.not.be.null; diff --git a/test/features/personalization/mepSettings.js b/test/features/personalization/mepSettings.js new file mode 100644 index 0000000000..d55161f0ae --- /dev/null +++ b/test/features/personalization/mepSettings.js @@ -0,0 +1,10 @@ +const mepSettings = { + mepParam: false, + mepHighlight: false, + mepButton: false, + pzn: '/path/to/manifest.json', + promo: false, + target: false, +}; + +export default mepSettings; diff --git a/test/features/personalization/mepTargetSettings.js b/test/features/personalization/mepTargetSettings.js new file mode 100644 index 0000000000..a5313f2cde --- /dev/null +++ b/test/features/personalization/mepTargetSettings.js @@ -0,0 +1,10 @@ +const mepSettings = { + mepParam: '/path/to/manifest.json--target-var1', + mepHighlight: false, + mepButton: false, + pzn: '/path/to/manifest.json', + promo: false, + target: false, +}; + +export default mepSettings; diff --git a/test/features/personalization/mocks/actions/manifestAppendToSection.json b/test/features/personalization/mocks/actions/manifestAppendToSection.json index 0269996cd6..9bd9bfdf47 100644 --- a/test/features/personalization/mocks/actions/manifestAppendToSection.json +++ b/test/features/personalization/mocks/actions/manifestAppendToSection.json @@ -8,6 +8,7 @@ "selector": "section2", "page filter (optional)": "", "param-newoffer=123": "", + "target-var1": "/test/features/personalization/mocks/fragments/appendToSection", "chrome": "/test/features/personalization/mocks/fragments/appendToSection" } ], diff --git a/test/features/personalization/mocks/actions/manifestCustomAction.json b/test/features/personalization/mocks/actions/manifestCustomAction.json index fafaccbe56..1de7092bbb 100644 --- a/test/features/personalization/mocks/actions/manifestCustomAction.json +++ b/test/features/personalization/mocks/actions/manifestCustomAction.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/fragmentreplaced", + "target-var1": "/fragments/fragmentreplaced", "firefox": "", "android": "", "ios": "" @@ -19,6 +20,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/new-large-menu", + "target-var1": "/fragments/new-large-menu", "firefox": "", "android": "", "ios": "" @@ -29,6 +31,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/new-sub-menu", + "target-var1": "/fragments/new-sub-menu", "firefox": "", "android": "", "ios": "" @@ -38,6 +41,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/even-more-new-sub-menu", + "target-var1": "/fragments/even-more-new-sub-menu", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestInsertAfter.json b/test/features/personalization/mocks/actions/manifestInsertAfter.json index d694ec80cc..0e40395cf9 100644 --- a/test/features/personalization/mocks/actions/manifestInsertAfter.json +++ b/test/features/personalization/mocks/actions/manifestInsertAfter.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertafter", + "target-var1": "/test/features/personalization/mocks/fragments/insertafter", "firefox": "", "android": "", "ios": "" @@ -19,6 +20,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertafterfragment", + "target-var1": "/test/features/personalization/mocks/fragments/insertafterfragment", "firefox": "", "android": "", "ios": "" @@ -29,6 +31,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertafterfragment", + "target-var1": "/test/features/personalization/mocks/fragments/insertafterfragment", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestInsertBefore.json b/test/features/personalization/mocks/actions/manifestInsertBefore.json index e58e39e05c..057cfaacf4 100644 --- a/test/features/personalization/mocks/actions/manifestInsertBefore.json +++ b/test/features/personalization/mocks/actions/manifestInsertBefore.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertbefore", + "target-var1": "/test/features/personalization/mocks/fragments/insertbefore", "firefox": "", "android": "", "ios": "" @@ -19,6 +20,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertbeforefragment", + "target-var1": "/test/features/personalization/mocks/fragments/insertbeforefragment", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestPrependToSection.json b/test/features/personalization/mocks/actions/manifestPrependToSection.json index cd5f43432b..c2ccb7af8b 100644 --- a/test/features/personalization/mocks/actions/manifestPrependToSection.json +++ b/test/features/personalization/mocks/actions/manifestPrependToSection.json @@ -8,7 +8,8 @@ "selector": "section2", "page filter (optional)": "", "param-newoffer=123": "", - "chrome": "/test/features/personalization/mocks/fragments/prependToSection" + "chrome": "/test/features/personalization/mocks/fragments/prependToSection", + "target-var1": "/test/features/personalization/mocks/fragments/prependToSection" } ], ":type": "sheet" diff --git a/test/features/personalization/mocks/actions/manifestRemove.json b/test/features/personalization/mocks/actions/manifestRemove.json index 8ff94af803..030c9efb0a 100644 --- a/test/features/personalization/mocks/actions/manifestRemove.json +++ b/test/features/personalization/mocks/actions/manifestRemove.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "yes", + "target-var1": "yes", "firefox": "", "android": "", "ios": "" @@ -19,6 +20,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "yes", + "target-var1": "yes", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestReplace.json b/test/features/personalization/mocks/actions/manifestReplace.json index f1feae60ca..c1d0d78d78 100644 --- a/test/features/personalization/mocks/actions/manifestReplace.json +++ b/test/features/personalization/mocks/actions/manifestReplace.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "", + "target-var1": "", "firefox": "/test/features/personalization/mocks/fragments/milo-replace-content-firefox-accordion", "android": "", "ios": "" @@ -19,6 +20,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2", + "target-var1": "/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2", "firefox": "", "android": "", "ios": "" @@ -29,6 +31,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/fragmentReplaced", + "target-var1": "/test/features/personalization/mocks/fragments/fragmentReplaced", "firefox": "", "android": "", "ios": "" @@ -39,6 +42,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/inlineFragReplaced", + "target-var1": "/test/features/personalization/mocks/fragments/inlineFragReplaced", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestTargetReplace.json b/test/features/personalization/mocks/actions/manifestTargetReplace.json new file mode 100644 index 0000000000..f1feae60ca --- /dev/null +++ b/test/features/personalization/mocks/actions/manifestTargetReplace.json @@ -0,0 +1,48 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "replace", + "selector": ".how-to", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "firefox": "/test/features/personalization/mocks/fragments/milo-replace-content-firefox-accordion", + "android": "", + "ios": "" + }, + { + "action": "replace", + "selector": "#features-of-milo-experimentation-platform", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2", + "firefox": "", + "android": "", + "ios": "" + }, + { + "action": "replace", + "selector": "/fragments/replaceme", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/fragments/fragmentReplaced", + "firefox": "", + "android": "", + "ios": "" + }, + { + "action": "replace", + "selector": "/fragments/inline-replaceme", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/fragments/inlineFragReplaced", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/actions/manifestUseBlockCode.json b/test/features/personalization/mocks/actions/manifestUseBlockCode.json index 1d1d47764c..e8837cb45f 100644 --- a/test/features/personalization/mocks/actions/manifestUseBlockCode.json +++ b/test/features/personalization/mocks/actions/manifestUseBlockCode.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "test\\features\\personalization\\mocks\\promo", + "target-var1": "test\\features\\personalization\\mocks\\promo", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestUseBlockCode2.json b/test/features/personalization/mocks/actions/manifestUseBlockCode2.json index f377c358d9..a2a48fdd0a 100644 --- a/test/features/personalization/mocks/actions/manifestUseBlockCode2.json +++ b/test/features/personalization/mocks/actions/manifestUseBlockCode2.json @@ -9,6 +9,7 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/myblock", + "target-var1": "/test/features/personalization/mocks/myblock", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html b/test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html new file mode 100644 index 0000000000..a9a5c882f2 --- /dev/null +++ b/test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html @@ -0,0 +1 @@ +mock fragment diff --git a/test/features/personalization/pageFilter.test.js b/test/features/personalization/pageFilter.test.js index 263e11cbba..0a08e45481 100644 --- a/test/features/personalization/pageFilter.test.js +++ b/test/features/personalization/pageFilter.test.js @@ -2,7 +2,8 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig } from '../../../libs/utils/utils.js'; -import { applyPers } from '../../../libs/features/personalization/personalization.js'; +import { init } from '../../../libs/features/personalization/personalization.js'; +import mepSettings from './mepSettings.js'; document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -35,7 +36,7 @@ it('pageFilter should exclude page if it is not a match', async () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('.newpage')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); // Nothing should be changed since the pageFilter excludes this page expect(document.querySelector('.marquee')).to.not.be.null; @@ -71,7 +72,7 @@ it('pageFilter should include page if it is a match', async () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('.newpage')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector('.marquee')).to.be.null; expect(document.querySelector('.newpage')).to.not.be.null; diff --git a/test/features/personalization/personalization.test.js b/test/features/personalization/personalization.test.js index 38211b358c..d2554b7734 100644 --- a/test/features/personalization/personalization.test.js +++ b/test/features/personalization/personalization.test.js @@ -2,8 +2,12 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import { assert, stub } from 'sinon'; import { getConfig, setConfig } from '../../../libs/utils/utils.js'; -import { applyPers, matchGlob, createFrag } from '../../../libs/features/personalization/personalization.js'; +import { + handleFragmentCommand, applyPers, + init, matchGlob, createFrag, combineMepSources, +} from '../../../libs/features/personalization/personalization.js'; import spoofParams from './spoofParams.js'; +import mepSettings from './mepSettings.js'; document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -42,7 +46,7 @@ describe('Functional Test', () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter2"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter2"]'); expect(fragment).to.not.be.null; expect(fragment.parentElement.previousElementSibling.className).to.equal('marquee'); @@ -58,7 +62,7 @@ describe('Functional Test', () => { const secondMarquee = document.getElementsByClassName('marquee')[1]; expect(secondMarquee).to.not.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const fragment = document.querySelector('a[href="/fragments/replace/marquee/r2c1"]'); expect(fragment).to.not.be.null; @@ -76,7 +80,7 @@ describe('Functional Test', () => { expect(document.querySelector('.custom-block-2')).to.not.be.null; expect(document.querySelector('.custom-block-3')).to.not.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector('.special-block')).to.be.null; expect(document.querySelector('.custom-block-2')).to.be.null; @@ -84,12 +88,27 @@ describe('Functional Test', () => { }); it('scheduled manifest should apply changes if active (bts)', async () => { + const config = getConfig(); + config.mep = { + handleFragmentCommand, + preview: false, + variantOverride: {}, + highlight: false, + targetEnabled: false, + experiments: [], + }; + const promoMepSettings = [ + { + manifestPath: '/promos/bts/manifest.json', + disabled: false, + event: { name: 'bts', start: new Date('2023-11-24T13:00:00+00:00'), end: new Date('2222-11-24T13:00:00+00:00') }, + }, + ]; let manifestJson = await readFile({ path: './mocks/manifestScheduledActive.json' }); manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter3"]')).to.be.null; - const event = { name: 'bts', start: new Date('2023-11-24T13:00:00+00:00'), end: new Date('2222-11-24T13:00:00+00:00') }; - await applyPers([{ manifestPath: '/promos/bts/manifest.json', disabled: false, event }]); + await applyPers(promoMepSettings); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter3"]'); expect(fragment).to.not.be.null; @@ -98,10 +117,25 @@ describe('Functional Test', () => { }); it('scheduled manifest should not apply changes if not active (blackfriday)', async () => { + const config = getConfig(); + config.mep = { + handleFragmentCommand, + preview: false, + variantOverride: {}, + highlight: false, + targetEnabled: false, + experiments: [], + }; + const promoMepSettings = [ + { + manifestPath: '/promos/blackfriday/manifest.json', + disabled: true, + event: { name: 'blackfriday', start: new Date('2022-11-24T13:00:00+00:00'), end: new Date('2022-11-24T13:00:00+00:00') }, + }, + ]; await loadManifestAndSetResponse('./mocks/manifestScheduledInactive.json'); expect(document.querySelector('a[href="/fragments/insertafter4"]')).to.be.null; - const event = { name: 'blackfriday', start: new Date('2022-11-24T13:00:00+00:00'), end: new Date('2022-11-24T13:00:00+00:00') }; - await applyPers([{ manifestPath: '/promos/blackfriday/manifest.json', disabled: true, event }]); + await applyPers(promoMepSettings); const fragment = document.querySelector('a[href="/fragments/insertafter4"]'); expect(fragment).to.be.null; @@ -112,20 +146,20 @@ describe('Functional Test', () => { config.mep = {}; await loadManifestAndSetResponse('./mocks/manifestTestOrPromo.json'); config = getConfig(); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(config.mep?.martech).to.be.undefined; }); it('should choose chrome & logged out', async () => { await loadManifestAndSetResponse('./mocks/manifestWithAmpersand.json'); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const config = getConfig(); expect(config.mep?.martech).to.equal('|chrome & logged|ampersand'); }); it('should choose not firefox', async () => { await loadManifestAndSetResponse('./mocks/manifestWithNot.json'); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); const config = getConfig(); expect(config.mep?.martech).to.equal('|not firefox|not'); }); @@ -137,7 +171,7 @@ describe('Functional Test', () => { config.entitlements = () => Promise.resolve(['indesign-any', 'fireflies', 'after-effects-any']); await loadManifestAndSetResponse('./mocks/manifestUseEntitlements.json'); - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(getConfig().mep?.martech).to.equal('|fireflies|manifest'); }); @@ -146,7 +180,7 @@ describe('Functional Test', () => { await loadManifestAndSetResponse('./mocks/manifestInvalidSelector.json'); - await applyPers([{ manifestPath: '/mocks/manifestRemove.json' }]); + await init(mepSettings); assert.calledWith(window.console.log, 'Invalid selector: ', '.bad...selector'); window.console.log.reset(); @@ -164,7 +198,7 @@ describe('Functional Test', () => { expect(document.querySelector('meta[property="og:title"]').content).to.equal('milo'); expect(document.querySelector('meta[property="og:image"]')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(geoMetadata.content).to.equal('on'); expect(document.querySelector('meta[name="mynewmetadata"]').content).to.equal('woot'); @@ -177,7 +211,7 @@ describe('Functional Test', () => { const config = getConfig(); await loadManifestAndSetResponse('./mocks/actions/manifestAppendToSection.json'); setTimeout(async () => { - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(config.mep.experiments[0].selectedVariantName).to.equal('param-newoffer=123'); }, 100); }); @@ -235,3 +269,36 @@ describe('matchGlob function', () => { expect(wrapper.classList.contains('hide-block')).to.be.true; }); }); + +describe('MEP Utils', () => { + describe('combineMepSources', async () => { + it('yields an empty list when everything is undefined', async () => { + const manifests = await combineMepSources(undefined, undefined, undefined); + expect(manifests.length).to.equal(0); + }); + it('combines promos and personalization', async () => { + document.head.innerHTML = await readFile({ path: '../../utils/mocks/mep/head-promo.html' }); + const promos = { manifestnames: 'pre-black-friday-global,black-friday-global' }; + const manifests = await combineMepSources('/pers/manifest.json', promos, undefined); + expect(manifests.length).to.equal(3); + expect(manifests[0].manifestPath).to.equal('/pers/manifest.json'); + expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json'); + expect(manifests[2].manifestPath).to.equal('/black-friday.json'); + }); + it('combines promos and personalization and mep param', async () => { + document.head.innerHTML = await readFile({ path: '../../utils/mocks/mep/head-promo.html' }); + const promos = { manifestnames: 'pre-black-friday-global,black-friday-global' }; + const manifests = await combineMepSources( + '/pers/manifest.json', + promos, + '/pers/manifest.json--var1---/mep-param/manifest1.json--all---/mep-param/manifest2.json--all', + ); + expect(manifests.length).to.equal(5); + expect(manifests[0].manifestPath).to.equal('/pers/manifest.json'); + expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json'); + expect(manifests[2].manifestPath).to.equal('/black-friday.json'); + expect(manifests[3].manifestPath).to.equal('/mep-param/manifest1.json'); + expect(manifests[4].manifestPath).to.equal('/mep-param/manifest2.json'); + }); + }); +}); diff --git a/test/features/personalization/replacePage.test.js b/test/features/personalization/replacePage.test.js index 3b3fed3038..3ea70387f5 100644 --- a/test/features/personalization/replacePage.test.js +++ b/test/features/personalization/replacePage.test.js @@ -2,7 +2,8 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig } from '../../../libs/utils/utils.js'; -import { applyPers } from '../../../libs/features/personalization/personalization.js'; +import { init } from '../../../libs/features/personalization/personalization.js'; +import mepSettings from './mepSettings.js'; document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -35,7 +36,7 @@ it('replacePage should replace all of the main block', async () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('.newpage')).to.be.null; - await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + await init(mepSettings); expect(document.querySelector('.marquee')).to.be.null; expect(document.querySelector('.newpage')).to.not.be.null; diff --git a/test/utils/utils-mep.test.js b/test/utils/utils-mep.test.js index 53af9d1d12..fdbdd7a2e7 100644 --- a/test/utils/utils-mep.test.js +++ b/test/utils/utils-mep.test.js @@ -1,6 +1,7 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -import { combineMepSources, getMepEnablement } from '../../libs/utils/utils.js'; +import { getMepEnablement } from '../../libs/utils/utils.js'; +import { combineMepSources } from '../../libs/features/personalization/personalization.js'; describe('MEP Utils', () => { describe('combineMepSources', async () => { diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js index e75e67afd4..fbdfd5e184 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -631,10 +631,10 @@ describe('Utils', () => { document.head.innerHTML = await readFile({ path: './mocks/head-personalization.html' }); await utils.loadArea(); const resultConfig = utils.getConfig(); - const resultExperiment = resultConfig.mep.experiments[2]; + const resultExperiment = resultConfig.mep.experiments[0]; expect(resultConfig.mep.preview).to.be.true; expect(resultConfig.mep.experiments.length).to.equal(3); - expect(resultExperiment.manifest).to.equal('/products/special-offers-manifest.json'); + expect(resultExperiment.manifest).to.equal('https://main--milo--adobecom.hlx.page/products/special-offers-manifest.json'); }); }); From 0569bfd3e33d639cf00f740031c133f1401d2cad Mon Sep 17 00:00:00 2001 From: Bandana Laishram Date: Wed, 24 Jul 2024 17:59:54 +0530 Subject: [PATCH 3/5] Fix for errors in dynamically loaded scripts in test cases (#2619) * Fix for dynamic script loading in test cases * Lint fix --- libs/navigation/navigation.js | 8 +++--- test/navigation/bootstrapper.test.js | 38 +++++++++++++++++++++++++--- test/navigation/navigation.test.js | 4 +-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/libs/navigation/navigation.js b/libs/navigation/navigation.js index 21992cbb33..ab721d2a47 100644 --- a/libs/navigation/navigation.js +++ b/libs/navigation/navigation.js @@ -12,10 +12,10 @@ const envMap = { qa: 'https://feds--milo--adobecom.hlx.page', }; -export default async function loadBlock(configs = {}) { - const { footer, locale, env = 'prod' } = configs; +export default async function loadBlock(configs, customLib) { + const { footer, locale, env = 'prod' } = configs || {}; const branch = new URLSearchParams(window.location.search).get('navbranch'); - const miloLibs = branch ? `https://${branch}--milo--adobecom.hlx.page` : envMap[env]; + const miloLibs = branch ? `https://${branch}--milo--adobecom.hlx.page` : customLib || envMap[env]; // Relative path can't be used, as the script will run on consumer's app const { default: bootstrapBlock } = await import(`${miloLibs}/libs/navigation/bootstrapper.js`); @@ -27,7 +27,7 @@ export default async function loadBlock(configs = {}) { locales: configs.locales || locales, }; if (footer) { - const { footer: { authoringPath, privacyId, privacyLoadDelay = 3000 } } = configs; + const { authoringPath, privacyId, privacyLoadDelay = 3000 } = footer; blockConfig.delay = privacyLoadDelay; bootstrapBlock({ ...clientConfig, contentRoot: authoringPath, privacyId }, blockConfig.footer); } diff --git a/test/navigation/bootstrapper.test.js b/test/navigation/bootstrapper.test.js index 8bfb286e82..4415788452 100644 --- a/test/navigation/bootstrapper.test.js +++ b/test/navigation/bootstrapper.test.js @@ -1,27 +1,57 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -import sinon from 'sinon'; +import { stub, useFakeTimers, restore } from 'sinon'; import loadBlock from '../../libs/navigation/bootstrapper.js'; +import fetchedFooter from '../blocks/global-footer/mocks/fetched-footer.js'; +import placeholders from '../blocks/global-navigation/mocks/placeholders.js'; document.body.innerHTML = await readFile({ path: './mocks/body.html' }); const blockConfig = { name: 'global-footer', targetEl: 'footer', - appendType: 'appendChild', + appendType: 'append', footer: { authoringPath: '/federal/home', privacyLoadDelay: 0 }, }; const miloConfigs = { origin: 'https://feds--milo--adobecom.hlx.page', - miloLibs: 'https://feds--milo--adobecom.hlx.page/libs', + miloLibs: 'http://localhost:2000/libs', pathname: '/', }; +const mockRes = ({ payload, status = 200, ok = true } = {}) => new Promise((resolve) => { + resolve({ + status, + ok, + json: () => payload, + text: () => payload, + }); +}); + describe('Bootstrapper', async () => { + beforeEach(async () => { + stub(window, 'fetch').callsFake(async (url) => { + if (url.includes('/footer')) { + return mockRes({ + payload: fetchedFooter( + { regionPickerHash: '/fragments/regions#langnav' }, + ), + }); + } + if (url.includes('/placeholders')) return mockRes({ payload: placeholders }); + if (url.includes('/footer.plain.html')) return mockRes({ payload: await readFile({ path: '../blocks/region-nav/mocks/regions.html' }) }); + return null; + }); + }); + + afterEach(() => { + restore(); + }); + it('Renders the footer block', async () => { await loadBlock(miloConfigs, blockConfig); - const clock = sinon.useFakeTimers({ + const clock = useFakeTimers({ toFake: ['setTimeout'], shouldAdvanceTime: true, }); diff --git a/test/navigation/navigation.test.js b/test/navigation/navigation.test.js index a29a75ced1..28b3bacc13 100644 --- a/test/navigation/navigation.test.js +++ b/test/navigation/navigation.test.js @@ -4,9 +4,9 @@ import loadBlock from '../../libs/navigation/navigation.js'; document.body.innerHTML = await readFile({ path: './mocks/body.html' }); -describe('Bootstrapper', async () => { +describe('Navigation component', async () => { it('Renders the footer block', async () => { - await loadBlock({ footer: { authoringPath: '/federal/home' }, env: 'qa' }); + await loadBlock({ footer: { authoringPath: '/federal/home' }, env: 'qa' }, 'http://localhost:2000'); const el = document.getElementsByTagName('footer'); expect(el).to.exist; }); From 543d9a8ddd3ffe7f2aa817ecf7d9f69783c78150 Mon Sep 17 00:00:00 2001 From: Chris Peyer Date: Wed, 24 Jul 2024 10:25:57 -0400 Subject: [PATCH 4/5] MWPW-128600 Locale Tool: Langstore points to langstore/en (#2615) * MWPW-128600 Langstore points to langstore/en * Update libs/blocks/locale-nav/locale-nav.js Co-authored-by: Rares Munteanu --------- Co-authored-by: Rares Munteanu --- libs/blocks/locale-nav/locale-nav.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/blocks/locale-nav/locale-nav.js b/libs/blocks/locale-nav/locale-nav.js index 4563d9b1df..45f6a8a305 100644 --- a/libs/blocks/locale-nav/locale-nav.js +++ b/libs/blocks/locale-nav/locale-nav.js @@ -52,9 +52,14 @@ const decorateLocales = (current) => { delete locales[currLocale]; return Object.keys(locales).map((key) => { const prefix = key === '' ? key : `/${key}`; - const localePath = currLocale === '' + let localePath = currLocale === '' ? `/${key}${current.pathname}` : current.pathname.replace(current.locale.prefix, prefix); + if (localePath.startsWith('/langstore/')) { + // eslint-disable-next-line no-param-reassign + key = 'langstore/en'; + localePath = localePath.replace('/langstore/', `/${key}/`); + } const li = createTag('li', { class: 'detail' }, `${key || 'us'}`); getStatus(li, localePath); return li; From c0395ae278f97a006f9f30fefaf6d968a6cf258d Mon Sep 17 00:00:00 2001 From: Saloni Jain <6162294+salonijain3@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:24:09 +0530 Subject: [PATCH 5/5] Revert "MWPW-146528[MILO][MEP][ANALYTICS] Add attribute to content changed by Target for analytics and MWPW-152274" (#2627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "MWPW-146528[MILO][MEP][ANALYTICS] Add attribute to content changed by…" This reverts commit c5fcc8e82016236eac7e1c81391bafa38a5cc0ab. --- libs/blocks/fragment/fragment.js | 19 +- .../global-navigation/global-navigation.js | 12 +- .../global-navigation/utilities/menu/menu.js | 4 +- .../global-navigation/utilities/utilities.js | 12 +- .../personalization/personalization.js | 308 +++++++----------- libs/martech/martech.js | 42 ++- .../marquee/marquee.css | 0 libs/mep/mwpw-142224/marquee/marquee.js | 3 + libs/mep/sample-block-code/marquee/marquee.js | 3 - libs/utils/utils.js | 92 ++++-- test/features/personalization/actions.test.js | 55 +--- .../actionsTargetManifestId.test.js | 164 ---------- .../personalization/deprecatedActions.test.js | 15 +- test/features/personalization/mepSettings.js | 10 - .../personalization/mepTargetSettings.js | 10 - .../actions/manifestAppendToSection.json | 1 - .../mocks/actions/manifestCustomAction.json | 4 - .../mocks/actions/manifestInsertAfter.json | 3 - .../mocks/actions/manifestInsertBefore.json | 2 - .../actions/manifestPrependToSection.json | 3 +- .../mocks/actions/manifestRemove.json | 2 - .../mocks/actions/manifestReplace.json | 4 - .../mocks/actions/manifestTargetReplace.json | 48 --- .../mocks/actions/manifestUseBlockCode.json | 1 - .../mocks/actions/manifestUseBlockCode2.json | 1 - ...replace-content-target-howto-h2.plain.html | 1 - .../personalization/pageFilter.test.js | 7 +- .../personalization/personalization.test.js | 97 +----- .../personalization/replacePage.test.js | 5 +- test/utils/utils-mep.test.js | 3 +- test/utils/utils.test.js | 4 +- 31 files changed, 298 insertions(+), 637 deletions(-) rename libs/mep/{sample-block-code => mwpw-142224}/marquee/marquee.css (100%) create mode 100644 libs/mep/mwpw-142224/marquee/marquee.js delete mode 100644 libs/mep/sample-block-code/marquee/marquee.js delete mode 100644 test/features/personalization/actionsTargetManifestId.test.js delete mode 100644 test/features/personalization/mepSettings.js delete mode 100644 test/features/personalization/mepTargetSettings.js delete mode 100644 test/features/personalization/mocks/actions/manifestTargetReplace.json delete mode 100644 test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html diff --git a/libs/blocks/fragment/fragment.js b/libs/blocks/fragment/fragment.js index 84a922d6f5..2bca02da83 100644 --- a/libs/blocks/fragment/fragment.js +++ b/libs/blocks/fragment/fragment.js @@ -32,6 +32,12 @@ const updateFragMap = (fragment, a, href) => { } }; +const setManifestIdOnChildren = (sections, manifestId) => { + [...sections[0].children].forEach( + (child) => (child.dataset.manifestId = manifestId), + ); +}; + const insertInlineFrag = (sections, a, relHref) => { // Inline fragments only support one section, other sections are ignored const fragChildren = [...sections[0].children]; @@ -73,7 +79,7 @@ export default async function init(a) { } const path = new URL(a.href).pathname; - if (mep?.fragments?.[path]) { + if (mep?.fragments?.[path] && mep) { relHref = mep.handleFragmentCommand(mep?.fragments[path], a); if (!relHref) return; } @@ -116,10 +122,15 @@ export default async function init(a) { } updateFragMap(fragment, a, relHref); - if (a.dataset.manifestId || a.dataset.adobeTargetTestid) { - const { updateFragDataProps } = await import('../../features/personalization/personalization.js'); - updateFragDataProps(a, inline, sections, fragment); + + if (a.dataset.manifestId) { + if (inline) { + setManifestIdOnChildren(sections, a.dataset.manifestId); + } else { + fragment.dataset.manifestId = a.dataset.manifestId; + } } + if (inline) { insertInlineFrag(sections, a, relHref); } else { diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index 8da869c76e..4bdab1954a 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -33,7 +33,7 @@ import { toFragment, trigger, yieldToMain, - addMepHighlightAndTargetId, + addMepHighlight, } from './utilities/utilities.js'; import { replaceKey, replaceKeyArray } from '../../features/placeholders.js'; @@ -919,14 +919,14 @@ class Gnav { observer.observe(dropdownTrigger, { attributeFilter: ['aria-expanded'] }); delayDropdownDecoration({ template: triggerTemplate }); - return addMepHighlightAndTargetId(triggerTemplate, item); + return addMepHighlight(triggerTemplate, item); } case 'primaryCta': case 'secondaryCta': // Remove its 'em' or 'strong' wrapper item.parentElement.replaceWith(item); - return addMepHighlightAndTargetId(toFragment`
+ return addMepHighlight(toFragment`
${decorateCta({ elem: item, type: itemType, index: index + 1 })}
`, item); case 'link': { @@ -945,15 +945,15 @@ class Gnav {
${linkElem}
`; - return addMepHighlightAndTargetId(linkTemplate, item); + return addMepHighlight(linkTemplate, item); } case 'text': - return addMepHighlightAndTargetId(toFragment`
+ return addMepHighlight(toFragment`
${item.textContent}
`, item); default: /* c8 ignore next 3 */ - return addMepHighlightAndTargetId(toFragment`
+ return addMepHighlight(toFragment`
${item}
`, item); } diff --git a/libs/blocks/global-navigation/utilities/menu/menu.js b/libs/blocks/global-navigation/utilities/menu/menu.js index aa2a21e704..d5f6d8fabb 100644 --- a/libs/blocks/global-navigation/utilities/menu/menu.js +++ b/libs/blocks/global-navigation/utilities/menu/menu.js @@ -11,7 +11,7 @@ import { toFragment, trigger, yieldToMain, - addMepHighlightAndTargetId, + addMepHighlight, } from '../utilities.js'; const decorateHeadline = (elem, index) => { @@ -317,7 +317,7 @@ const decorateMenu = (config) => logErrorFor(async () => { ${menuContent}
`; - addMepHighlightAndTargetId(menuTemplate, content); + addMepHighlight(menuTemplate, content); decorateCrossCloudMenu(menuTemplate); diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index a4c73212f7..fcc3c24f74 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -47,12 +47,13 @@ export const logErrorFor = async (fn, message, tags) => { } }; -export function addMepHighlightAndTargetId(el, source) { - let { manifestId, targetManifestId } = source.dataset; - manifestId ??= source?.closest('[data-manifest-id]')?.dataset?.manifestId; - targetManifestId ??= source?.closest('[data-adobe-target-testid]')?.dataset?.adobeTargetTestid; +export function addMepHighlight(el, source) { + let { manifestId } = source.dataset; + if (!manifestId) { + const closestManifestId = source?.closest('[data-manifest-id]'); + if (closestManifestId) manifestId = closestManifestId.dataset.manifestId; + } if (manifestId) el.dataset.manifestId = manifestId; - if (targetManifestId) el.dataset.adobeTargetTestid = targetManifestId; return el; } @@ -309,7 +310,6 @@ export async function fetchAndProcessPlainHtml({ url, shouldDecorateLinks = true const text = await res.text(); const { body } = new DOMParser().parseFromString(text, 'text/html'); if (mepFragment?.manifestId) body.dataset.manifestId = mepFragment.manifestId; - if (mepFragment?.targetManifestId) body.dataset.adobeTargetTestid = mepFragment.targetManifestId; const commands = mepGnav?.commands; if (commands?.length) { const { handleCommands, deleteMarkedEls } = await import('../../../features/personalization/personalization.js'); diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index c58b2c6953..30b993165b 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -1,4 +1,3 @@ -/* eslint-disable no-underscore-dangle */ /* eslint-disable no-console */ import { @@ -55,6 +54,8 @@ const DATA_TYPE = { const IN_BLOCK_SELECTOR_PREFIX = 'in-block:'; +export const appendJsonExt = (path) => (path.endsWith('.json') ? path : `${path}.json`); + export const normalizePath = (p, localize = true) => { let path = p; @@ -86,6 +87,28 @@ export const normalizePath = (p, localize = true) => { return path; }; +export const preloadManifests = ({ targetManifests = [], persManifests = [] }) => { + let manifests = targetManifests; + + manifests = manifests.concat( + persManifests.map((manifest) => ({ + ...manifest, + manifestPath: normalizePath(appendJsonExt(manifest.manifestPath)), + manifestUrl: manifest.manifestPath, + })), + ); + + for (const manifest of manifests) { + if (!manifest.manifestData && manifest.manifestPath && !manifest.disabled) { + loadLink( + manifest.manifestPath, + { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }, + ); + } + } + return manifests; +}; + export const getFileName = (path) => path?.split('/').pop(); const isInLcpSection = (el) => { @@ -93,7 +116,7 @@ const isInLcpSection = (el) => { return lcpSection === el || lcpSection?.contains(el); }; -export const createFrag = (el, url, manifestId, targetManifestId) => { +export const createFrag = (el, url, manifestId) => { let href = url; try { const { pathname, search, hash } = new URL(url); @@ -103,7 +126,6 @@ export const createFrag = (el, url, manifestId, targetManifestId) => { } const a = createTag('a', { href }, url); if (manifestId) a.dataset.manifestId = manifestId; - if (targetManifestId) a.dataset.adobeTargetTestid = targetManifestId; let frag = createTag('p', undefined, a); const isDelayedModalAnchor = /#.*delay=/.test(href); if (isDelayedModalAnchor) frag.classList.add('hide-block'); @@ -140,9 +162,9 @@ const COMMANDS = { } el.classList.add(CLASS_EL_DELETE); }, - replace: (el, target, manifestId, targetManifestId) => { + replace: (el, target, manifestId) => { if (!el || el.classList.contains(CLASS_EL_REPLACE)) return; - el.insertAdjacentElement('beforebegin', createFrag(el, target, manifestId, targetManifestId)); + el.insertAdjacentElement('beforebegin', createFrag(el, target, manifestId)); el.classList.add(CLASS_EL_DELETE, CLASS_EL_REPLACE); }, }; @@ -205,7 +227,6 @@ const consolidateObjects = (arr, prop, existing = {}) => arr.reduce((propMap, it selector, manifestPath: item.manifestPath, manifestId: i.manifestId, - targetManifestId: i.targetManifestId, }; // eslint-disable-next-line no-restricted-syntax for (const key in propMap) { @@ -289,9 +310,9 @@ function getSection(rootEl, idx) { : rootEl.querySelector(`:scope > div:nth-child(${idx})`); } -function registerInBlockActions(cmd, manifestId, targetManifestId) { +function registerInBlockActions(cmd, manifestId) { const { action, target, selector } = cmd; - const command = { action, target, manifestId, targetManifestId }; + const command = { action, target, manifestId }; const blockAndSelector = selector.substring(IN_BLOCK_SELECTOR_PREFIX.length).trim().split(/\s+/); const [blockName] = blockAndSelector; @@ -403,39 +424,24 @@ const addHash = (url, newHash) => { } }; -const setDataIdOnChildren = (sections, id, value) => { - [...sections[0].children].forEach( - (child) => (child.dataset[id] = value), - ); -}; - -export const updateFragDataProps = (a, inline, sections, fragment) => { - const { manifestId, adobeTargetTestid } = a.dataset; - if (inline) { - if (manifestId) setDataIdOnChildren(sections, 'manifestId', manifestId); - if (adobeTargetTestid) setDataIdOnChildren(sections, 'adobeTargetTestid', adobeTargetTestid); - } else { - if (manifestId) fragment.dataset.manifestId = manifestId; - if (adobeTargetTestid) fragment.dataset.adobeTargetTestid = adobeTargetTestid; - } -}; export function handleCommands(commands, rootEl = document, forceInline = false) { commands.forEach((cmd) => { - const { manifestId, targetManifestId, action, selector, target: trgt } = cmd; + const { manifestId } = cmd; + const { action, selector, target: trgt } = cmd; const target = forceInline ? addHash(trgt, INLINE_HASH) : trgt; if (selector.startsWith(IN_BLOCK_SELECTOR_PREFIX)) { - registerInBlockActions(cmd, manifestId, targetManifestId); + registerInBlockActions(cmd, manifestId); return; } if (action in COMMANDS) { const el = getSelectedElement(selector, action, rootEl); - COMMANDS[action](el, target, manifestId, targetManifestId); + COMMANDS[action](el, target, manifestId); } else if (action in CREATE_CMDS) { const el = getSelectedElement(selector, action, rootEl); el?.insertAdjacentElement( CREATE_CMDS[action], - createFrag(el, target, manifestId, targetManifestId), + createFrag(el, target, manifestId), ); } else { /* c8 ignore next 2 */ @@ -444,21 +450,14 @@ export function handleCommands(commands, rootEl = document, forceInline = false) }); } -const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOverrideName) => { - const config = getConfig(); - let manifestId = getFileName(manifestPath); - let targetId = manifestId.replace('.json', ''); - if (manifestOverrideName) targetId = manifestOverrideName; - if (!config.mep?.preview) manifestId = false; +const getVariantInfo = (line, variantNames, variants, manifestId) => { const action = line.action?.toLowerCase().replace('content', '').replace('fragment', ''); const { selector } = line; const pageFilter = line['page filter'] || line['page filter optional']; if (pageFilter && !matchGlob(pageFilter, new URL(window.location).pathname)) return; - if (!config.mep?.preview) manifestId = false; variantNames.forEach((vn) => { - const targetManifestId = vn.startsWith(TARGET_EXP_PREFIX) ? targetId : false; if (!line[vn] || line[vn].toLowerCase() === 'false') return; const variantInfo = { @@ -468,7 +467,6 @@ const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOver target: line[vn], selectorType: checkSelectorType(selector), manifestId, - targetManifestId, }; if (action in COMMANDS && variantInfo.selectorType === 'fragment') { @@ -477,7 +475,6 @@ const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOver val: normalizePath(line[vn]), action, manifestId, - targetManifestId, }); } else if (GLOBAL_CMDS.includes(action)) { variants[vn][action] = variants[vn][action] || []; @@ -489,7 +486,6 @@ const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOver val: blockTarget, pageFilter, manifestId, - targetManifestId, }); } else { variants[vn][action].push({ @@ -497,7 +493,6 @@ const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOver val: normalizePath(line[vn]), pageFilter, manifestId, - targetManifestId, }); } } else if (action in COMMANDS || action in CREATE_CMDS) { @@ -509,7 +504,7 @@ const getVariantInfo = (line, variantNames, variants, manifestPath, manifestOver }); }; -export function parseManifestVariants(data, manifestPath, manifestOverrideName) { +export function parseManifestVariants(data, manifestId) { if (!data?.length) return null; const manifestConfig = {}; @@ -524,15 +519,10 @@ export function parseManifestVariants(data, manifestPath, manifestOverrideName) variants[vn] = { commands: [], fragments: [] }; }); - experiences.forEach((line) => { - getVariantInfo(line, variantNames, variants, manifestPath, manifestOverrideName); - }); + experiences.forEach((line) => getVariantInfo(line, variantNames, variants, manifestId)); manifestConfig.variants = variants; manifestConfig.variantNames = variantNames; - const config = getConfig(); - if (!config.mep?.preview) manifestConfig.manifestId = false; - return manifestConfig; } catch (e) { /* c8 ignore next 3 */ @@ -625,30 +615,12 @@ const createDefaultExperiment = (manifest) => ({ disabled: manifest.disabled, event: manifest.event, manifest: manifest.manifestPath, - executionOrder: '1-1', selectedVariant: { commands: [], fragments: [] }, selectedVariantName: 'default', variantNames: ['all'], variants: {}, }); -export const addMepAnalytics = (config, header) => { - config.mep.experiments.forEach((experiment) => { - experiment?.selectedVariant?.useblockcode?.forEach(({ selector, targetManifestId }) => { - if (selector && targetManifestId) { - document.querySelectorAll(`.${selector}`) - .forEach((el) => (el.dataset.adobeTargetTestid = targetManifestId)); - } - }); - if (header) { - experiment?.selectedVariant?.updatemetadata?.forEach((updateMetaData) => { - if (updateMetaData?.selector === 'gnav-source' && updateMetaData.targetManifestId) { - header.dataset.adobeTargetTestid = updateMetaData.targetManifestId; - } - }); - } - }); -}; export async function getManifestConfig(info, variantOverride = false) { const { name, @@ -661,7 +633,7 @@ export async function getManifestConfig(info, variantOverride = false) { disabled, event, } = info; - if (disabled && (!variantOverride || !Object.keys(variantOverride).length)) { + if (disabled && !variantOverride) { return createDefaultExperiment(info); } let data = manifestData; @@ -672,25 +644,33 @@ export async function getManifestConfig(info, variantOverride = false) { const persData = data?.experiences?.data || data?.data || data; if (!persData) return null; - const infoTab = manifestInfo || data?.info?.data; - const infoObj = infoTab?.reduce((acc, item) => { - acc[item.key] = item.value; - return acc; - }, {}); - const manifestOverrideName = name || infoObj?.['manifest-override-name']?.toLowerCase(); - const manifestConfig = parseManifestVariants(persData, manifestPath, manifestOverrideName); + + let manifestId = getFileName(manifestPath); + const config = getConfig(); + if (!config.mep?.preview) { + manifestId = false; + } else if (name) { + manifestId = `${name}: ${manifestId}`; + } + const manifestConfig = parseManifestVariants(persData, manifestId); if (!manifestConfig) { /* c8 ignore next 3 */ console.log('Error loading personalization manifestConfig: ', name || manifestPath); return null; } + + const infoTab = manifestInfo || data?.info?.data; const infoKeyMap = { 'manifest-type': ['Personalization', 'Promo', 'Test'], 'manifest-execution-order': ['First', 'Normal', 'Last'], }; if (infoTab) { - manifestConfig.manifestOverrideName = manifestOverrideName; + const infoObj = infoTab?.reduce((acc, item) => { + acc[item.key] = item.value; + return acc; + }, {}); + manifestConfig.manifestOverrideName = infoObj?.['manifest-override-name']?.toLowerCase(); manifestConfig.manifestType = infoObj?.['manifest-type']?.toLowerCase(); const executionOrder = { 'manifest-type': 1, @@ -724,6 +704,7 @@ export async function getManifestConfig(info, variantOverride = false) { manifestConfig.selectedVariantName = 'default'; manifestConfig.selectedVariant = 'default'; } + const placeholders = manifestPlaceholders || data?.placeholders?.data; if (placeholders) { updateConfig( @@ -744,22 +725,22 @@ export const deleteMarkedEls = (rootEl = document) => { .forEach((el) => el.remove()); }; -const normalizeFragPaths = ({ selector, val, action, manifestId, targetManifestId }) => ({ +const normalizeFragPaths = ({ selector, val, action }) => ({ selector: normalizePath(selector), val: normalizePath(val), action, - manifestId, - targetManifestId, }); -export async function categorizeActions(experiment, config) { + +export async function categorizeActions(experiment) { if (!experiment) return null; const { manifestPath, selectedVariant } = experiment; if (!selectedVariant || selectedVariant === 'default') return { experiment }; - // only one replacepage can be defined - const { replacepage } = selectedVariant; - // eslint-disable-next-line prefer-destructuring - if (selectedVariant.replacepage?.length) config.mep.replacepage = replacepage[0]; + if (selectedVariant.replacepage) { + // only one replacepage can be defined + await replaceInner(selectedVariant.replacepage[0]?.val, document.querySelector('main')); + document.querySelector('main').dataset.manifestId = manifestPath; + } selectedVariant.insertscript?.map((script) => loadScript(script.val)); selectedVariant.updatemetadata?.map((metadata) => setMetadata(metadata)); @@ -823,18 +804,14 @@ export function cleanAndSortManifestList(manifests) { window.lana?.log(`MEP Error parsing manifests: ${e.toString()}`); } }); - Object.keys(manifestObj).forEach((key) => { - delete manifestObj[key].variants; - }); return Object.values(manifestObj).sort(compareExecutionOrder); } export function handleFragmentCommand(command, a) { - const { action, fragment, manifestId, targetManifestId } = command; + const { action, fragment, manifestId } = command; if (action === 'replace') { a.href = fragment; if (manifestId) a.dataset.manifestId = manifestId; - if (targetManifestId) a.dataset.adobeTargetTestid = targetManifestId; return fragment; } if (action === 'remove') { @@ -848,125 +825,64 @@ export function handleFragmentCommand(command, a) { } export async function applyPers(manifests, postLCP = false) { - if (!manifests?.length) return; - let experiments = manifests; - const config = getConfig(); - for (let i = 0; i < experiments.length; i += 1) { - experiments[i] = await getManifestConfig(experiments[i], config.mep?.variantOverride); - } - - experiments = cleanAndSortManifestList(experiments); - - let results = []; - - for (const experiment of experiments) { - const result = await categorizeActions(experiment, config); - if (result) results.push(result); - } - results = results.filter(Boolean); - - config.mep.experiments = [...config.mep.experiments, ...experiments]; - config.mep.blocks = consolidateObjects(results, 'blocks', config.mep.blocks); - config.mep.fragments = consolidateObjects(results, 'fragments', config.mep.fragments); - config.mep.commands = consolidateArray(results, 'commands', config.mep.commands); - - const main = document.querySelector('main'); - if (config.mep.replacepage && !postLCP && main) { - await replaceInner(config.mep.replacepage.val, main); - const { manifestId, targetManifestId } = config.mep.replacepage; - if (manifestId) main.dataset.manifestId = manifestId; - if (targetManifestId) main.dataset.adobeTargetTestid = targetManifestId; - } + try { + const config = getConfig(); + if (!postLCP) { + const { + mep: mepParam, + mepHighlight, + mepButton, + } = Object.fromEntries(PAGE_URL.searchParams); + config.mep = { + handleFragmentCommand, + preview: (mepButton !== 'off' + && (config.env?.name !== 'prod' || mepParam || mepParam === '' || mepButton)), + variantOverride: parseMepParam(mepParam), + highlight: (mepHighlight !== undefined && mepHighlight !== 'false'), + mepParam, + targetEnabled: config.mep?.targetEnabled, + }; + } - if (!postLCP) handleCommands(config.mep.commands); - deleteMarkedEls(); + if (!manifests?.length) return; + let experiments = manifests; + for (let i = 0; i < experiments.length; i += 1) { + experiments[i] = await getManifestConfig(experiments[i], config.mep?.variantOverride); + } - const pznList = results.filter((r) => (r.experiment?.manifestType === TRACKED_MANIFEST_TYPE)); - if (!pznList.length) return; + experiments = cleanAndSortManifestList(experiments); - const pznVariants = pznList.map((r) => { - 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); - }); - config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; -} + let results = []; -export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => { - let persManifests = []; + for (const experiment of experiments) { + const result = await categorizeActions(experiment); + if (result) { + results.push(result); + } + } + results = results.filter(Boolean); - if (persEnabled) { - persManifests = persEnabled.toLowerCase() - .split(/,|(\s+)|(\\n)/g) - .filter((path) => path?.trim()) - .map((manifestPath) => ({ manifestPath })); - } + config.mep.experiments ??= []; + config.mep.experiments = experiments; + config.mep.blocks = consolidateObjects(results, 'blocks', config.mep.blocks); + config.mep.fragments = consolidateObjects(results, 'fragments', config.mep.fragments); + config.mep.commands = consolidateArray(results, 'commands', config.mep.commands); - if (promoEnabled) { - const { default: getPromoManifests } = await import('./promo-utils.js'); - persManifests = persManifests.concat(getPromoManifests(promoEnabled, PAGE_URL.searchParams)); - } + if (!postLCP) handleCommands(config.mep.commands); + deleteMarkedEls(); - if (mepParam && mepParam !== 'off') { - const persManifestPaths = persManifests.map((manifest) => { - const { manifestPath } = manifest; - if (manifestPath.startsWith('/')) return manifestPath; - try { - const url = new URL(manifestPath); - return url.pathname; - } catch (e) { - return manifestPath; - } - }); + const pznList = results.filter((r) => (r.experiment?.manifestType === TRACKED_MANIFEST_TYPE)); + if (!pznList.length) return; - mepParam.split('---').forEach((manifestPair) => { - const manifestPath = manifestPair.trim().toLowerCase().split('--')[0]; - if (!persManifestPaths.includes(manifestPath)) { - persManifests.push({ manifestPath }); - } + const pznVariants = pznList.map((r) => { + const val = r.experiment.selectedVariantName.replace(TARGET_EXP_PREFIX, '').trim().slice(0, 15); + return val === 'default' ? 'nopzn' : val; }); - } - return persManifests; -}; - -export async function init(enablements = {}) { - let manifests = []; - const { - mepParam, mepHighlight, mepButton, pzn, promo, target, postLCP, - } = enablements; - const config = getConfig(); - if (!postLCP) { - config.mep = { - handleFragmentCommand, - updateFragDataProps, - preview: (mepButton !== 'off' - && (config.env?.name !== 'prod' || mepParam || mepParam === '' || mepButton)), - variantOverride: parseMepParam(mepParam), - highlight: (mepHighlight !== undefined && mepHighlight !== 'false'), - targetEnabled: target, - experiments: [], - }; - manifests = manifests.concat(await combineMepSources(pzn, promo, mepParam)); - manifests?.forEach((manifest) => { - if (manifest.disabled) return; - const localizedURL = localizeLink(manifest.manifestPath); - loadLink(localizedURL, { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }); + const pznManifests = pznList.map((r) => { + const val = r.experiment?.manifestOverrideName || r.experiment?.manifest; + return getFileName(val).replace('.json', '').trim().slice(0, 15); }); - } - - if (target === true || (target === 'gnav' && postLCP)) { - const { getTargetPersonalization } = await import('../../martech/martech.js'); - const { targetManifests, targetPropositions } = await getTargetPersonalization(); - manifests = manifests.concat(targetManifests); - if (targetPropositions?.length && window._satellite) { - window._satellite.track('propositionDisplay', targetPropositions); - } - } - try { - await applyPers(manifests, postLCP); + config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; } catch (e) { console.warn(e); window.lana?.log(`MEP Error: ${e.toString()}`); diff --git a/libs/martech/martech.js b/libs/martech/martech.js index fac3ef32a8..bef35b6249 100644 --- a/libs/martech/martech.js +++ b/libs/martech/martech.js @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ import { getConfig, getMetadata, loadIms, loadLink, loadScript } from '../utils/utils.js'; const ALLOY_SEND_EVENT = 'alloy_sendEvent'; @@ -46,6 +47,16 @@ const waitForEventOrTimeout = (eventName, timeout, returnValIfTimeout) => new Pr window.addEventListener(ALLOY_SEND_EVENT_ERROR, errorListener, { once: true }); }); +const getExpFromParam = (expParam) => { + const lastSlash = expParam.lastIndexOf('/'); + return { + experiments: [{ + experimentPath: expParam.substring(0, lastSlash), + variantLabel: expParam.substring(lastSlash + 1), + }], + }; +}; + const handleAlloyResponse = (response) => { const items = ( (response.propositions?.length && response.propositions) @@ -105,9 +116,12 @@ function sendTargetResponseAnalytics(failure, responseStart, timeout, message) { }); } -export const getTargetPersonalization = async () => { +const getTargetPersonalization = async () => { const params = new URL(window.location.href).searchParams; + const experimentParam = params.get('experiment'); + if (experimentParam) return getExpFromParam(experimentParam); + const timeout = parseInt(params.get('target-timeout'), 10) || parseInt(getMetadata('target-timeout'), 10) || TARGET_TIMEOUT_MS; @@ -235,7 +249,7 @@ const loadMartechFiles = async (config) => { ? '/marketingtech/main.standard.min.js' : '/marketingtech/main.standard.qa.min.js' )); - // eslint-disable-next-line no-underscore-dangle + window._satellite.track('pageload'); }; @@ -243,8 +257,30 @@ const loadMartechFiles = async (config) => { return filesLoadedPromise; }; -export default async function init() { +export default async function init({ + persEnabled = false, + persManifests = [], + postLCP = false, +}) { const config = getConfig(); const martechPromise = loadMartechFiles(config); + + if (persEnabled) { + loadLink( + `${config.miloLibs || config.codeRoot}/features/personalization/personalization.js`, + { as: 'script', rel: 'modulepreload' }, + ); + + const { targetManifests, targetPropositions } = await getTargetPersonalization(); + if (targetManifests?.length || persManifests?.length) { + const { preloadManifests, applyPers } = await import('../features/personalization/personalization.js'); + const manifests = preloadManifests({ targetManifests, persManifests }); + await applyPers(manifests, postLCP); + if (targetPropositions?.length && window._satellite) { + window._satellite.track('propositionDisplay', targetPropositions); + } + } + } + return martechPromise; } diff --git a/libs/mep/sample-block-code/marquee/marquee.css b/libs/mep/mwpw-142224/marquee/marquee.css similarity index 100% rename from libs/mep/sample-block-code/marquee/marquee.css rename to libs/mep/mwpw-142224/marquee/marquee.css diff --git a/libs/mep/mwpw-142224/marquee/marquee.js b/libs/mep/mwpw-142224/marquee/marquee.js new file mode 100644 index 0000000000..e7742cca13 --- /dev/null +++ b/libs/mep/mwpw-142224/marquee/marquee.js @@ -0,0 +1,3 @@ +export default function init(el) { + el.innerHTML = 'Marquee was replaced MEP and the content were overwritten.'; +} diff --git a/libs/mep/sample-block-code/marquee/marquee.js b/libs/mep/sample-block-code/marquee/marquee.js deleted file mode 100644 index f322c6e100..0000000000 --- a/libs/mep/sample-block-code/marquee/marquee.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function init(el) { - el.innerHTML = 'Marquee code was replaced MEP and the content was overwritten.'; -} diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 28b5b4b8b5..c4f4a6f64b 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -922,36 +922,81 @@ export const getMepEnablement = (mdKey, paramKey = false) => { return getMdValue(mdKey); }; +export const combineMepSources = async (persEnabled, promoEnabled, mepParam) => { + let persManifests = []; + + if (persEnabled) { + persManifests = persEnabled.toLowerCase() + .split(/,|(\s+)|(\\n)/g) + .filter((path) => path?.trim()) + .map((manifestPath) => ({ manifestPath })); + } + + if (promoEnabled) { + const { default: getPromoManifests } = await import('../features/personalization/promo-utils.js'); + persManifests = persManifests.concat(getPromoManifests(promoEnabled, PAGE_URL.searchParams)); + } + + if (mepParam && mepParam !== 'off') { + const persManifestPaths = persManifests.map((manifest) => { + const { manifestPath } = manifest; + if (manifestPath.startsWith('/')) return manifestPath; + try { + const url = new URL(manifestPath); + return url.pathname; + } catch (e) { + return manifestPath; + } + }); + + mepParam.split('---').forEach((manifestPair) => { + const manifestPath = manifestPair.trim().toLowerCase().split('--')[0]; + if (!persManifestPaths.includes(manifestPath)) { + persManifests.push({ manifestPath }); + } + }); + } + return persManifests; +}; + async function checkForPageMods() { - const { mep: mepParam, mepHighlight, mepButton } = Object.fromEntries(PAGE_URL.searchParams); + const { mep: mepParam } = Object.fromEntries(PAGE_URL.searchParams); if (mepParam === 'off') return; - const pzn = getMepEnablement('personalization'); - const promo = getMepEnablement('manifestnames', PROMO_PARAM); - const target = getMepEnablement('target'); - if (!pzn && !target && !promo && !mepParam && !mepHighlight && !mepButton) return; + const persEnabled = getMepEnablement('personalization'); + const promoEnabled = getMepEnablement('manifestnames', PROMO_PARAM); + const targetEnabled = getMepEnablement('target'); + const mepEnabled = persEnabled || targetEnabled || promoEnabled || mepParam; + if (!mepEnabled) return; - if (target) { - loadMartech(); - } else if (pzn) { - loadIms() - .then(() => { - /* c8 ignore next */ - if (window.adobeIMS?.isSignedInUser()) loadMartech(); - }) - .catch((e) => { console.log('Unable to load IMS:', e); }); + const config = getConfig(); + config.mep = { targetEnabled }; + loadLink( + `${config.base}/features/personalization/personalization.js`, + { as: 'script', rel: 'modulepreload' }, + ); + + const persManifests = await combineMepSources(persEnabled, promoEnabled, mepParam); + if (targetEnabled === true) { + await loadMartech({ persEnabled: true, persManifests, targetEnabled }); + return; } + if (!persManifests.length) return; - const { init } = await import('../features/personalization/personalization.js'); - await init({ - mepParam, mepHighlight, mepButton, pzn, promo, target, - }); + loadIms() + .then(() => { + if (window.adobeIMS.isSignedInUser()) loadMartech(); + }) + .catch((e) => { console.log('Unable to load IMS:', e); }); + + const { preloadManifests, applyPers } = await import('../features/personalization/personalization.js'); + const manifests = preloadManifests({ persManifests }, { getConfig, loadLink }); + + await applyPers(manifests); } async function loadPostLCP(config) { if (config.mep?.targetEnabled === 'gnav') { - /* c8 ignore next 2 */ - const { init } = await import('../features/personalization/personalization.js'); - await init({ postLCP: true }); + await loadMartech({ persEnabled: true, postLCP: true }); } else { loadMartech(); } @@ -969,11 +1014,6 @@ async function loadPostLCP(config) { loadTemplate(); const { default: loadFonts } = await import('./fonts.js'); loadFonts(config.locale, loadStyle); - - if (config?.mep) { - import('../features/personalization/personalization.js') - .then(({ addMepAnalytics }) => addMepAnalytics(config, header)); - } if (config.mep?.preview) { import('../features/personalization/preview.js') .then(({ default: decoratePreviewMode }) => decoratePreviewMode()); diff --git a/test/features/personalization/actions.test.js b/test/features/personalization/actions.test.js index fe9e935202..e9bce4b9c0 100644 --- a/test/features/personalization/actions.test.js +++ b/test/features/personalization/actions.test.js @@ -3,8 +3,8 @@ import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig, loadBlock } from '../../../libs/utils/utils.js'; import initFragments from '../../../libs/blocks/fragment/fragment.js'; -import { init, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; -import mepSettings from './mepSettings.js'; +import { applyPers, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; +import spoofParams from './spoofParams.js'; document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -35,12 +35,10 @@ describe('replace action', () => { expect(document.querySelector('.how-to')).to.not.be.null; const parentEl = document.querySelector('#features-of-milo-experimentation-platform')?.parentElement; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector('#features-of-milo-experimentation-platform')).to.be.null; - const el = parentEl.firstElementChild.firstElementChild; - expect(el.href) + expect(parentEl.firstElementChild.firstElementChild.href) .to.equal('http://localhost:2000/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2'); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); // .how-to should not be changed as it is targeted to firefox expect(document.querySelector('.how-to')).to.not.be.null; }); @@ -54,8 +52,7 @@ describe('replace action', () => { expect(document.querySelector('a[href="/fragments/replaceme"]')).to.exist; expect(document.querySelector('a[href="/fragments/inline-replaceme#_inline"]')).to.exist; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragmentResp = await readFile({ path: './mocks/fragments/fragmentReplaced.plain.html' }); const inlineFragmentResp = await readFile({ path: './mocks/fragments/inlineFragReplaced.plain.html' }); @@ -86,8 +83,7 @@ describe('insertAfter action', async () => { expect(document.querySelector('a[href="/fragments/insertafter"]')).to.be.null; expect(document.querySelector('a[href="/fragments/insertafterfragment"]')).to.be.null; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); let fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter"]'); expect(fragment).to.not.be.null; @@ -109,8 +105,7 @@ describe('insertBefore action', async () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/fragments/insertbefore"]')).to.be.null; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); let fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertbefore"]'); expect(fragment).to.not.be.null; @@ -132,8 +127,7 @@ describe('prependToSection action', async () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/prependToSection"]')).to.be.null; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragment = document.querySelector('main > div:nth-child(2) > div:first-child a[href="/test/features/personalization/mocks/fragments/prependToSection"]'); expect(fragment).to.not.be.null; @@ -149,8 +143,7 @@ describe('appendToSection action', async () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/appendToSection"]')).to.be.null; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragment = document.querySelector('main > div:nth-child(2) > div:last-child a[href="/test/features/personalization/mocks/fragments/appendToSection"]'); expect(fragment).to.not.be.null; @@ -162,9 +155,7 @@ describe('remove action', () => { let manifestJson = await readFile({ path: './mocks/actions/manifestRemove.json' }); manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - mepSettings.mepButton = 'off'; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); }); it('remove should remove content', async () => { expect(document.querySelector('.z-pattern')).to.be.null; @@ -177,6 +168,7 @@ describe('remove action', () => { }); it('removeContent should tag but not remove content in preview', async () => { + spoofParams({ mep: '' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); let manifestJson = await readFile({ path: './mocks/actions/manifestRemove.json' }); @@ -185,10 +177,7 @@ describe('remove action', () => { setTimeout(async () => { expect(document.querySelector('.z-pattern')).to.not.be.null; - mepSettings.mepButton = false; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); - + await applyPers([{ manifestPath: '/mocks/manifestRemove.json' }]); expect(document.querySelector('.z-pattern')).to.not.be.null; expect(document.querySelector('.z-pattern').dataset.removedManifestId).to.not.be.null; @@ -206,9 +195,7 @@ describe('useBlockCode action', async () => { manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await init(mepSettings); - expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0] - .targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(getConfig().mep.blocks).to.deep.equal({ promo: 'http://localhost:2000/test/features/personalization/mocks/promo' }); const promoBlock = document.querySelector('.promo'); @@ -222,9 +209,7 @@ describe('useBlockCode action', async () => { manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await init(mepSettings); - expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0] - .targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(getConfig().mep.blocks).to.deep.equal({ myblock: 'http://localhost:2000/test/features/personalization/mocks/myblock' }); const myBlock = document.querySelector('.myblock'); @@ -239,8 +224,7 @@ describe('custom actions', async () => { let manifestJson = await readFile({ path: './mocks/actions/manifestReplace.json' }); manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal(false); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(getConfig().mep.custom).to.be.undefined; }); @@ -249,34 +233,31 @@ describe('custom actions', async () => { manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + console.log(getConfig().mep.inBlock); expect(getConfig().mep.inBlock).to.deep.equal({ 'my-block': { commands: [{ action: 'replace', target: '/fragments/fragmentreplaced', manifestId: false, - targetManifestId: false, }, { action: 'replace', target: '/fragments/new-large-menu', manifestId: false, selector: '.large-menu', - targetManifestId: false, }], fragments: { '/fragments/sub-menu': { action: 'replace', target: '/fragments/even-more-new-sub-menu', manifestId: false, - targetManifestId: false, }, '/fragments/new-sub-menu': { action: 'replace', target: '/fragments/even-more-new-sub-menu', manifestId: false, - targetManifestId: false, }, }, }, @@ -299,7 +280,7 @@ describe('custom actions', async () => { expect(document.querySelector(lcpLink)).not.to.exist; expect(document.querySelector(notLcpLink)).not.to.exist; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector(lcpLink)).to.exist; expect(document.querySelector(notLcpLink)).not.to.exist; diff --git a/test/features/personalization/actionsTargetManifestId.test.js b/test/features/personalization/actionsTargetManifestId.test.js deleted file mode 100644 index 7345cabb8b..0000000000 --- a/test/features/personalization/actionsTargetManifestId.test.js +++ /dev/null @@ -1,164 +0,0 @@ -import { expect } from '@esm-bundle/chai'; -import { readFile } from '@web/test-runner-commands'; -import { stub } from 'sinon'; -import { getConfig } from '../../../libs/utils/utils.js'; -import initFragments from '../../../libs/blocks/fragment/fragment.js'; -import { init, handleFragmentCommand, addMepAnalytics } from '../../../libs/features/personalization/personalization.js'; -import mepSettings from './mepTargetSettings.js'; - -document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); -document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); - -// Add custom keys so tests doesn't rely on real data -const config = getConfig(); -config.env = { name: 'prod' }; - -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)); -}; - -// Note that the manifestPath doesn't matter as we stub the fetch -describe('replace action', () => { - it('with a CSS Selector, it should replace an element with a fragment', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestReplace.json' }); - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); - const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2"]'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); - - it('with a fragment selector, it should replace a fragment in the document', async () => { - document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); - - let manifestJson = await readFile({ path: './mocks/actions/manifestReplace.json' }); - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.fragments['/fragments/replaceme'].targetManifestId).to.equal('manifest'); - const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2"]'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - const fragmentResp = await readFile({ path: './mocks/fragments/fragmentReplaced.plain.html' }); - const inlineFragmentResp = await readFile({ path: './mocks/fragments/inlineFragReplaced.plain.html' }); - window.fetch = stub(); - window.fetch.withArgs('http://localhost:2000/test/features/personalization/mocks/fragments/fragmentReplaced.plain.html') - .returns(getFetchPromise(fragmentResp, 'text')); - window.fetch.withArgs('http://localhost:2000/test/features/personalization/mocks/fragments/inlineFragReplaced.plain.html') - .returns(getFetchPromise(inlineFragmentResp, 'text')); - const replacemeFrag = document.querySelector('a[href="/fragments/replaceme"]'); - await initFragments(replacemeFrag); - expect(document.querySelector('a[href="/fragments/replaceme"]')).to.be.null; - expect(document.querySelector('div[data-path="/test/features/personalization/mocks/fragments/fragmentReplaced"]')).to.exist; - - const inlineReplacemeFrag = document.querySelector('a[href="/fragments/inline-replaceme#_inline"]'); - await initFragments(inlineReplacemeFrag); - expect(document.querySelector('a[href="/fragments/inline-replaceme#_inline"]')).to.be.null; - expect(document.querySelector('.inlinefragmentreplaced')).to.exist; - }); -}); - -describe('insertAfter action', async () => { - it('insertContentAfter should add fragment after target content and fragment', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestInsertAfter.json' }); - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); - const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter"]'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); -}); - -describe('insertBefore action', async () => { - it('insertContentBefore should add fragment before target content and fragment', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestInsertBefore.json' }); - - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); - const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertbefore"]'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); -}); - -describe('prependToSection action', async () => { - it('appendToSection should add fragment to beginning of section', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestPrependToSection.json' }); - - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/prependToSection"]')).to.be.null; - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); - const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/prependToSection"]'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); -}); - -describe('appendToSection action', async () => { - it('appendToSection should add fragment to end of section', async () => { - config.mep = { handleFragmentCommand }; - let manifestJson = await readFile({ path: './mocks/actions/manifestAppendToSection.json' }); - - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.commands[0].targetManifestId).to.equal('manifest'); - const el = document.querySelector('a[href="/test/features/personalization/mocks/fragments/appendToSection"]'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); -}); - -describe('useBlockCode action', async () => { - it('useBlockCode should override a current block with the custom block code provided', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestUseBlockCode.json' }); - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0].targetManifestId).to.equal('manifest'); - await addMepAnalytics(config); - const el = document.querySelector('.promo'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); - - it('useBlockCode should be able to use a new type of block', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestUseBlockCode2.json' }); - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.experiments[0].selectedVariant.useblockcode[0].targetManifestId).to.equal('manifest'); - await addMepAnalytics(config); - const el = document.querySelector('.myblock'); - expect(el.dataset.adobeTargetTestid).to.equal('manifest'); - }); -}); - -describe('custom actions', async () => { - it('should add a custom action configuration', async () => { - let manifestJson = await readFile({ path: './mocks/actions/manifestCustomAction.json' }); - manifestJson = JSON.parse(manifestJson); - setFetchResponse(manifestJson); - - await init(mepSettings); - expect(getConfig().mep.inBlock['my-block'].commands[0].targetManifestId).to.equal('manifest'); - expect(getConfig().mep.inBlock['my-block'].commands[1].targetManifestId).to.equal('manifest'); - expect(getConfig().mep.inBlock['my-block'].fragments['/fragments/sub-menu'].targetManifestId).to.equal('manifest'); - expect(getConfig().mep.inBlock['my-block'].fragments['/fragments/new-sub-menu'].targetManifestId).to.equal('manifest'); - }); -}); diff --git a/test/features/personalization/deprecatedActions.test.js b/test/features/personalization/deprecatedActions.test.js index 30ac2cb931..b6041034ad 100644 --- a/test/features/personalization/deprecatedActions.test.js +++ b/test/features/personalization/deprecatedActions.test.js @@ -3,9 +3,8 @@ import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig } from '../../../libs/utils/utils.js'; import initFragments from '../../../libs/blocks/fragment/fragment.js'; -import { init, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; +import { applyPers, handleFragmentCommand } from '../../../libs/features/personalization/personalization.js'; import spoofParams from './spoofParams.js'; -import mepSettings from './mepSettings.js'; document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -39,7 +38,7 @@ describe('Functional Test', () => { expect(document.querySelector('.how-to')).to.not.be.null; const parentEl = document.querySelector('#features-of-milo-experimentation-platform')?.parentElement; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector('#features-of-milo-experimentation-platform')).to.be.null; expect(parentEl.firstElementChild.firstElementChild.href) .to.equal('http://localhost:2000/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2'); @@ -53,7 +52,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('.z-pattern')).to.not.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector('.z-pattern')).to.be.null; }); @@ -63,7 +62,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/fragments/insertafter"]')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter"]'); expect(fragment).to.not.be.null; @@ -78,7 +77,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('a[href="/fragments/insertbefore"]')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertbefore"]'); expect(fragment).to.not.be.null; @@ -95,7 +94,7 @@ describe('Functional Test', () => { expect(document.querySelector('a[href="/fragments/replaceme"]')).to.exist; expect(document.querySelector('a[href="/fragments/inline-replaceme#_inline"]')).to.exist; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragmentResp = await readFile({ path: './mocks/fragments/fragmentReplaced.plain.html' }); const inlineFragmentResp = await readFile({ path: './mocks/fragments/inlineFragReplaced.plain.html' }); @@ -127,7 +126,7 @@ describe('Functional Test', () => { setFetchResponse(manifestJson); expect(document.querySelector('.z-pattern')).to.not.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/mocks/manifestRemove.json' }]); expect(document.querySelector('.z-pattern')).to.not.be.null; expect(document.querySelector('.z-pattern').dataset.removedManifestId).to.not.be.null; diff --git a/test/features/personalization/mepSettings.js b/test/features/personalization/mepSettings.js deleted file mode 100644 index d55161f0ae..0000000000 --- a/test/features/personalization/mepSettings.js +++ /dev/null @@ -1,10 +0,0 @@ -const mepSettings = { - mepParam: false, - mepHighlight: false, - mepButton: false, - pzn: '/path/to/manifest.json', - promo: false, - target: false, -}; - -export default mepSettings; diff --git a/test/features/personalization/mepTargetSettings.js b/test/features/personalization/mepTargetSettings.js deleted file mode 100644 index a5313f2cde..0000000000 --- a/test/features/personalization/mepTargetSettings.js +++ /dev/null @@ -1,10 +0,0 @@ -const mepSettings = { - mepParam: '/path/to/manifest.json--target-var1', - mepHighlight: false, - mepButton: false, - pzn: '/path/to/manifest.json', - promo: false, - target: false, -}; - -export default mepSettings; diff --git a/test/features/personalization/mocks/actions/manifestAppendToSection.json b/test/features/personalization/mocks/actions/manifestAppendToSection.json index 9bd9bfdf47..0269996cd6 100644 --- a/test/features/personalization/mocks/actions/manifestAppendToSection.json +++ b/test/features/personalization/mocks/actions/manifestAppendToSection.json @@ -8,7 +8,6 @@ "selector": "section2", "page filter (optional)": "", "param-newoffer=123": "", - "target-var1": "/test/features/personalization/mocks/fragments/appendToSection", "chrome": "/test/features/personalization/mocks/fragments/appendToSection" } ], diff --git a/test/features/personalization/mocks/actions/manifestCustomAction.json b/test/features/personalization/mocks/actions/manifestCustomAction.json index 1de7092bbb..fafaccbe56 100644 --- a/test/features/personalization/mocks/actions/manifestCustomAction.json +++ b/test/features/personalization/mocks/actions/manifestCustomAction.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/fragmentreplaced", - "target-var1": "/fragments/fragmentreplaced", "firefox": "", "android": "", "ios": "" @@ -20,7 +19,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/new-large-menu", - "target-var1": "/fragments/new-large-menu", "firefox": "", "android": "", "ios": "" @@ -31,7 +29,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/new-sub-menu", - "target-var1": "/fragments/new-sub-menu", "firefox": "", "android": "", "ios": "" @@ -41,7 +38,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/fragments/even-more-new-sub-menu", - "target-var1": "/fragments/even-more-new-sub-menu", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestInsertAfter.json b/test/features/personalization/mocks/actions/manifestInsertAfter.json index 0e40395cf9..d694ec80cc 100644 --- a/test/features/personalization/mocks/actions/manifestInsertAfter.json +++ b/test/features/personalization/mocks/actions/manifestInsertAfter.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertafter", - "target-var1": "/test/features/personalization/mocks/fragments/insertafter", "firefox": "", "android": "", "ios": "" @@ -20,7 +19,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertafterfragment", - "target-var1": "/test/features/personalization/mocks/fragments/insertafterfragment", "firefox": "", "android": "", "ios": "" @@ -31,7 +29,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertafterfragment", - "target-var1": "/test/features/personalization/mocks/fragments/insertafterfragment", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestInsertBefore.json b/test/features/personalization/mocks/actions/manifestInsertBefore.json index 057cfaacf4..e58e39e05c 100644 --- a/test/features/personalization/mocks/actions/manifestInsertBefore.json +++ b/test/features/personalization/mocks/actions/manifestInsertBefore.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertbefore", - "target-var1": "/test/features/personalization/mocks/fragments/insertbefore", "firefox": "", "android": "", "ios": "" @@ -20,7 +19,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/insertbeforefragment", - "target-var1": "/test/features/personalization/mocks/fragments/insertbeforefragment", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestPrependToSection.json b/test/features/personalization/mocks/actions/manifestPrependToSection.json index c2ccb7af8b..cd5f43432b 100644 --- a/test/features/personalization/mocks/actions/manifestPrependToSection.json +++ b/test/features/personalization/mocks/actions/manifestPrependToSection.json @@ -8,8 +8,7 @@ "selector": "section2", "page filter (optional)": "", "param-newoffer=123": "", - "chrome": "/test/features/personalization/mocks/fragments/prependToSection", - "target-var1": "/test/features/personalization/mocks/fragments/prependToSection" + "chrome": "/test/features/personalization/mocks/fragments/prependToSection" } ], ":type": "sheet" diff --git a/test/features/personalization/mocks/actions/manifestRemove.json b/test/features/personalization/mocks/actions/manifestRemove.json index 030c9efb0a..8ff94af803 100644 --- a/test/features/personalization/mocks/actions/manifestRemove.json +++ b/test/features/personalization/mocks/actions/manifestRemove.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "yes", - "target-var1": "yes", "firefox": "", "android": "", "ios": "" @@ -20,7 +19,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "yes", - "target-var1": "yes", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestReplace.json b/test/features/personalization/mocks/actions/manifestReplace.json index c1d0d78d78..f1feae60ca 100644 --- a/test/features/personalization/mocks/actions/manifestReplace.json +++ b/test/features/personalization/mocks/actions/manifestReplace.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "", - "target-var1": "", "firefox": "/test/features/personalization/mocks/fragments/milo-replace-content-firefox-accordion", "android": "", "ios": "" @@ -20,7 +19,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2", - "target-var1": "/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2", "firefox": "", "android": "", "ios": "" @@ -31,7 +29,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/fragmentReplaced", - "target-var1": "/test/features/personalization/mocks/fragments/fragmentReplaced", "firefox": "", "android": "", "ios": "" @@ -42,7 +39,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/fragments/inlineFragReplaced", - "target-var1": "/test/features/personalization/mocks/fragments/inlineFragReplaced", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestTargetReplace.json b/test/features/personalization/mocks/actions/manifestTargetReplace.json deleted file mode 100644 index f1feae60ca..0000000000 --- a/test/features/personalization/mocks/actions/manifestTargetReplace.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "total": 5, - "offset": 0, - "limit": 5, - "data": [ - { - "action": "replace", - "selector": ".how-to", - "page filter (optional)": "", - "param-newoffer=123": "", - "chrome": "", - "firefox": "/test/features/personalization/mocks/fragments/milo-replace-content-firefox-accordion", - "android": "", - "ios": "" - }, - { - "action": "replace", - "selector": "#features-of-milo-experimentation-platform", - "page filter (optional)": "", - "param-newoffer=123": "", - "chrome": "/test/features/personalization/mocks/fragments/milo-replace-content-chrome-howto-h2", - "firefox": "", - "android": "", - "ios": "" - }, - { - "action": "replace", - "selector": "/fragments/replaceme", - "page filter (optional)": "", - "param-newoffer=123": "", - "chrome": "/test/features/personalization/mocks/fragments/fragmentReplaced", - "firefox": "", - "android": "", - "ios": "" - }, - { - "action": "replace", - "selector": "/fragments/inline-replaceme", - "page filter (optional)": "", - "param-newoffer=123": "", - "chrome": "/test/features/personalization/mocks/fragments/inlineFragReplaced", - "firefox": "", - "android": "", - "ios": "" - } - ], - ":type": "sheet" -} diff --git a/test/features/personalization/mocks/actions/manifestUseBlockCode.json b/test/features/personalization/mocks/actions/manifestUseBlockCode.json index e8837cb45f..1d1d47764c 100644 --- a/test/features/personalization/mocks/actions/manifestUseBlockCode.json +++ b/test/features/personalization/mocks/actions/manifestUseBlockCode.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "test\\features\\personalization\\mocks\\promo", - "target-var1": "test\\features\\personalization\\mocks\\promo", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/actions/manifestUseBlockCode2.json b/test/features/personalization/mocks/actions/manifestUseBlockCode2.json index a2a48fdd0a..f377c358d9 100644 --- a/test/features/personalization/mocks/actions/manifestUseBlockCode2.json +++ b/test/features/personalization/mocks/actions/manifestUseBlockCode2.json @@ -9,7 +9,6 @@ "page filter (optional)": "", "param-newoffer=123": "", "chrome": "/test/features/personalization/mocks/myblock", - "target-var1": "/test/features/personalization/mocks/myblock", "firefox": "", "android": "", "ios": "" diff --git a/test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html b/test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html deleted file mode 100644 index a9a5c882f2..0000000000 --- a/test/features/personalization/mocks/fragments/milo-replace-content-target-howto-h2.plain.html +++ /dev/null @@ -1 +0,0 @@ -mock fragment diff --git a/test/features/personalization/pageFilter.test.js b/test/features/personalization/pageFilter.test.js index 0a08e45481..263e11cbba 100644 --- a/test/features/personalization/pageFilter.test.js +++ b/test/features/personalization/pageFilter.test.js @@ -2,8 +2,7 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig } from '../../../libs/utils/utils.js'; -import { init } from '../../../libs/features/personalization/personalization.js'; -import mepSettings from './mepSettings.js'; +import { applyPers } from '../../../libs/features/personalization/personalization.js'; document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -36,7 +35,7 @@ it('pageFilter should exclude page if it is not a match', async () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('.newpage')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); // Nothing should be changed since the pageFilter excludes this page expect(document.querySelector('.marquee')).to.not.be.null; @@ -72,7 +71,7 @@ it('pageFilter should include page if it is a match', async () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('.newpage')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector('.marquee')).to.be.null; expect(document.querySelector('.newpage')).to.not.be.null; diff --git a/test/features/personalization/personalization.test.js b/test/features/personalization/personalization.test.js index d2554b7734..38211b358c 100644 --- a/test/features/personalization/personalization.test.js +++ b/test/features/personalization/personalization.test.js @@ -2,12 +2,8 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import { assert, stub } from 'sinon'; import { getConfig, setConfig } from '../../../libs/utils/utils.js'; -import { - handleFragmentCommand, applyPers, - init, matchGlob, createFrag, combineMepSources, -} from '../../../libs/features/personalization/personalization.js'; +import { applyPers, matchGlob, createFrag } from '../../../libs/features/personalization/personalization.js'; import spoofParams from './spoofParams.js'; -import mepSettings from './mepSettings.js'; document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -46,7 +42,7 @@ describe('Functional Test', () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter2"]')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter2"]'); expect(fragment).to.not.be.null; expect(fragment.parentElement.previousElementSibling.className).to.equal('marquee'); @@ -62,7 +58,7 @@ describe('Functional Test', () => { const secondMarquee = document.getElementsByClassName('marquee')[1]; expect(secondMarquee).to.not.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const fragment = document.querySelector('a[href="/fragments/replace/marquee/r2c1"]'); expect(fragment).to.not.be.null; @@ -80,7 +76,7 @@ describe('Functional Test', () => { expect(document.querySelector('.custom-block-2')).to.not.be.null; expect(document.querySelector('.custom-block-3')).to.not.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector('.special-block')).to.be.null; expect(document.querySelector('.custom-block-2')).to.be.null; @@ -88,27 +84,12 @@ describe('Functional Test', () => { }); it('scheduled manifest should apply changes if active (bts)', async () => { - const config = getConfig(); - config.mep = { - handleFragmentCommand, - preview: false, - variantOverride: {}, - highlight: false, - targetEnabled: false, - experiments: [], - }; - const promoMepSettings = [ - { - manifestPath: '/promos/bts/manifest.json', - disabled: false, - event: { name: 'bts', start: new Date('2023-11-24T13:00:00+00:00'), end: new Date('2222-11-24T13:00:00+00:00') }, - }, - ]; let manifestJson = await readFile({ path: './mocks/manifestScheduledActive.json' }); manifestJson = JSON.parse(manifestJson); setFetchResponse(manifestJson); expect(document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter3"]')).to.be.null; - await applyPers(promoMepSettings); + const event = { name: 'bts', start: new Date('2023-11-24T13:00:00+00:00'), end: new Date('2222-11-24T13:00:00+00:00') }; + await applyPers([{ manifestPath: '/promos/bts/manifest.json', disabled: false, event }]); const fragment = document.querySelector('a[href="/test/features/personalization/mocks/fragments/insertafter3"]'); expect(fragment).to.not.be.null; @@ -117,25 +98,10 @@ describe('Functional Test', () => { }); it('scheduled manifest should not apply changes if not active (blackfriday)', async () => { - const config = getConfig(); - config.mep = { - handleFragmentCommand, - preview: false, - variantOverride: {}, - highlight: false, - targetEnabled: false, - experiments: [], - }; - const promoMepSettings = [ - { - manifestPath: '/promos/blackfriday/manifest.json', - disabled: true, - event: { name: 'blackfriday', start: new Date('2022-11-24T13:00:00+00:00'), end: new Date('2022-11-24T13:00:00+00:00') }, - }, - ]; await loadManifestAndSetResponse('./mocks/manifestScheduledInactive.json'); expect(document.querySelector('a[href="/fragments/insertafter4"]')).to.be.null; - await applyPers(promoMepSettings); + const event = { name: 'blackfriday', start: new Date('2022-11-24T13:00:00+00:00'), end: new Date('2022-11-24T13:00:00+00:00') }; + await applyPers([{ manifestPath: '/promos/blackfriday/manifest.json', disabled: true, event }]); const fragment = document.querySelector('a[href="/fragments/insertafter4"]'); expect(fragment).to.be.null; @@ -146,20 +112,20 @@ describe('Functional Test', () => { config.mep = {}; await loadManifestAndSetResponse('./mocks/manifestTestOrPromo.json'); config = getConfig(); - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(config.mep?.martech).to.be.undefined; }); it('should choose chrome & logged out', async () => { await loadManifestAndSetResponse('./mocks/manifestWithAmpersand.json'); - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const config = getConfig(); expect(config.mep?.martech).to.equal('|chrome & logged|ampersand'); }); it('should choose not firefox', async () => { await loadManifestAndSetResponse('./mocks/manifestWithNot.json'); - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); const config = getConfig(); expect(config.mep?.martech).to.equal('|not firefox|not'); }); @@ -171,7 +137,7 @@ describe('Functional Test', () => { config.entitlements = () => Promise.resolve(['indesign-any', 'fireflies', 'after-effects-any']); await loadManifestAndSetResponse('./mocks/manifestUseEntitlements.json'); - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(getConfig().mep?.martech).to.equal('|fireflies|manifest'); }); @@ -180,7 +146,7 @@ describe('Functional Test', () => { await loadManifestAndSetResponse('./mocks/manifestInvalidSelector.json'); - await init(mepSettings); + await applyPers([{ manifestPath: '/mocks/manifestRemove.json' }]); assert.calledWith(window.console.log, 'Invalid selector: ', '.bad...selector'); window.console.log.reset(); @@ -198,7 +164,7 @@ describe('Functional Test', () => { expect(document.querySelector('meta[property="og:title"]').content).to.equal('milo'); expect(document.querySelector('meta[property="og:image"]')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(geoMetadata.content).to.equal('on'); expect(document.querySelector('meta[name="mynewmetadata"]').content).to.equal('woot'); @@ -211,7 +177,7 @@ describe('Functional Test', () => { const config = getConfig(); await loadManifestAndSetResponse('./mocks/actions/manifestAppendToSection.json'); setTimeout(async () => { - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(config.mep.experiments[0].selectedVariantName).to.equal('param-newoffer=123'); }, 100); }); @@ -269,36 +235,3 @@ describe('matchGlob function', () => { expect(wrapper.classList.contains('hide-block')).to.be.true; }); }); - -describe('MEP Utils', () => { - describe('combineMepSources', async () => { - it('yields an empty list when everything is undefined', async () => { - const manifests = await combineMepSources(undefined, undefined, undefined); - expect(manifests.length).to.equal(0); - }); - it('combines promos and personalization', async () => { - document.head.innerHTML = await readFile({ path: '../../utils/mocks/mep/head-promo.html' }); - const promos = { manifestnames: 'pre-black-friday-global,black-friday-global' }; - const manifests = await combineMepSources('/pers/manifest.json', promos, undefined); - expect(manifests.length).to.equal(3); - expect(manifests[0].manifestPath).to.equal('/pers/manifest.json'); - expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json'); - expect(manifests[2].manifestPath).to.equal('/black-friday.json'); - }); - it('combines promos and personalization and mep param', async () => { - document.head.innerHTML = await readFile({ path: '../../utils/mocks/mep/head-promo.html' }); - const promos = { manifestnames: 'pre-black-friday-global,black-friday-global' }; - const manifests = await combineMepSources( - '/pers/manifest.json', - promos, - '/pers/manifest.json--var1---/mep-param/manifest1.json--all---/mep-param/manifest2.json--all', - ); - expect(manifests.length).to.equal(5); - expect(manifests[0].manifestPath).to.equal('/pers/manifest.json'); - expect(manifests[1].manifestPath).to.equal('/pre-black-friday.json'); - expect(manifests[2].manifestPath).to.equal('/black-friday.json'); - expect(manifests[3].manifestPath).to.equal('/mep-param/manifest1.json'); - expect(manifests[4].manifestPath).to.equal('/mep-param/manifest2.json'); - }); - }); -}); diff --git a/test/features/personalization/replacePage.test.js b/test/features/personalization/replacePage.test.js index 3ea70387f5..3b3fed3038 100644 --- a/test/features/personalization/replacePage.test.js +++ b/test/features/personalization/replacePage.test.js @@ -2,8 +2,7 @@ import { expect } from '@esm-bundle/chai'; import { readFile } from '@web/test-runner-commands'; import { stub } from 'sinon'; import { getConfig } from '../../../libs/utils/utils.js'; -import { init } from '../../../libs/features/personalization/personalization.js'; -import mepSettings from './mepSettings.js'; +import { applyPers } from '../../../libs/features/personalization/personalization.js'; document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); @@ -36,7 +35,7 @@ it('replacePage should replace all of the main block', async () => { expect(document.querySelector('.marquee')).to.not.be.null; expect(document.querySelector('.newpage')).to.be.null; - await init(mepSettings); + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); expect(document.querySelector('.marquee')).to.be.null; expect(document.querySelector('.newpage')).to.not.be.null; diff --git a/test/utils/utils-mep.test.js b/test/utils/utils-mep.test.js index fdbdd7a2e7..53af9d1d12 100644 --- a/test/utils/utils-mep.test.js +++ b/test/utils/utils-mep.test.js @@ -1,7 +1,6 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -import { getMepEnablement } from '../../libs/utils/utils.js'; -import { combineMepSources } from '../../libs/features/personalization/personalization.js'; +import { combineMepSources, getMepEnablement } from '../../libs/utils/utils.js'; describe('MEP Utils', () => { describe('combineMepSources', async () => { diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js index fbdfd5e184..e75e67afd4 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -631,10 +631,10 @@ describe('Utils', () => { document.head.innerHTML = await readFile({ path: './mocks/head-personalization.html' }); await utils.loadArea(); const resultConfig = utils.getConfig(); - const resultExperiment = resultConfig.mep.experiments[0]; + const resultExperiment = resultConfig.mep.experiments[2]; expect(resultConfig.mep.preview).to.be.true; expect(resultConfig.mep.experiments.length).to.equal(3); - expect(resultExperiment.manifest).to.equal('https://main--milo--adobecom.hlx.page/products/special-offers-manifest.json'); + expect(resultExperiment.manifest).to.equal('/products/special-offers-manifest.json'); }); });