From f9970159a04abdc407bd1ce99fd038920de24020 Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Wed, 31 Jul 2024 02:59:18 -0600 Subject: [PATCH] MWPW-155079 [MILO][MEP] Manifests not changing domains (#2626) * 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 * normalizePath * add normalizePath back into preload * MWPW-155568 [MILO][MEP] If MEP is completely off, mep param should still show button (#2658) don't return if param is present but empty --------- Co-authored-by: markpadbe Co-authored-by: Mark Perry <124626043+markpadbe@users.noreply.github.com> --- 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 | 309 +++++++++++------- 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 | 94 ++---- 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 | 2 +- 31 files changed, 637 insertions(+), 299 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 dbd6c14f49..0242698af9 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 cce6c71084..38f38c4195 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); }, }; @@ -175,7 +153,7 @@ function checkSelectorType(selector) { const fetchData = async (url, type = DATA_TYPE.JSON) => { try { - const resp = await fetch(url); + const resp = await fetch(normalizePath(url)); if (!resp.ok) { /* c8 ignore next 5 */ if (resp.status === 404) { @@ -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 */ @@ -622,6 +632,23 @@ const createDefaultExperiment = (manifest) => ({ 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, @@ -634,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; @@ -645,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, @@ -705,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( @@ -726,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)); @@ -805,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') { @@ -826,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 normalizedURL = normalizePath(manifest.manifestPath); + loadLink(normalizedURL, { 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..5feb6d18c7 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 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; + const pzn = getMepEnablement('personalization'); + const promo = getMepEnablement('manifestnames', PROMO_PARAM); + const target = getMepEnablement('target'); + if (!(pzn || target || promo || mepParam + || mepHighlight || mepButton || mepParam === '')) 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 586183ec6a..fbdfd5e184 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -634,7 +634,7 @@ describe('Utils', () => { 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'); }); });