From 6b71db6132fa71e7c6b8bf5e94baeb33c6f22a05 Mon Sep 17 00:00:00 2001 From: Ratko Zagorac Date: Wed, 7 Aug 2024 10:36:41 +0200 Subject: [PATCH 1/4] MWPW-155385: Added personalization --- edsdme/scripts/personalization.js | 82 +++++++++++++++++++++++++++++++ edsdme/scripts/scripts.js | 2 + edsdme/scripts/utils.js | 23 +++++++++ 3 files changed, 107 insertions(+) create mode 100644 edsdme/scripts/personalization.js diff --git a/edsdme/scripts/personalization.js b/edsdme/scripts/personalization.js new file mode 100644 index 0000000..d33742e --- /dev/null +++ b/edsdme/scripts/personalization.js @@ -0,0 +1,82 @@ +import { + getCurrentProgramType, + getPartnerDataCookieValue, + isMember, + partnerIsSignedIn, + getPartnerDataCookieObject, + signedInNonMember, + isResseler +} +from "./utils.js"; + +const PAGE_PERSONALIZATION_PLACEHOLDERS = { + firstName: '//*[contains(text(), "$firstName")]', +} + +const LEVEL_CONDITION = 'partner-level'; +const PERSONALIZATION_MARKER = 'partner-personalization'; +const PROGRAM = getCurrentProgramType(); +const PARTNER_LEVEL = getPartnerDataCookieValue(PROGRAM, 'level'); +const COOKIE_OBJECT = getPartnerDataCookieObject(PROGRAM); +const PERSONALIZATION_CONDITIONS = { + 'partner-not-member': signedInNonMember(), + 'partner-not-signed-in': !partnerIsSignedIn(), + 'partner-all-levels': isMember(), + 'partner-reseller': isResseler(PARTNER_LEVEL), + 'partner-level': (level) => PARTNER_LEVEL === level +} + +function getNodesByXPath(query, context = document) { + const nodes = []; + const xpathResult = document.evaluate(query, context); + let current = xpathResult?.iterateNext(); + while(current) { + nodes.push(current); + current = xpathResult.iterateNext(); + } + return nodes; +} + +function personalizePlaceholders(placeholders, context = document) { + Object.entries(placeholders).forEach(([key, value]) => { + const placeholderValue = COOKIE_OBJECT[key]; + getNodesByXPath(value, context).forEach((el) => { + if (!placeholderValue) el.remove(); + el.textContent = el.textContent.replace(`$${key}`, placeholderValue); + }); + }); +} + +function removeElement(element, conditions) { + if(!element || !conditions?.length) return; + const removeElement = conditions.every((condition) => { + const conditionLevel = condition.startsWith(LEVEL_CONDITION) ? condition.split('-').pop() : ''; + return conditionLevel ? !PERSONALIZATION_CONDITIONS[LEVEL_CONDITION](conditionLevel) : !PERSONALIZATION_CONDITIONS[condition]; + }); + if(removeElement) element.remove(); +} + +function personalizePage(page) { + const blocks = Array.from(page.getElementsByClassName(PERSONALIZATION_MARKER)); + const sections = Array.from(page.getElementsByClassName('section-metadata')); + [...blocks, ...sections].forEach((el) => { + let conditions = Object.values(el.classList) + let elementToRemove = el; + if(el.classList.contains('section-metadata')) { + elementToRemove = el.parentElement; + Array.from(el.children).forEach((child) => { + const col1 = child.firstElementChild; + const col2 = child.lastElementChild; + if (col1?.textContent !== 'style' || !col2?.textContent.includes(PERSONALIZATION_MARKER)) return; + conditions = col2?.textContent?.split(',').map((el) => el.trim()); + }); + } + removeElement(elementToRemove, conditions); + }); +} + +export function applyPagePersonalization() { + const main = document.querySelector('main'); + personalizePlaceholders(PAGE_PERSONALIZATION_PLACEHOLDERS, main); + personalizePage(main); +} diff --git a/edsdme/scripts/scripts.js b/edsdme/scripts/scripts.js index 219af40..0fe4dae 100644 --- a/edsdme/scripts/scripts.js +++ b/edsdme/scripts/scripts.js @@ -1,4 +1,5 @@ import { setLibs, redirectLoggedinPartner, updateIMSConfig } from './utils.js'; +import { applyPagePersonalization } from './personalization.js'; // Add project-wide style path here. const STYLES = ''; @@ -72,6 +73,7 @@ const miloLibs = setLibs(LIBS); }()); (async function loadPage() { + applyPagePersonalization(); redirectLoggedinPartner(); updateIMSConfig(); const { loadArea, setConfig } = await import(`${miloLibs}/utils/utils.js`); diff --git a/edsdme/scripts/utils.js b/edsdme/scripts/utils.js index a18e41d..fd15339 100644 --- a/edsdme/scripts/utils.js +++ b/edsdme/scripts/utils.js @@ -13,6 +13,17 @@ /** * The decision engine for where to get Milo's libs from. */ + +export const LEVELS = { + REGISTERED: 'registered', + CERTIFIED: 'certified', + GOLD: 'gold', + PLATINUM: 'platinum', + DISTRIBBUTOR: 'distributor' +} + +export const RESSELER_LEVELS = [LEVELS.REGISTERED, LEVELS.CERTIFIED, LEVELS.GOLD, LEVELS.PLATINUM]; + export const [setLibs, getLibs] = (() => { let libs; return [ @@ -138,6 +149,18 @@ export function isMember() { return status === 'MEMBER'; } +export function partnerIsSignedIn() { + return getCookieValue('partner_data'); +} + +export function signedInNonMember() { + return partnerIsSignedIn() && !isMember(); +} + +export function isResseler(level) { + return RESSELER_LEVELS.includes(level?.toLowerCase()); +} + export function getMetadataContent(name) { return document.querySelector(`meta[name="${name}"]`)?.content; } From a28db8a8719947312f50e695f878710660257ae1 Mon Sep 17 00:00:00 2001 From: Ratko Zagorac Date: Fri, 9 Aug 2024 12:30:16 +0200 Subject: [PATCH 2/4] MWPW-155385: Fix bug --- edsdme/scripts/personalization.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edsdme/scripts/personalization.js b/edsdme/scripts/personalization.js index 42b66da..5d9f9e2 100644 --- a/edsdme/scripts/personalization.js +++ b/edsdme/scripts/personalization.js @@ -75,7 +75,7 @@ function personalizePage(page) { } export function applyPagePersonalization() { - const main = document.querySelector('main'); + const main = document.querySelector('main') ?? document; personalizePlaceholders(PAGE_PERSONALIZATION_PLACEHOLDERS, main); personalizePage(main); } From 3d4bc81790371e2203a7f4cc188b0673878cf806 Mon Sep 17 00:00:00 2001 From: Ratko Zagorac Date: Tue, 13 Aug 2024 07:43:03 +0200 Subject: [PATCH 3/4] MWPW-155385: Changed logic for hiding elements --- edsdme/scripts/personalization.js | 55 +++++++++++++++++++------------ edsdme/scripts/utils.js | 2 +- edsdme/styles/styles.css | 4 +++ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/edsdme/scripts/personalization.js b/edsdme/scripts/personalization.js index 5d9f9e2..fdfcb77 100644 --- a/edsdme/scripts/personalization.js +++ b/edsdme/scripts/personalization.js @@ -5,7 +5,7 @@ import { partnerIsSignedIn, getPartnerDataCookieObject, signedInNonMember, - isResseler, + isReseller , } from './utils.js'; @@ -20,11 +20,11 @@ const PERSONALIZATION_CONDITIONS = { 'partner-not-member': signedInNonMember(), 'partner-not-signed-in': !partnerIsSignedIn(), 'partner-all-levels': isMember(), - 'partner-reseller': isResseler(PARTNER_LEVEL), + 'partner-reseller': isReseller (PARTNER_LEVEL), 'partner-level': (level) => PARTNER_LEVEL === level, }; -function getNodesByXPath(query, context = document) { +export function getNodesByXPath(query, context = document) { const nodes = []; const xpathResult = document.evaluate(query, context); let current = xpathResult?.iterateNext(); @@ -45,32 +45,45 @@ function personalizePlaceholders(placeholders, context = document) { }); } -function removeElement(element, conditions) { - if (!element || !conditions?.length) return; - const remove = conditions.every((condition) => { +function shouldHide(conditions) { + return conditions.every((condition) => { const conditionLevel = condition.startsWith(LEVEL_CONDITION) ? condition.split('-').pop() : ''; return conditionLevel ? !PERSONALIZATION_CONDITIONS[LEVEL_CONDITION](conditionLevel) : !PERSONALIZATION_CONDITIONS[condition]; }); - if (remove) element.remove(); } +function hideElement(element, conditions) { + if (!element || !conditions?.length) return; + shouldHide(conditions) && element.classList.add('personalization-hide'); +} + +function hideSections(page) { + const sections = Array.from(page.getElementsByClassName('section-metadata')); + sections.forEach((section) => { + let hide = false; + Array.from(section.children).forEach((child) => { + const col1 = child.firstElementChild; + let col2 = child.lastElementChild; + if (col1?.textContent !== 'style' || !col2?.textContent.includes(PERSONALIZATION_MARKER)) return; + const conditions = col2?.textContent?.split(',').map((text) => text.trim()); + hide = shouldHide(conditions); + }); + if (!hide) return; + const parent = section.parentElement; + Array.from(parent.children).forEach((el) => { + el.classList.add('personalization-hide'); + }); + }); +} + + function personalizePage(page) { const blocks = Array.from(page.getElementsByClassName(PERSONALIZATION_MARKER)); - const sections = Array.from(page.getElementsByClassName('section-metadata')); - [...blocks, ...sections].forEach((el) => { - let conditions = Object.values(el.classList); - let elementToRemove = el; - if (el.classList.contains('section-metadata')) { - elementToRemove = el.parentElement; - Array.from(el.children).forEach((child) => { - const col1 = child.firstElementChild; - const col2 = child.lastElementChild; - if (col1?.textContent !== 'style' || !col2?.textContent.includes(PERSONALIZATION_MARKER)) return; - conditions = col2?.textContent?.split(',').map((text) => text.trim()); - }); - } - removeElement(elementToRemove, conditions); + hideSections(page); + blocks.forEach((el) => { + const conditions = Object.values(el.classList); + hideElement(el, conditions); }); } diff --git a/edsdme/scripts/utils.js b/edsdme/scripts/utils.js index 37329b7..8a8c1ff 100644 --- a/edsdme/scripts/utils.js +++ b/edsdme/scripts/utils.js @@ -134,7 +134,7 @@ export function signedInNonMember() { return partnerIsSignedIn() && !isMember(); } -export function isResseler(level) { +export function isReseller (level) { return RESSELER_LEVELS.includes(level?.toLowerCase()); } diff --git a/edsdme/styles/styles.css b/edsdme/styles/styles.css index e80440f..3d0ff2f 100644 --- a/edsdme/styles/styles.css +++ b/edsdme/styles/styles.css @@ -36,3 +36,7 @@ font-size: 16px; font-weight: bold; } + +.personalization-hide { + display: none; +} From b8371e6132b8fd1a7e621a93defb282b373c141f Mon Sep 17 00:00:00 2001 From: Ratko Zagorac Date: Tue, 13 Aug 2024 12:39:02 +0200 Subject: [PATCH 4/4] MWPW-155385: Added unit tests for personalization --- edsdme/scripts/personalization.js | 21 ++-- edsdme/scripts/scripts.js | 1 - edsdme/scripts/utils.js | 11 ++ test/scripts/mocks/personalization.html | 114 +++++++++++++++++ test/scripts/personalization.jest.js | 158 ++++++++++++++++++++++++ 5 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 test/scripts/mocks/personalization.html create mode 100644 test/scripts/personalization.jest.js diff --git a/edsdme/scripts/personalization.js b/edsdme/scripts/personalization.js index fdfcb77..3f6669d 100644 --- a/edsdme/scripts/personalization.js +++ b/edsdme/scripts/personalization.js @@ -5,7 +5,8 @@ import { partnerIsSignedIn, getPartnerDataCookieObject, signedInNonMember, - isReseller , + isReseller, + getNodesByXPath } from './utils.js'; @@ -16,6 +17,7 @@ const PERSONALIZATION_MARKER = 'partner-personalization'; const PROGRAM = getCurrentProgramType(); const PARTNER_LEVEL = getPartnerDataCookieValue(PROGRAM, 'level'); const COOKIE_OBJECT = getPartnerDataCookieObject(PROGRAM); + const PERSONALIZATION_CONDITIONS = { 'partner-not-member': signedInNonMember(), 'partner-not-signed-in': !partnerIsSignedIn(), @@ -24,22 +26,15 @@ const PERSONALIZATION_CONDITIONS = { 'partner-level': (level) => PARTNER_LEVEL === level, }; -export function getNodesByXPath(query, context = document) { - const nodes = []; - const xpathResult = document.evaluate(query, context); - let current = xpathResult?.iterateNext(); - while (current) { - nodes.push(current); - current = xpathResult.iterateNext(); - } - return nodes; -} function personalizePlaceholders(placeholders, context = document) { Object.entries(placeholders).forEach(([key, value]) => { const placeholderValue = COOKIE_OBJECT[key]; getNodesByXPath(value, context).forEach((el) => { - if (!placeholderValue) el.remove(); + if (!placeholderValue) { + el.remove(); + return; + } el.textContent = el.textContent.replace(`$${key}`, placeholderValue); }); }); @@ -80,11 +75,11 @@ function hideSections(page) { function personalizePage(page) { const blocks = Array.from(page.getElementsByClassName(PERSONALIZATION_MARKER)); - hideSections(page); blocks.forEach((el) => { const conditions = Object.values(el.classList); hideElement(el, conditions); }); + hideSections(page); } export function applyPagePersonalization() { diff --git a/edsdme/scripts/scripts.js b/edsdme/scripts/scripts.js index 5d3cac1..68a99e9 100644 --- a/edsdme/scripts/scripts.js +++ b/edsdme/scripts/scripts.js @@ -1,4 +1,3 @@ -import { setLibs, redirectLoggedinPartner, updateIMSConfig, preloadResources, getRenewBanner } from './utils.js'; import { applyPagePersonalization } from './personalization.js'; import { setLibs, redirectLoggedinPartner, updateIMSConfig, preloadResources, getRenewBanner, updateNavigation, updateFooter } from './utils.js'; diff --git a/edsdme/scripts/utils.js b/edsdme/scripts/utils.js index eca43e3..270eac0 100644 --- a/edsdme/scripts/utils.js +++ b/edsdme/scripts/utils.js @@ -397,3 +397,14 @@ export function updateFooter(locales) { const footerLoggedIn = getMetadataContent('footer-loggedin-source'); footerMeta.content = footerLoggedIn ?? `${prefix}/edsdme/partners-shared/loggedin-footer`; } + +export function getNodesByXPath(query, context = document) { + const nodes = []; + const xpathResult = document.evaluate(query, context); + let current = xpathResult?.iterateNext(); + while (current) { + nodes.push(current); + current = xpathResult.iterateNext(); + } + return nodes; +} diff --git a/test/scripts/mocks/personalization.html b/test/scripts/mocks/personalization.html new file mode 100644 index 0000000..a1be0d6 --- /dev/null +++ b/test/scripts/mocks/personalization.html @@ -0,0 +1,114 @@ +
+
+
+
+
+ + + + + + +
+
+
+
+

