From c38748c6c32f2c9bf544e86568dc2528cb78bc5e Mon Sep 17 00:00:00 2001 From: Chris Peyer Date: Wed, 9 Aug 2023 12:38:14 -0700 Subject: [PATCH] Personalization Tests (#1095) --- libs/blocks/fragment/fragment.js | 8 +- .../personalization/personalization.js | 76 ++------- libs/utils/utils.js | 19 ++- .../mocks/fragmentReplaced.plain.html | 3 + .../mocks/manifestInsertContentAfter.json | 18 ++ .../mocks/manifestInsertContentBefore.json | 18 ++ .../mocks/manifestPageFilterExclude.json | 15 ++ .../mocks/manifestPageFilterInclude.json | 15 ++ .../personalization/mocks/manifestRemove.json | 18 ++ .../mocks/manifestReplace.json | 28 +++ .../mocks/manifestReplaceFragment.json | 18 ++ .../mocks/manifestReplacePage.json | 15 ++ .../mocks/manifestUpdateMetadata.json | 36 ++++ .../mocks/manifestUseBlockCode.json | 18 ++ .../mocks/manifestUseBlockCode2.json | 18 ++ .../personalization/mocks/metadata.html | 3 + .../personalization/mocks/myblock/myblock.css | 0 .../personalization/mocks/myblock/myblock.js | 3 + .../mocks/newpromo/newpromo.css | 0 .../mocks/newpromo/newpromo.js | 3 + .../mocks/personalization.html | 113 +++++++++++++ .../mocks/replacePage.plain.html | 1 + .../personalization/pageFilter.test.js | 71 ++++++++ .../personalization/personalization.test.js | 160 +++++++++++++++--- .../personalization/replacePage.test.js | 38 +++++ 25 files changed, 618 insertions(+), 97 deletions(-) create mode 100644 test/features/personalization/mocks/fragmentReplaced.plain.html create mode 100644 test/features/personalization/mocks/manifestInsertContentAfter.json create mode 100644 test/features/personalization/mocks/manifestInsertContentBefore.json create mode 100644 test/features/personalization/mocks/manifestPageFilterExclude.json create mode 100644 test/features/personalization/mocks/manifestPageFilterInclude.json create mode 100644 test/features/personalization/mocks/manifestRemove.json create mode 100644 test/features/personalization/mocks/manifestReplace.json create mode 100644 test/features/personalization/mocks/manifestReplaceFragment.json create mode 100644 test/features/personalization/mocks/manifestReplacePage.json create mode 100644 test/features/personalization/mocks/manifestUpdateMetadata.json create mode 100644 test/features/personalization/mocks/manifestUseBlockCode.json create mode 100644 test/features/personalization/mocks/manifestUseBlockCode2.json create mode 100644 test/features/personalization/mocks/metadata.html create mode 100644 test/features/personalization/mocks/myblock/myblock.css create mode 100644 test/features/personalization/mocks/myblock/myblock.js create mode 100644 test/features/personalization/mocks/newpromo/newpromo.css create mode 100644 test/features/personalization/mocks/newpromo/newpromo.js create mode 100644 test/features/personalization/mocks/personalization.html create mode 100644 test/features/personalization/mocks/replacePage.plain.html create mode 100644 test/features/personalization/pageFilter.test.js create mode 100644 test/features/personalization/replacePage.test.js diff --git a/libs/blocks/fragment/fragment.js b/libs/blocks/fragment/fragment.js index dc7e01c011..d08c2bb3d8 100644 --- a/libs/blocks/fragment/fragment.js +++ b/libs/blocks/fragment/fragment.js @@ -5,8 +5,6 @@ const fragMap = {}; const removeHash = (url) => (url?.endsWith('#_dnt') ? url : url?.split('#')[0]); -// TODO: Can we just use a simple list of loaded fragments? - const isCircularRef = (href) => [...Object.values(fragMap)] .some((tree) => { const node = tree.find(href); @@ -44,11 +42,7 @@ export default async function init(a) { const resp = await fetch(`${a.href}.plain.html`); if (resp.ok) { const html = await resp.text(); - let doc = (new DOMParser()).parseFromString(html, 'text/html'); - if (doc.querySelector('.fragment-personalization')) { - const { fragmentPersonalization } = await import('../../features/personalization/personalization.js'); - doc = await fragmentPersonalization(doc); - } + const doc = (new DOMParser()).parseFromString(html, 'text/html'); const sections = doc.querySelectorAll('body > div'); if (sections.length > 0) { const fragment = createTag('div', { class: 'fragment', 'data-path': relHref }); diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 6b39247141..01008a3685 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -5,6 +5,7 @@ const CLASS_EL_DELETE = 'p13n-deleted'; const CLASS_EL_REPLACE = 'p13n-replaced'; const PAGE_URL = new URL(window.location.href); +/* c8 ignore start */ export const PERSONALIZATION_TAGS = { chrome: () => navigator.userAgent.includes('Chrome') && !navigator.userAgent.includes('Mobile'), firefox: () => navigator.userAgent.includes('Firefox') && !navigator.userAgent.includes('Mobile'), @@ -15,6 +16,7 @@ export const PERSONALIZATION_TAGS = { darkmode: () => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches, lightmode: () => !PERSONALIZATION_TAGS.darkmode(), }; +/* c8 ignore stop */ // Replace any non-alpha chars except comma, space and hyphen const RE_KEY_REPLACE = /[^a-z0-9\- ,=]/g; @@ -73,6 +75,7 @@ const fetchData = async (url, type = DATA_TYPE.JSON) => { try { const resp = await fetch(url); if (!resp.ok) { + /* c8 ignore next 5 */ if (resp.status === 404) { throw new Error('File not found'); } @@ -80,6 +83,7 @@ const fetchData = async (url, type = DATA_TYPE.JSON) => { } return await resp[type](); } catch (e) { + /* c8 ignore next 2 */ console.log(`Error loading content: ${url}`, e.message || e); } return null; @@ -92,6 +96,7 @@ const consolidateObjects = (arr, prop) => arr.reduce((propMap, item) => { return propMap; }, {}); +/* c8 ignore start */ function normalizePath(p) { let path = p; @@ -112,6 +117,7 @@ function normalizePath(p) { } return path; } +/* c8 ignore stop */ const matchGlob = (searchStr, inputStr) => { const pattern = searchStr.replace(/\*\*/g, '.*'); @@ -179,6 +185,7 @@ function handleCommands(commands, manifestId, rootEl = document) { COMMANDS[cmd.action](selectorEl, cmd.target, manifestId); } else { + /* c8 ignore next */ console.log('Invalid command found: ', cmd); } }); @@ -205,12 +212,13 @@ const getVariantInfo = (line, variantNames, variants) => { variants[vn][action] = variants[vn][action] || []; variants[vn][action].push({ - selector: action === 'useblockcode' ? line[vn]?.split('/').pop() : normalizePath(selector), + selector: normalizePath(selector), val: normalizePath(line[vn]), }); } else if (VALID_COMMANDS.includes(action)) { variants[vn].commands.push(variantInfo); } else { + /* c8 ignore next 2 */ console.log('Invalid action found: ', line); } }); @@ -237,6 +245,7 @@ export function parseConfig(data) { config.variantNames = variantNames; return config; } catch (e) { + /* c8 ignore next 3 */ console.log('error parsing personalization config:', e, experiences); } return null; @@ -305,6 +314,7 @@ export async function getPersConfig(name, variantLabel, manifestData, manifestPa const config = parseConfig(persData); if (!config) { + /* c8 ignore next 3 */ console.log('Error loading personalization config: ', name || manifestPath); return null; } @@ -335,75 +345,11 @@ export async function getPersConfig(name, variantLabel, manifestData, manifestPa return config; } -const getFPInfo = (fpTableRows) => { - const names = []; - const info = [...fpTableRows].reduce((infoObj, row) => { - const [actionEl, selectorEl, variantEl, fragment] = row.children; - if (actionEl?.innerText?.toLowerCase() === 'action') { - return infoObj; - } - const variantName = variantEl?.innerText?.toLowerCase(); - if (!names.includes(variantName)) { - names.push(variantName); - infoObj[variantName] = []; - } - infoObj[variantName].push({ - action: actionEl.innerText?.toLowerCase(), - selector: selectorEl.innerText?.toLowerCase(), - htmlFragment: fragment.firstElementChild, - }); - return infoObj; - }, {}); - return { info, names }; -}; - -const modifyFragment = (selectedEl, action, htmlFragment, manifestId) => { - htmlFragment.dataset.manifestId = manifestId; - switch (action) { - case 'replace': case 'replacecontent': - selectedEl.replaceWith(htmlFragment); - break; - case 'insertbefore': case 'insertcontentbefore': - selectedEl.insertAdjacentElement('beforebegin', htmlFragment); - break; - case 'insertafter': case 'insertcontentafter': - selectedEl.insertAdjacentElement('afterend', htmlFragment); - break; - case 'remove': case 'removecontent': - selectedEl.insertAdjacentElement('beforebegin', createTag('div', { 'data-remove-manifest-id': manifestId })); - selectedEl.remove(); - break; - default: - console.warn(`Unknown action: ${action}`); - } -}; - const deleteMarkedEls = () => { [...document.querySelectorAll(`.${CLASS_EL_DELETE}`)] .forEach((el) => el.remove()); }; -export async function fragmentPersonalization(el) { - const fpTable = el.querySelector('div.fragment-personalization'); - if (!fpTable) return el; - const fpTableRows = fpTable.querySelectorAll(':scope > div'); - - const { info, names } = getFPInfo(fpTableRows); - fpTable.remove(); - - const manifestId = 'fragment-personalization'; - const selectedVariant = getPersonalizationVariant(manifestId, names); - if (!selectedVariant) return el; - - info[selectedVariant].forEach((cmd) => { - const selectedEl = el.querySelector(cmd.selector); - if (!selectedEl) return; - modifyFragment(selectedEl, cmd.action, cmd.htmlFragment, manifestId); - }); - - return el; -} - const normalizeFragPaths = ({ selector, val }) => ({ selector: normalizePath(selector), val: normalizePath(val), diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 678b77e2bc..3fb98f5aaf 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -375,12 +375,27 @@ export async function loadTemplate() { await Promise.all([styleLoaded, scriptLoaded]); } +function checkForExpBlock(name, expBlocks) { + const expBlock = expBlocks?.[name]; + if (!expBlock) return null; + + const blockName = expBlock.split('/').pop(); + return { blockPath: expBlock, blockName }; +} + export async function loadBlock(block) { - const name = block.classList[0]; + let name = block.classList[0]; const { miloLibs, codeRoot, expBlocks } = getConfig(); const base = miloLibs && MILO_BLOCKS.includes(name) ? miloLibs : codeRoot; - const path = expBlocks?.[name] ? `${expBlocks[name]}` : `${base}/blocks/${name}`; + let path = `${base}/blocks/${name}`; + + const expBlock = checkForExpBlock(name, expBlocks); + if (expBlock) { + name = expBlock.blockName; + path = expBlock.blockPath; + } + const blockPath = `${path}/${name}`; const styleLoaded = new Promise((resolve) => { diff --git a/test/features/personalization/mocks/fragmentReplaced.plain.html b/test/features/personalization/mocks/fragmentReplaced.plain.html new file mode 100644 index 0000000000..b7eaff117d --- /dev/null +++ b/test/features/personalization/mocks/fragmentReplaced.plain.html @@ -0,0 +1,3 @@ +
+

The fragment has been replaced

+
diff --git a/test/features/personalization/mocks/manifestInsertContentAfter.json b/test/features/personalization/mocks/manifestInsertContentAfter.json new file mode 100644 index 0000000000..9c5b58f53b --- /dev/null +++ b/test/features/personalization/mocks/manifestInsertContentAfter.json @@ -0,0 +1,18 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "insertContentAfter", + "selector": ".marquee", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/fragments/insertafter", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestInsertContentBefore.json b/test/features/personalization/mocks/manifestInsertContentBefore.json new file mode 100644 index 0000000000..b55188763c --- /dev/null +++ b/test/features/personalization/mocks/manifestInsertContentBefore.json @@ -0,0 +1,18 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "insertContentBefore", + "selector": ".marquee", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/fragments/insertbefore", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestPageFilterExclude.json b/test/features/personalization/mocks/manifestPageFilterExclude.json new file mode 100644 index 0000000000..6e0fb867b1 --- /dev/null +++ b/test/features/personalization/mocks/manifestPageFilterExclude.json @@ -0,0 +1,15 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "replacePage", + "selector": "", + "page filter (optional)": "/no/match/**", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/replacePage" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestPageFilterInclude.json b/test/features/personalization/mocks/manifestPageFilterInclude.json new file mode 100644 index 0000000000..9c1c3ea0d1 --- /dev/null +++ b/test/features/personalization/mocks/manifestPageFilterInclude.json @@ -0,0 +1,15 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "replacePage", + "selector": "", + "page filter (optional)": "/**", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/replacePage" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestRemove.json b/test/features/personalization/mocks/manifestRemove.json new file mode 100644 index 0000000000..323f192b0e --- /dev/null +++ b/test/features/personalization/mocks/manifestRemove.json @@ -0,0 +1,18 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "removeContent", + "selector": ".z-pattern", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "yes", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestReplace.json b/test/features/personalization/mocks/manifestReplace.json new file mode 100644 index 0000000000..d26eab08fe --- /dev/null +++ b/test/features/personalization/mocks/manifestReplace.json @@ -0,0 +1,28 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "replaceContent", + "selector": ".how-to", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "firefox": "/drafts/rwoblesk/personalization-testing/fragments/milo-replace-content-firefox-accordion", + "android": "", + "ios": "" + }, + { + "action": "replaceContent", + "selector": "#features-of-milo-experimentation-platform", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/fragments/milo-replace-content-chrome-howto-h2", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestReplaceFragment.json b/test/features/personalization/mocks/manifestReplaceFragment.json new file mode 100644 index 0000000000..7f1cda425b --- /dev/null +++ b/test/features/personalization/mocks/manifestReplaceFragment.json @@ -0,0 +1,18 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "replaceFragment", + "selector": "/fragments/replaceme", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/fragments/fragmentreplaced", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestReplacePage.json b/test/features/personalization/mocks/manifestReplacePage.json new file mode 100644 index 0000000000..7361cf7569 --- /dev/null +++ b/test/features/personalization/mocks/manifestReplacePage.json @@ -0,0 +1,15 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "replacePage", + "selector": "", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/replacePage" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestUpdateMetadata.json b/test/features/personalization/mocks/manifestUpdateMetadata.json new file mode 100644 index 0000000000..cce9f7c1fa --- /dev/null +++ b/test/features/personalization/mocks/manifestUpdateMetadata.json @@ -0,0 +1,36 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "updateMetadata", + "selector": "georouting", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "on" + }, + { + "action": "updateMetadata", + "selector": "mynewmetadata", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "woot" + }, + { + "action": "updateMetadata", + "selector": "og:title", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "New Title" + }, + { + "action": "updateMetadata", + "selector": "og:image", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "https://adobe.com/path/to/image.jpg" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestUseBlockCode.json b/test/features/personalization/mocks/manifestUseBlockCode.json new file mode 100644 index 0000000000..b308823bae --- /dev/null +++ b/test/features/personalization/mocks/manifestUseBlockCode.json @@ -0,0 +1,18 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "useBlockCode", + "selector": "promo", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/newpromo", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/manifestUseBlockCode2.json b/test/features/personalization/mocks/manifestUseBlockCode2.json new file mode 100644 index 0000000000..edccc2451a --- /dev/null +++ b/test/features/personalization/mocks/manifestUseBlockCode2.json @@ -0,0 +1,18 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "useBlockCode", + "selector": "myblock", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "/test/features/personalization/mocks/myblock", + "firefox": "", + "android": "", + "ios": "" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/mocks/metadata.html b/test/features/personalization/mocks/metadata.html new file mode 100644 index 0000000000..8d87356ed6 --- /dev/null +++ b/test/features/personalization/mocks/metadata.html @@ -0,0 +1,3 @@ + + + diff --git a/test/features/personalization/mocks/myblock/myblock.css b/test/features/personalization/mocks/myblock/myblock.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/features/personalization/mocks/myblock/myblock.js b/test/features/personalization/mocks/myblock/myblock.js new file mode 100644 index 0000000000..687dbe25c5 --- /dev/null +++ b/test/features/personalization/mocks/myblock/myblock.js @@ -0,0 +1,3 @@ +export default function init(el) { + el.innerHTML = '
My New Block!
'; +} diff --git a/test/features/personalization/mocks/newpromo/newpromo.css b/test/features/personalization/mocks/newpromo/newpromo.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/features/personalization/mocks/newpromo/newpromo.js b/test/features/personalization/mocks/newpromo/newpromo.js new file mode 100644 index 0000000000..55b0be5ce7 --- /dev/null +++ b/test/features/personalization/mocks/newpromo/newpromo.js @@ -0,0 +1,3 @@ +export default function init(el) { + el.innerHTML = '
New Promo!
'; +} diff --git a/test/features/personalization/mocks/personalization.html b/test/features/personalization/mocks/personalization.html new file mode 100644 index 0000000000..ef2bcf65a9 --- /dev/null +++ b/test/features/personalization/mocks/personalization.html @@ -0,0 +1,113 @@ + +
+
+
+
+
+
+ + + +
+
+
+
+

Milo Experimentation Platform

+

Leverage the Milo Experimentation Platform (MEP) for all your personalization needs on Milo!

+

Review Docs

+
+
+ + + +
+
+
+
+
+
+
+
+

Features of Milo Experimentation Platform

+

Learn more about the features of the Milo Experimentation Platform and what it can do

+
+
+
+
+ + + +
+
+

Who will win?

+

A/B/N Testing

+

Milo Experimentation Platform is integrated with Adobe Target and can help you test new experiences.

+

Learn more

+
+
+
+
+ + + +
+
+

Speak directly to your audience

+

Audience Experience Targeting

+

Leveraging Adobe Target and Adobe Audience Manager, Milo Experimentation Platform can help serve specific experiences to specific visitors.

+

Learn more

+
+
+
+
+ + + +
+
+

Personalize based on visitor attributes

+

Attribute Experience Targeting

+

For simple use cases, the Milo Experimentation Platform can hide/show/add content to a page based on the attributes of a visitor.

+

Learn more Learn more

+
+
+
+
+
+
+
+
+

How to leverage Milo Experimentation Platform

+

This will explain the basic steps on how to use the Milo Experimentation Platform.

+

+ + + +

+
+
+
+
+
    +
  • Create a webpage using Milo
  • +
  • Create a page manifest
  • +
  • Configure the personalization based on your requirements
  • +
  • Sit back and watch visitors enjoy your personalization!
  • +
+
+
+
+
+
+

/fragments/replaceme

+
+
+
+
+
Old Promo Block
+
+
+
This block does not exist
+
+
+ diff --git a/test/features/personalization/mocks/replacePage.plain.html b/test/features/personalization/mocks/replacePage.plain.html new file mode 100644 index 0000000000..a09980c128 --- /dev/null +++ b/test/features/personalization/mocks/replacePage.plain.html @@ -0,0 +1 @@ +
This is the new page
diff --git a/test/features/personalization/pageFilter.test.js b/test/features/personalization/pageFilter.test.js new file mode 100644 index 0000000000..941b058d19 --- /dev/null +++ b/test/features/personalization/pageFilter.test.js @@ -0,0 +1,71 @@ +import { expect } from '@esm-bundle/chai'; +import { readFile } from '@web/test-runner-commands'; +import { stub } from 'sinon'; +import { applyPers } from '../../../libs/features/personalization/personalization.js'; + +document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); + +it('pageFilter should exclude page if it is not a match', async () => { + let manifestJson = await readFile({ path: './mocks/manifestPageFilterExclude.json' }); + manifestJson = JSON.parse(manifestJson); + const replacePageHtml = await readFile({ path: './mocks/replacePage.plain.html' }); + + window.fetch = stub(); + window.fetch.onCall(0).returns( + new Promise((resolve) => { + resolve({ + ok: true, + json: () => manifestJson, + }); + }), + ); + window.fetch.onCall(1).returns( + new Promise((resolve) => { + resolve({ + ok: true, + text: () => replacePageHtml, + }); + }), + ); + + expect(document.querySelector('.marquee')).to.not.be.null; + expect(document.querySelector('.newpage')).to.be.null; + + 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; + expect(document.querySelector('.newpage')).to.be.null; +}); + +it('pageFilter should include page if it is a match', async () => { + let manifestJson = await readFile({ path: './mocks/manifestPageFilterInclude.json' }); + manifestJson = JSON.parse(manifestJson); + const replacePageHtml = await readFile({ path: './mocks/replacePage.plain.html' }); + + window.fetch = stub(); + window.fetch.onCall(0).returns( + new Promise((resolve) => { + resolve({ + ok: true, + json: () => manifestJson, + }); + }), + ); + window.fetch.onCall(1).returns( + new Promise((resolve) => { + resolve({ + ok: true, + text: () => replacePageHtml, + }); + }), + ); + + expect(document.querySelector('.marquee')).to.not.be.null; + expect(document.querySelector('.newpage')).to.be.null; + + 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 7a3f96163c..78cf20755c 100644 --- a/test/features/personalization/personalization.test.js +++ b/test/features/personalization/personalization.test.js @@ -1,30 +1,144 @@ import { expect } from '@esm-bundle/chai'; -import { createTag, getConfig, updateConfig } from '../../../libs/utils/utils.js'; -import { stubFetch } from '../../helpers/mockFetch.js'; +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 } from '../../../libs/features/personalization/personalization.js'; -// document.body.innerHTML = await readFile({ path: './mocks/head.html' }); +document.head.innerHTML = await readFile({ path: './mocks/metadata.html' }); +document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); +const setFetchResponse = (data, type = 'json') => { + window.fetch = stub().returns( + new Promise((resolve) => { + resolve({ + ok: true, + [type]: () => data, + }); + }), + ); +}; + +// Note that the manifestPath doesn't matter as we stub the fetch describe('Functional Test', () => { - it.skip('replaceContent should replace an element with a fragment', async () => { - const manifestData = [{ test: true }]; - stubFetch(manifestData); - - const loadedlinkParams = {}; - const loadLink = (url, options) => { - loadedlinkParams.url = url; - loadedlinkParams.options = options; - }; - - await applyPers( - // Path doesn't matter as we stub fetch above - ['/path/to/manifest.json'], - { createTag, getConfig, updateConfig, loadLink, loadScript: () => {} }, - ); - - expect(loadedlinkParams).to.deep.equal({ - url: '/path/to/manifest.json', - options: { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' }, - }); + it('replaceContent should replace an element with a fragment', async () => { + let manifestJson = await readFile({ path: './mocks/manifestReplace.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + expect(document.querySelector('#features-of-milo-experimentation-platform')).to.not.be.null; + 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' }]); + expect(document.querySelector('#features-of-milo-experimentation-platform')).to.be.null; + expect(parentEl.firstElementChild.firstElementChild.href) + .to.equal('http://localhost:2000/fragments/milo-replace-content-chrome-howto-h2'); + // .how-to should not be changed as it is targeted to firefox + expect(document.querySelector('.how-to')).to.not.be.null; + }); + + it('removeContent should remove z-pattern content from the page', async () => { + let manifestJson = await readFile({ path: './mocks/manifestRemove.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + expect(document.querySelector('.z-pattern')).to.not.be.null; + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + expect(document.querySelector('.z-pattern')).to.be.null; + }); + + it('insertContentAfter should add fragment after target element', async () => { + let manifestJson = await readFile({ path: './mocks/manifestInsertContentAfter.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + expect(document.querySelector('a[href="/fragments/insertafter"]')).to.be.null; + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + + const fragment = document.querySelector('a[href="/fragments/insertafter"]'); + expect(fragment).to.not.be.null; + + expect(fragment.parentElement.parentElement.firstElementChild.className).to.equal('marquee'); + }); + + it('insertContentBefore should add fragment before target element', async () => { + let manifestJson = await readFile({ path: './mocks/manifestInsertContentBefore.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + expect(document.querySelector('a[href="/fragments/insertbefore"]')).to.be.null; + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + + const fragment = document.querySelector('a[href="/fragments/insertbefore"]'); + expect(fragment).to.not.be.null; + + expect(fragment.parentElement.parentElement.children[1].className).to.equal('marquee'); + }); + + it('replaceFragment should replace a fragment in the document', async () => { + let manifestJson = await readFile({ path: './mocks/manifestReplaceFragment.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + expect(document.querySelector('a[href="/fragments/replaceme"]')).to.not.be.null; + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + + const fragmentResp = await readFile({ path: './mocks/fragmentReplaced.plain.html' }); + setFetchResponse(fragmentResp, '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="/fragments/fragmentreplaced"]')).to.not.be.null; + }); + + it('useBlockCode should override a current block with the custom block code provided', async () => { + let manifestJson = await readFile({ path: './mocks/manifestUseBlockCode.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + + expect(getConfig().expBlocks).to.deep.equal({ promo: '/test/features/personalization/mocks/newpromo' }); + const promoBlock = document.querySelector('.promo'); + expect(promoBlock.textContent?.trim()).to.equal('Old Promo Block'); + await loadBlock(promoBlock); + expect(promoBlock.textContent?.trim()).to.equal('New Promo!'); + }); + + it('useBlockCode should be able to use a new type of block', async () => { + let manifestJson = await readFile({ path: './mocks/manifestUseBlockCode2.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + + expect(getConfig().expBlocks).to.deep.equal({ myblock: '/test/features/personalization/mocks/myblock' }); + const myBlock = document.querySelector('.myblock'); + expect(myBlock.textContent?.trim()).to.equal('This block does not exist'); + await loadBlock(myBlock); + expect(myBlock.textContent?.trim()).to.equal('My New Block!'); + }); + + it('updateMetadata should be able to add and change metadata', async () => { + let manifestJson = await readFile({ path: './mocks/manifestUpdateMetadata.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + + const geoMetadata = document.querySelector('meta[name="georouting"]'); + expect(geoMetadata.content).to.equal('off'); + + expect(document.querySelector('meta[name="mynewmetadata"]')).to.be.null; + 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' }]); + + expect(geoMetadata.content).to.equal('on'); + expect(document.querySelector('meta[name="mynewmetadata"]').content).to.equal('woot'); + expect(document.querySelector('meta[property="og:title"]').content).to.equal('New Title'); + expect(document.querySelector('meta[property="og:image"]').content).to.equal('https://adobe.com/path/to/image.jpg'); }); }); diff --git a/test/features/personalization/replacePage.test.js b/test/features/personalization/replacePage.test.js new file mode 100644 index 0000000000..951a879189 --- /dev/null +++ b/test/features/personalization/replacePage.test.js @@ -0,0 +1,38 @@ +import { expect } from '@esm-bundle/chai'; +import { readFile } from '@web/test-runner-commands'; +import { stub } from 'sinon'; +import { applyPers } from '../../../libs/features/personalization/personalization.js'; + +document.body.innerHTML = await readFile({ path: './mocks/personalization.html' }); + +it('replacePage should replace all of the main block', async () => { + let manifestJson = await readFile({ path: './mocks/manifestReplacePage.json' }); + manifestJson = JSON.parse(manifestJson); + const replacePageHtml = await readFile({ path: './mocks/replacePage.plain.html' }); + + window.fetch = stub(); + window.fetch.onCall(0).returns( + new Promise((resolve) => { + resolve({ + ok: true, + json: () => manifestJson, + }); + }), + ); + window.fetch.onCall(1).returns( + new Promise((resolve) => { + resolve({ + ok: true, + text: () => replacePageHtml, + }); + }), + ); + + expect(document.querySelector('.marquee')).to.not.be.null; + expect(document.querySelector('.newpage')).to.be.null; + + await applyPers([{ manifestPath: '/path/to/manifest.json' }]); + + expect(document.querySelector('.marquee')).to.be.null; + expect(document.querySelector('.newpage')).to.not.be.null; +});