Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deconflicting for MWPW-136727 MEP: Support for simplified selectors #1959

Merged
merged 30 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5a3dc01
WIP
chrischrischris Oct 6, 2023
dc7a628
Add tests
chrischrischris Feb 26, 2024
6a9284a
stash
vgoodric Feb 29, 2024
51e3a0a
stash
vgoodric Feb 29, 2024
c2185a6
stash
vgoodric Mar 1, 2024
4f1ee26
Merge branch 'mepmoreactions' into mepmanifestorder
vgoodric Mar 2, 2024
1089101
Merge remote-tracking branch 'origin/mep-cell' into simplifiedselectors
vgoodric Mar 2, 2024
a80d31d
merge select functions
vgoodric Mar 2, 2024
dd79578
unit tests fixed
vgoodric Mar 4, 2024
1199eca
fix for test or promo type
vgoodric Mar 4, 2024
1486a5b
create unit test file
vgoodric Mar 4, 2024
66b803b
fix unit test error
vgoodric Mar 4, 2024
cf3fb12
unit tests
vgoodric Mar 4, 2024
c95d7b0
Merge branch 'mepmoreactions' into mepmanifestorder
vgoodric Mar 5, 2024
6f8fd8b
improve merge of duplicate manifests
vgoodric Mar 5, 2024
655b272
fix merge of Target with fresh manifest, add unit test
vgoodric Mar 5, 2024
79b48e5
Merge branch 'mepmanifestorder' into simplifiedselectors
vgoodric Mar 5, 2024
464d030
new branch to separate my updates from others
vgoodric Mar 9, 2024
a0a9878
export matchGlob
vgoodric Mar 9, 2024
6294c18
see if removing matchGlob fixes unit test
vgoodric Mar 9, 2024
d96926b
found unit test issue
vgoodric Mar 9, 2024
ca185e5
stash
vgoodric Mar 11, 2024
2664595
Merge branch 'mepmoreactions' into mepmanifestorder2
vgoodric Mar 11, 2024
1575a7f
unit test repair
vgoodric Mar 12, 2024
9a0cea9
increase coverage
vgoodric Mar 12, 2024
53cef6b
Merge branch 'mepmanifestorder2' into simplifiedselectors
vgoodric Mar 12, 2024
f594c15
Merge branch 'mepmoreactions' into simplifiedselectors
vgoodric Mar 15, 2024
89d436b
merge conflicts
vgoodric Mar 15, 2024
c88c97b
remove duplicate from merge
vgoodric Mar 15, 2024
b236921
remove another duplicate from merge
vgoodric Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 148 additions & 57 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,35 +262,106 @@ function normalizeKeys(obj) {
}, {});
}