+ + + + + + +

+

Heading XL Marquee standard medium left

+

Welcome $firstName

+

Body M Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.

+

Lorem ipsum Learn more Text link

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

Partner NOT SIGNED IN

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+

Join Now

+
+
+
+
+
+
+

Partner NON MEMBER

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+

Join Now

+
+
+
+
+
+
+

MEMBER

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+
+
+
+
+
+
+
+
+

Partner GOLD

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+
+
+
+
+
+
+
+
+

Partner Platinum

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+
+
+
+ +
+
+
+
+
+

Partner Platinum section

+

Featuring over 600,000 hand-picked stock photos and graphics, curated from the world’s leading photographers, illustrators, and agencies. Our Premium collection is perfect for organizations looking for authentic, high-quality commercial content, and easy licensing plans.

+

Explore the premium collection

+
+
+
+ +
+
diff --git a/test/scripts/personalization.jest.js b/test/scripts/personalization.jest.js new file mode 100644 index 0000000..2319a18 --- /dev/null +++ b/test/scripts/personalization.jest.js @@ -0,0 +1,158 @@ +/** + * @jest-environment jsdom + */ +import path from 'path'; +import fs from 'fs'; + +const PERSONALIZATION_HIDE_CLASS = 'personalization-hide'; + +function importModules() { + const utils = require('../../edsdme/scripts/utils.js'); + const placeholderElement = document.querySelector('#welcome-firstname'); + jest.spyOn(utils, 'getNodesByXPath').mockImplementation(() => [placeholderElement]); + const { applyPagePersonalization } = require('../../edsdme/scripts/personalization.js'); + + return applyPagePersonalization; +} + +describe('Test utils.js', () => { + beforeEach(() => { + jest.clearAllMocks(); + window = Object.create(window); + Object.defineProperty(window, 'location', { + value: { + pathname:'/channelpartners', + }, + writable: true + }); + document.body.innerHTML = fs.readFileSync( + path.resolve(__dirname, './mocks/personalization.html'), + 'utf8' + ); + document.cookie = 'partner_data='; + }); + afterEach(() => { + document.body.innerHTML = ''; + }); + it('Populate placeholder if user is a member', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test user' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const placeholderElementAfter = document.querySelector('#welcome-firstname'); + expect(placeholderElementAfter.textContent.includes(cookieObject.CPP.firstName)).toBe(true); + }); + }); + it('Remove placeholder if user is not a member', () => { + jest.isolateModules(() => { + const cookieObject = { + SPP: { + status: 'MEMBER', + firstName: 'Test use' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const placeholderElementAfter = document.querySelector('#welcome-firstname'); + expect(placeholderElementAfter).toBe(null); + }); + }); + it('Show partner-not-signed-in block', () => { + jest.isolateModules(() => { + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const notSignedInBlock = document.querySelector('.partner-not-signed-in'); + expect(notSignedInBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + + it('Show partner-not-member block', () => { + jest.isolateModules(() => { + const cookieObject = { + SPP: { + status: 'MEMBER', + firstName: 'Test use' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const notMemberBlock = document.querySelector('.partner-not-member'); + expect(notMemberBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + it('Show partner-all-levels block', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Gold' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const allLevelsBlock = document.querySelector('.partner-all-levels'); + expect(allLevelsBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + it('Show partner-level-gold block', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Gold' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const goldBlock = document.querySelector('.partner-level-gold'); + expect(goldBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); + it('Show partner-level-platinum but don\'t show partner-level-gold block', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Platinum' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const goldBlock = document.querySelector('.partner-level-gold'); + const platinumBlock = document.querySelector('.partner-level-platinum'); + expect(platinumBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + expect(goldBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(true); + }); + }); + it('Show partner-level-platinum section', () => { + jest.isolateModules(() => { + const cookieObject = { + CPP: { + status: 'MEMBER', + firstName: 'Test use', + level: 'Platinum' + } + }; + document.cookie = `partner_data=${JSON.stringify(cookieObject)}`; + const applyPagePersonalization = importModules(); + applyPagePersonalization(); + const platinumBlock = document.querySelector('#platinum-section'); + expect(platinumBlock.classList.contains(PERSONALIZATION_HIDE_CLASS)).toBe(false); + }); + }); +}); +