From fd32858dd369723fa8736f83d4723234cbdc91f8 Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Wed, 19 Feb 2025 21:17:47 -0700 Subject: [PATCH] MWPW-168109, 166126 MEP Stage Batch (#3709) * MWPW-168109: do not modify absolute path to a script hosted in DAM when using insertScript action (#3708) * MWPW-168109: do not modify absolute path to a script hosted in DAM when using insertScript action * wording change * Update libs/features/personalization/personalization.js Co-authored-by: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> --------- Co-authored-by: Denys Fedotov Co-authored-by: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> * [MWPW-166126] Add new "updateAttribute" action to MEP (#3670) * Get last string, check for match, save as attribute. * Attribute update, working state. * Refactor, good state. * Parameter update refactor. * Unit testing. * Highlight updates. * Unit test update. * Code review updates. * Error log update. * Spacing fix. * PR update. * Update libs/features/personalization/personalization.js * unit test update --------- Co-authored-by: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Co-authored-by: Vivian A Goodrich --------- Co-authored-by: Denys Fedotov Co-authored-by: Denys Fedotov Co-authored-by: Dave Linhart <132396886+AdobeLinhart@users.noreply.github.com> --- .../personalization/personalization.js | 91 +++++++++++++++---- test/features/personalization/actions.test.js | 13 +++ .../actions/manifestUpdateAttribute.json | 48 ++++++++++ .../modifyNonFragmentSelector.test.js | 21 +++++ .../personalization/personalization.test.js | 9 +- 5 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 test/features/personalization/mocks/actions/manifestUpdateAttribute.json diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index e92babfeb1..b26f4c9d8b 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -64,12 +64,12 @@ export const DATA_TYPE = { const IN_BLOCK_SELECTOR_PREFIX = 'in-block:'; +const isDamContent = (path) => path?.includes('/content/dam/'); + export const normalizePath = (p, localize = true) => { let path = p; - if (!path?.includes('/')) { - return path; - } + if (isDamContent(path) || !path?.includes('/')) return path; const config = getConfig(); if (path.startsWith('https://www.adobe.com/federal/')) { @@ -123,6 +123,7 @@ const CREATE_CMDS = { const COMMANDS_KEYS = { remove: 'remove', replace: 'replace', + updateAttribute: 'updateattribute', }; function addIds(el, manifestId, targetManifestId) { @@ -222,6 +223,34 @@ const COMMANDS = { createContent(el, cmd), ); }, + [COMMANDS_KEYS.updateAttribute]: (el, cmd) => { + const { manifestId, targetManifestId } = cmd; + if (!cmd.attribute || !cmd.content) return; + const [attribute, parameter] = cmd.attribute.split('_'); + + let value; + + if (attribute === 'href' && parameter) { + const href = el.getAttribute('href'); + try { + const url = new URL(href); + const parameters = new URLSearchParams(url.search); + parameters.set(parameter, cmd.content); + url.search = parameters.toString(); + value = url.toString(); + } catch (error) { + /* c8 ignore next 2 */ + console.log(`Invalid updateAttribute URL: ${href}`, error.message || error); + } + } else { + value = cmd.content; + } + + if (value) { + el.setAttribute(attribute, value); + addIds(el, manifestId, targetManifestId); + } + }, }; const log = (...msg) => { @@ -433,22 +462,34 @@ function getModifiers(selector) { } return { sel, modifiers }; } -export function modifyNonFragmentSelector(selector) { +export function modifyNonFragmentSelector(selector, action) { const { sel, modifiers } = getModifiers(selector); + + let modifiedSelector = sel + .split('>').join(' > ') + .split(',').join(' , ') + .replaceAll(/main\s*>?\s*(section\d*)/gi, '$1') + .split(/\s+/) + .map(modifySelectorTerm) + .join(' ') + .trim(); + + let attribute; + + if (action === COMMANDS_KEYS.updateAttribute) { + const string = modifiedSelector.split(' ').pop(); + attribute = string.replace('.', ''); + modifiedSelector = modifiedSelector.replace(string, '').trim(); + } + return { - modifiedSelector: sel - .split('>').join(' > ') - .split(',').join(' , ') - .replaceAll(/main\s*>?\s*(section\d*)/gi, '$1') - .split(/\s+/) - .map(modifySelectorTerm) - .join(' ') - .trim(), + modifiedSelector, modifiers, + attribute, }; } -function getSelectedElements(sel, rootEl, forceRootEl) { +function getSelectedElements(sel, rootEl, forceRootEl, action) { const root = forceRootEl ? rootEl : document; const selector = sel.trim(); if (!selector) return {}; @@ -464,7 +505,12 @@ function getSelectedElements(sel, rootEl, forceRootEl) { return { els: [], modifiers: [] }; } } - const { modifiedSelector, modifiers } = modifyNonFragmentSelector(selector); + const { + modifiedSelector, + modifiers, + attribute, + } = modifyNonFragmentSelector(selector, action); + let els; try { els = root.querySelectorAll(modifiedSelector); @@ -473,10 +519,11 @@ function getSelectedElements(sel, rootEl, forceRootEl) { log('Invalid selector: ', selector); return null; } - if (modifiers.includes(FLAGS.all) || !els.length) return { els, modifiers }; + if (modifiers.includes(FLAGS.all) || !els.length) return { els, modifiers, attribute }; els = [els[0]]; - return { els, modifiers }; + return { els, modifiers, attribute }; } + const addHash = (url, newHash) => { if (!newHash) return url; try { @@ -523,8 +570,13 @@ export function handleCommands( cmd.selectorType = IN_BLOCK_SELECTOR_PREFIX; return; } - const { els, modifiers } = getSelectedElements(selector, rootEl, forceRootEl); - cmd.modifiers = modifiers; + const { + els, + modifiers, + attribute, + } = getSelectedElements(selector, rootEl, forceRootEl, action); + + Object.assign(cmd, { modifiers, attribute }); els?.forEach((el) => { if (!el @@ -1195,7 +1247,7 @@ function sendTargetResponseAnalytics(failure, responseStart, timeoutLocal, messa }, }, data: - { _adobe_corpnew: { digitalData: { primaryEvent: { eventInfo: { eventName: val } } } } }, + { _adobe_corpnew: { digitalData: { primaryEvent: { eventInfo: { eventName: val } } } } }, }); }, { once: true }); } @@ -1290,6 +1342,7 @@ export async function init(enablements = {}) { enablePersV2, hybridPersEnabled, }; + manifests = manifests.concat(await combineMepSources(pzn, promo, mepParam)); manifests?.forEach((manifest) => { if (manifest.disabled) return; diff --git a/test/features/personalization/actions.test.js b/test/features/personalization/actions.test.js index 82e9fac7ae..9c4110ba5d 100644 --- a/test/features/personalization/actions.test.js +++ b/test/features/personalization/actions.test.js @@ -80,6 +80,19 @@ describe('replace action', () => { }); }); +describe('updateAttribute action', async () => { + it('updateAttribute should add or modify a html element attribute', async () => { + let manifestJson = await readFile({ path: './mocks/actions/manifestUpdateAttribute.json' }); + manifestJson = JSON.parse(manifestJson); + setFetchResponse(manifestJson); + await init(mepSettings); + await handleCommands(manifestJson.data, undefined, true, true); + expect(document.querySelector('.marquee h2').getAttribute('class')).to.equal('added-class'); + expect(document.querySelector('.marquee strong a').getAttribute('href')).to.equal('https://www.google.com/?osi=new-parameter#_inline'); + expect(document.querySelector('.marquee em a').getAttribute('new-attribute')).to.equal('added-attribute'); + }); +}); + describe('insertAfter action', async () => { it('insertContentAfter should add fragment after target content and fragment', async () => { let manifestJson = await readFile({ path: './mocks/actions/manifestInsertAfter.json' }); diff --git a/test/features/personalization/mocks/actions/manifestUpdateAttribute.json b/test/features/personalization/mocks/actions/manifestUpdateAttribute.json new file mode 100644 index 0000000000..66716524ae --- /dev/null +++ b/test/features/personalization/mocks/actions/manifestUpdateAttribute.json @@ -0,0 +1,48 @@ +{ + "total": 5, + "offset": 0, + "limit": 5, + "data": [ + { + "action": "updateattribute", + "selector": "marquee strong a href", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "content": "https://www.google.com/" + }, + { + "action": "updateattribute", + "selector": "marquee strong a href_osi", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "content": "new-parameter" + }, + { + "action": "updateattribute", + "selector": "marquee em a new-attribute", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "content": "added-attribute" + }, + { + "action": "updateattribute", + "selector": "marquee h2 class", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "content": "added-class" + }, + { + "action": "updateattribute", + "selector": ".second-marquee-title strong a href", + "page filter (optional)": "", + "param-newoffer=123": "", + "chrome": "", + "content": "thisshouldfail" + } + ], + ":type": "sheet" +} diff --git a/test/features/personalization/modifyNonFragmentSelector.test.js b/test/features/personalization/modifyNonFragmentSelector.test.js index 58b6813b4b..dcd71ca128 100644 --- a/test/features/personalization/modifyNonFragmentSelector.test.js +++ b/test/features/personalization/modifyNonFragmentSelector.test.js @@ -1,6 +1,19 @@ import { expect } from '@esm-bundle/chai'; import { modifyNonFragmentSelector } from '../../../libs/features/personalization/personalization.js'; +const attributeValue = [ + { + c: 'href', + b: 'marquee primary-cta href', + a: '.marquee strong a', + }, + { + c: 'href_osi', + b: 'marquee primary-cta href_osi', + a: '.marquee strong a', + }, +]; + const values = [ { b: 'body > main > div', @@ -160,4 +173,12 @@ describe('test different values', () => { expect(modifiers).to.deep.equal(value.m || []); }); }); + attributeValue.forEach((value) => { + it(`should return the expected value for ${value.b} and ${value.c}`, () => { + const { modifiedSelector, modifiers, attribute } = modifyNonFragmentSelector(value.b, 'updateattribute'); + expect(modifiedSelector).to.equal(value.a); + expect(modifiers).to.deep.equal(value.m || []); + expect(attribute).to.equal(value.c); + }); + }); }); diff --git a/test/features/personalization/personalization.test.js b/test/features/personalization/personalization.test.js index 44f7106e3e..30edf9e66b 100644 --- a/test/features/personalization/personalization.test.js +++ b/test/features/personalization/personalization.test.js @@ -3,7 +3,7 @@ import { readFile } from '@web/test-runner-commands'; import { assert, stub } from 'sinon'; import { getConfig, setConfig } from '../../../libs/utils/utils.js'; import { - handleFragmentCommand, applyPers, cleanAndSortManifestList, + handleFragmentCommand, applyPers, cleanAndSortManifestList, normalizePath, init, matchGlob, createContent, combineMepSources, buildVariantInfo, } from '../../../libs/features/personalization/personalization.js'; import mepSettings from './mepSettings.js'; @@ -90,6 +90,13 @@ describe('Functional Test', () => { expect(document.querySelector('.custom-block-2')).to.be.null; }); + it('should not normalize absolute path to a script file, if the file is hosted in DAM', async () => { + const DAMpath = 'https://www.adobe.com/content/dam/cc/optimization/mwpw-168109/test.js'; + const nonDAMpath = 'https://www.adobe.com/foo/test.js'; + expect(normalizePath(DAMpath)).to.include('https://www.adobe.com'); + expect(normalizePath(nonDAMpath)).to.not.include('https://www.adobe.com'); + }); + it('scheduled manifest should apply changes if active (bts)', async () => { const config = getConfig(); config.mep = {