function getSelectedElement(selector, action) {
const getDivInTargetedCell = (tableCell) => {
tableCell.replaceChildren();
const div = document.createElement('div');
tableCell.appendChild(div);
return div;
};

const querySelector = (el, selector, all = false) => {
try {
if ((action.includes('appendtosection') || action.includes('prependtosection')) && selector.includes('section')) {
let section = selector.trim().replace('section', '');
if (section === '') section = 1;
if (Number.isNaN(section)) return null;
return document.querySelector(`main > :nth-child(${section})`);
}
if (checkSelectorType(selector) === 'fragment') {
return all ? el.querySelectorAll(selector) : el.querySelector(selector);
} catch (e) {
/* eslint-disable-next-line no-console */
console.log('Invalid selector: ', selector);
return null;
}
};

function getTrailingNumber(s) {
const match = s.match(/\d+$/);
return match ? parseInt(match[0], 10) : null;
}

function getSection(rootEl, idx) {
return rootEl === document
? document.querySelector(`body > main > div:nth-child(${idx})`)
: rootEl.querySelector(`:scope > div:nth-child(${idx})`);
}

function getSelectedElement(selector, action, rootEl) {
if (!selector) return null;
if ((action.includes('appendtosection') || action.includes('prependtosection'))) {
if (!selector.includes('section')) return null;
const section = selector.trim().replace('section', '');
if (section !== '' && Number.isNaN(section)) return null;
}
if (checkSelectorType(selector) === 'fragment') {
try {
const fragment = document.querySelector(`a[href*="${normalizePath(selector)}"]`);
if (fragment) {
return fragment.parentNode;
}
if (fragment) return fragment.parentNode;
return null;
} catch (e) {
return null;
}
return document.querySelector(selector);
} catch (e) {
return null;
}

let selectedEl;
if (selector.includes('.') || !['section', 'block', 'row'].some((s) => selector.includes(s))) {
selectedEl = querySelector(rootEl, selector);
if (selectedEl) return selectedEl;
}

const terms = selector.split(/\s+/);

let section = null;
if (terms[0]?.startsWith('section')) {
const sectionIdx = getTrailingNumber(terms[0]) || 1;
section = getSection(rootEl, sectionIdx);
terms.shift();
}

if (terms.length) {
const blockStr = terms.shift();
if (blockStr.includes('.')) {
selectedEl = querySelector(section || rootEl, blockStr);
} else {
const blockIndex = getTrailingNumber(blockStr) || 1;
const blockName = blockStr.replace(`${blockIndex}`, '');
if (blockName === 'block') {
if (!section) section = getSection(rootEl, 1);
selectedEl = querySelector(section, `:scope > div:nth-of-type(${blockIndex})`);
} else {
selectedEl = querySelector(section || rootEl, `.${blockName}`, true)?.[blockIndex - 1];
}
}
} else if (section) {
return section;
}

if (terms.length) {
// find targeted table cell in rowX colY format
const rowColMatch = /row(?<row>\d+)\s+col(?<col>\d+)/gm.exec(terms.join(' '));
if (rowColMatch) {
const { row, col } = rowColMatch.groups;
const tableCell = querySelector(selectedEl, `:nth-child(${row}) > :nth-child(${col})`);
if (!tableCell) return null;
return getDivInTargetedCell(tableCell);
}
}

return selectedEl;
}

function handleCommands(commands, manifestId) {
function handleCommands(commands, manifestId, rootEl = document) {
commands.forEach((cmd) => {
const { action, selector, target } = cmd;
if (action in COMMANDS) {
const el = getSelectedElement(selector, action);
const el = getSelectedElement(selector, action, rootEl);
COMMANDS[action](el, target, manifestId);
} else if (action in CREATE_CMDS) {
const el = getSelectedElement(selector, action);
const el = getSelectedElement(selector, action, rootEl);
el?.insertAdjacentElement(CREATE_CMDS[action], createFrag(el, target, manifestId));
} else {
/* c8 ignore next 2 */
Expand Down Expand Up @@ -396,7 +467,6 @@ function parsePlaceholders(placeholders, config, selectedVariantName = '') {
}
return config;
}
/* c8 ignore stop */

const checkForParamMatch = (paramStr) => {
const [name, val] = paramStr.split('param-')[1].split('=');
Expand Down Expand Up @@ -497,14 +567,32 @@ export async function getPersConfig(info) {
}

const infoTab = manifestInfo || data?.info?.data;
config.manifestType = infoTab
?.find((element) => element.key?.toLowerCase() === 'manifest-type')?.value?.toLowerCase()
|| 'personalization';

config.manifestOverrideName = infoTab
?.find((element) => element.key?.toLowerCase() === 'manifest-override-name')
?.value?.toLowerCase();

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;
}, {});
config.manifestOverrideName = infoObj?.['manifest-override-name']?.toLowerCase();
config.manifestType = infoObj?.['manifest-type']?.toLowerCase();
const executionOrder = {
'manifest-type': 1,
'manifest-execution-order': 1,
};
Object.keys(infoObj).forEach((key) => {
if (!infoKeyMap[key]) return;
const index = infoKeyMap[key].indexOf(infoObj[key]);
executionOrder[key] = index > -1 ? index : 1;
});
config.executionOrder = `${executionOrder['manifest-execution-order']}-${executionOrder['manifest-type']}`;
} else {
// eslint-disable-next-line prefer-destructuring
config.manifestType = infoKeyMap[1];
config.executionOrder = '1-1';
}
const selectedVariantName = await getPersonalizationVariant(
manifestPath,
config.variantNames,
Expand Down Expand Up @@ -547,17 +635,11 @@ const normalizeFragPaths = ({ selector, val, action }) => ({
action,
});

export async function runPersonalization(info, config) {
const { manifestPath } = info;

const experiment = await getPersConfig(info);
export async function runPersonalization(experiment, config) {
if (!experiment) return null;

const { selectedVariant } = experiment;
const { manifestPath, selectedVariant } = experiment;
if (!selectedVariant) return {};
if (selectedVariant === 'default') {
return { experiment };
}
if (selectedVariant === 'default') return { experiment };

if (selectedVariant.replacepage) {
// only one replacepage can be defined
Expand Down Expand Up @@ -586,25 +668,31 @@ export async function runPersonalization(info, config) {
};
}

function cleanManifestList(manifests) {
const manifestPaths = [];
const cleanedList = [];
function compareExecutionOrder(a, b) {
if (a.executionOrder === b.executionOrder) return 0;
return a.executionOrder > b.executionOrder ? 1 : -1;
}

export function cleanAndSortManifestList(manifests) {
const manifestObj = {};
manifests.forEach((manifest) => {
try {
const url = new URL(manifest.manifestPath);
manifest.manifestPath = url.pathname;
} catch (e) {
// do nothing
}
const foundIndex = manifestPaths.indexOf(manifest.manifestPath);
if (foundIndex === -1) {
manifestPaths.push(manifest.manifestPath);
cleanedList.push(manifest);
manifest.manifestPath = normalizePath(manifest.manifestUrl || manifest.manifest);
if (manifest.manifestPath in manifestObj) {
let fullManifest = manifestObj[manifest.manifestPath];
let freshManifest = manifest;
if (manifest.name) {
fullManifest = manifest;
freshManifest = manifestObj[manifest.manifestPath];
}
freshManifest.name = fullManifest.name;
freshManifest.selectedVariantName = fullManifest.selectedVariantName;
freshManifest.selectedVariant = freshManifest.variants[freshManifest.selectedVariantName];
manifestObj[manifest.manifestPath] = freshManifest;
} else {
cleanedList[foundIndex] = { ...cleanedList[foundIndex], ...manifest };
manifestObj[manifest.manifestPath] = manifest;
}
});
return cleanedList;
return Object.values(manifestObj).sort(compareExecutionOrder);
}

const createDefaultExperiment = (manifest) => ({
Expand All @@ -620,20 +708,23 @@ export async function applyPers(manifests) {
const config = getConfig();

if (!manifests?.length) return;
let experiments = manifests;
for (let i = 0; i < experiments.length; i += 1) {
experiments[i] = await getPersConfig(experiments[i]);
}

const cleanedManifests = cleanManifestList(manifests);
experiments = cleanAndSortManifestList(experiments);

const override = config.mep?.override;
let results = [];
const experiments = [];
for (const manifest of cleanedManifests) {
if (manifest.disabled && !override) {
experiments.push(createDefaultExperiment(manifest));

for (const experiment of experiments) {
if (experiment.disabled && !override) {
experiments.push(createDefaultExperiment(experiment));
} else {
const result = await runPersonalization(manifest, config);
const result = await runPersonalization(experiment, config);
if (result) {
results.push(result);
experiments.push(result.experiment);
}
}
}
Expand Down
76 changes: 76 additions & 0 deletions test/features/personalization/cleanAndSortManifestList.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect } from '@esm-bundle/chai';
import { readFile } from '@web/test-runner-commands';
import { cleanAndSortManifestList } from '../../../libs/features/personalization/personalization.js';

// Note that the manifestPath doesn't matter as we stub the fetch
describe('Functional test', () => {
it('Pzn before test, even when test is from Target', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/two-manifests-one-from-target.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first, second] = manifestList;
expect(first.manifestUrl).to.include('pzn-normal.json');
expect(second.manifestUrl).to.include('test-normal.json');
});

it('Duplicate manifests, Target version is updated', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/two-duplicate-manifests-one-from-target.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first] = manifestList;
const { name, selectedVariantName, selectedVariant } = first;
expect(name).to.equal('MILO0013b - use fresh manifest over saved');
expect(selectedVariantName).to.equal('all');
expect(selectedVariant.commands[0].target).to.include('fresh-from-json-version');
});

it('One of each, all normal', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/three-manifest-types-all-normal.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first, second, third] = manifestList;
expect(first.manifestUrl).to.include('pzn-normal.json');
expect(second.manifestUrl).to.include('promo-normal.json');
expect(third.manifestUrl).to.include('test-normal.json');
});

it('One of each, promo is first and rest are normal', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/three-manifest-types-one-first.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first, second, third] = manifestList;
expect(first.manifestUrl).to.include('promo-first.json');
expect(second.manifestUrl).to.include('pzn-normal.json');
expect(third.manifestUrl).to.include('test-normal.json');
});

it('One of each, promo is last and rest are normal', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/three-manifest-types-one-last.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first, second, third] = manifestList;
expect(first.manifestUrl).to.include('pzn-normal.json');
expect(second.manifestUrl).to.include('test-normal.json');
expect(third.manifestUrl).to.include('promo-last.json');
});

it('One of each, promo is deprecated "Test or Promo" and rest are normal', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/three-manifest-types-one-test-or-promo.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first, second, third] = manifestList;
expect(first.manifestUrl).to.include('pzn-normal.json');
expect(second.manifestUrl).to.include('test-or-promo.json');
expect(third.manifestUrl).to.include('test-normal.json');
});

it('One of each, promo has no type and rest are normal', async () => {
let manifestJson = await readFile({ path: './mocks/manifestLists/three-manifest-types-one-no-type.json' });
manifestJson = JSON.parse(manifestJson);
const manifestList = cleanAndSortManifestList(manifestJson);
const [first, second, third] = manifestList;
expect(first.manifestUrl).to.include('pzn-normal.json');
expect(second.manifestUrl).to.include('none.json');
expect(third.manifestUrl).to.include('test-normal.json');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
"firefox": "",
"android": "",
"ios": ""
},
{
"action": "replaceContent",
"selector": ".z-pattern.small row3 col2",
"page filter (optional)": "",
"param-newoffer=123": "",
"chrome": "/fragments/milo-replace-r3c2",
"firefox": "",
"android": "",
"ios": ""
}
],
":type": "sheet"
Expand Down
22 changes: 22 additions & 0 deletions test/features/personalization/mocks/manifestBlockNumber.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"total": 5,
"offset": 0,
"limit": 5,
"data": [
{
"action": "replaceContent",
"selector": "marquee row2 col1",
"page filter (optional)": "",
"param-newoffer=123": "",
"all": "/fragments/replace/marquee/r2c1"
},
{
"action": "replaceContent",
"selector": "section5 marquee row2 col2",
"page filter (optional)": "",
"param-newoffer=123": "",
"all": "/fragments/replace/marquee-2/r2c2"
}
],
":type": "sheet"
}
Loading
Loading