diff --git a/.github/workflows/helpers.js b/.github/workflows/helpers.js
new file mode 100644
index 0000000000..da3565c095
--- /dev/null
+++ b/.github/workflows/helpers.js
@@ -0,0 +1,86 @@
+// Those env variables are set by an github action automatically
+// For local testing, you should test on your fork.
+const owner = process.env.REPO_OWNER || ''; // example owner: adobecom
+const repo = process.env.REPO_NAME || ''; // example repo name: milo
+const auth = process.env.GH_TOKEN || ''; // https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
+
+const getLocalConfigs = () => {
+ if (!owner || !repo || !auth) {
+ throw new Error(`Create a .env file on the root of the project with credentials.
+Then run: node --env-file=.env .github/workflows/update-ims.js`);
+ }
+
+ const { Octokit } = require('@octokit/rest');
+ return {
+ github: { rest: new Octokit({ auth }) },
+ context: {
+ repo: {
+ owner,
+ repo,
+ },
+ },
+ };
+};
+
+const slackNotification = (text, webhook) => {
+ console.log(text);
+ return fetch(webhook || process.env.MILO_RELEASE_SLACK_WH, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ text }),
+ });
+};
+
+const addLabels = ({ pr, github, owner, repo }) =>
+ github.rest.issues
+ .listLabelsOnIssue({ owner, repo, issue_number: pr.number })
+ .then(({ data }) => {
+ pr.labels = data.map(({ name }) => name);
+ return pr;
+ });
+
+const addFiles = ({ pr, github, owner, repo }) =>
+ github.rest.pulls
+ .listFiles({ owner, repo, pull_number: pr.number })
+ .then(({ data }) => {
+ pr.files = data.map(({ filename }) => filename);
+ return pr;
+ });
+
+const getChecks = ({ pr, github, owner, repo }) =>
+ github.rest.checks
+ .listForRef({ owner, repo, ref: pr.head.sha })
+ .then(({ data }) => {
+ const checksByName = data.check_runs.reduce((map, check) => {
+ if (
+ !map.has(check.name) ||
+ new Date(map.get(check.name).completed_at) <
+ new Date(check.completed_at)
+ ) {
+ map.set(check.name, check);
+ }
+ return map;
+ }, new Map());
+ pr.checks = Array.from(checksByName.values());
+ return pr;
+ });
+
+const getReviews = ({ pr, github, owner, repo }) =>
+ github.rest.pulls
+ .listReviews({
+ owner,
+ repo,
+ pull_number: pr.number,
+ })
+ .then(({ data }) => {
+ pr.reviews = data;
+ return pr;
+ });
+
+module.exports = {
+ getLocalConfigs,
+ slackNotification,
+ pulls: { addLabels, addFiles, getChecks, getReviews },
+};
diff --git a/.github/workflows/localWorkflowConfigs.js b/.github/workflows/localWorkflowConfigs.js
deleted file mode 100644
index 399e0eb9ca..0000000000
--- a/.github/workflows/localWorkflowConfigs.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// Those env variables are set by an github action automatically
-// For local testing, you should test on your fork.
-const owner = process.env.REPO_OWNER || ''; // example owner: adobecom
-const repo = process.env.REPO_NAME || ''; // example repo name: milo
-const auth = process.env.GH_TOKEN || ''; // https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
-
-const getLocalConfigs = () => {
- if (!owner || !repo || !auth) {
- throw new Error(`Create a .env file on the root of the project with credentials.
-Then run: node --env-file=.env .github/workflows/update-ims.js`);
- }
-
- const { Octokit } = require('@octokit/rest');
- return {
- github: { rest: new Octokit({ auth: process.env.GH_TOKEN }) },
- context: {
- repo: {
- owner,
- repo,
- },
- },
- };
-};
-
-module.exports = getLocalConfigs;
diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js
new file mode 100644
index 0000000000..4340921b88
--- /dev/null
+++ b/.github/workflows/merge-to-stage.js
@@ -0,0 +1,250 @@
+const {
+ slackNotification,
+ getLocalConfigs,
+ pulls: { addLabels, addFiles, getChecks, getReviews },
+} = require('./helpers.js');
+
+// Run from the root of the project for local testing: node --env-file=.env .github/workflows/merge-to-stage.js
+const PR_TITLE = '[Release] Stage to Main';
+const SEEN = {};
+const REQUIRED_APPROVALS = process.env.REQUIRED_APPROVALS || 2;
+const STAGE = 'stage';
+const PROD = 'main';
+const LABELS = {
+ highPriority: 'high priority',
+ readyForStage: 'Ready for Stage',
+ SOTPrefix: 'SOT',
+ highImpact: 'high-impact',
+};
+
+const SLACK = {
+ merge: ({ html_url, number, title, highImpact }) =>
+ `:merged:${highImpact} PR merged to stage: <${html_url}|${number}: ${title}>.`,
+ openedSyncPr: ({ html_url, number }) =>
+ `:fast_forward: Created <${html_url}|Stage to Main PR ${number}>`,
+};
+
+let github, owner, repo;
+
+let body = `
+## common base root URLs
+**Homepage :** https://www.stage.adobe.com/
+**BACOM:** https://business.stage.adobe.com/fr/
+**CC:** https://www.stage.adobe.com/creativecloud.html
+**Blog:** https://blog.stage.adobe.com/
+**Acrobat:** https://www.stage.adobe.com/acrobat/online/sign-pdf.html
+
+**Milo:**
+- Before: https://main--milo--adobecom.hlx.live/?martech=off
+- After: https://stage--milo--adobecom.hlx.live/?martech=off
+`;
+
+const RCPDates = [
+ {
+ start: new Date('2024-05-26T00:00:00-07:00'),
+ end: new Date('2024-06-01T00:00:00-07:00'),
+ },
+ {
+ start: new Date('2024-06-13T11:00:00-07:00'),
+ end: new Date('2024-06-13T14:00:00-07:00'),
+ },
+ {
+ start: new Date('2024-06-30T00:00:00-07:00'),
+ end: new Date('2024-07-06T00:00:00-07:00'),
+ },
+ {
+ start: new Date('2024-08-25T00:00:00-07:00'),
+ end: new Date('2024-08-31T00:00:00-07:00'),
+ },
+ {
+ start: new Date('2024-09-12T11:00:00-07:00'),
+ end: new Date('2024-09-12T14:00:00-07:00'),
+ },
+ {
+ start: new Date('2024-10-14T00:00:00-07:00'),
+ end: new Date('2024-11-18T17:00:00-08:00'),
+ },
+ {
+ start: new Date('2024-11-17T00:00:00-08:00'),
+ end: new Date('2024-11-30T00:00:00-08:00'),
+ },
+ {
+ start: new Date('2024-12-12T11:00:00-08:00'),
+ end: new Date('2024-12-12T14:00:00-08:00'),
+ },
+ {
+ start: new Date('2024-12-15T00:00:00-08:00'),
+ end: new Date('2025-01-02T00:00:00-08:00'),
+ },
+];
+
+const isHighPrio = (labels) => labels.includes(LABELS.highPriority);
+
+const hasFailingChecks = (checks) =>
+ checks.some(
+ ({ conclusion, name }) =>
+ name !== 'merge-to-stage' && conclusion === 'failure'
+ );
+
+const getPRs = async () => {
+ let prs = await github.rest.pulls
+ .list({ owner, repo, state: 'open', per_page: 100, base: STAGE })
+ .then(({ data }) => data);
+ await Promise.all(prs.map((pr) => addLabels({ pr, github, owner, repo })));
+ prs = prs.filter((pr) => pr.labels.includes(LABELS.readyForStage));
+ await Promise.all([
+ ...prs.map((pr) => addFiles({ pr, github, owner, repo })),
+ ...prs.map((pr) => getChecks({ pr, github, owner, repo })),
+ ...prs.map((pr) => getReviews({ pr, github, owner, repo })),
+ ]);
+
+ prs = prs.filter(({ checks, reviews, number, title }) => {
+ if (hasFailingChecks(checks)) {
+ console.log(`Skipping ${number}: ${title} due to failing checks`);
+ return false;
+ }
+
+ const approvals = reviews.filter(({ state }) => state === 'APPROVED');
+ if (approvals.length < REQUIRED_APPROVALS) {
+ console.log(`Skipping ${number}: ${title} due to insufficient approvals`);
+ return false;
+ }
+
+ return true;
+ });
+
+ return prs.reverse(); // OLD PRs first
+};
+
+const merge = async ({ prs }) => {
+ console.log(`Merging ${prs.length || 0} PRs that are ready... `);
+ for await (const { number, files, html_url, title, labels } of prs) {
+ if (files.some((file) => SEEN[file])) {
+ console.log(`Skipping ${number}: ${title} due to overlap in files.`);
+ continue;
+ }
+ files.forEach((file) => (SEEN[file] = true));
+ if (!process.env.LOCAL_RUN) {
+ await github.rest.pulls.merge({
+ owner,
+ repo,
+ pull_number: number,
+ merge_method: 'squash',
+ });
+ }
+ body = `- [${title}](${html_url})\n${body}`;
+ const isHighImpact = labels.includes(LABELS.highImpact);
+ if (isHighImpact && process.env.SLACK_HIGH_IMPACT_PR_WEBHOOK) {
+ await slackNotification(
+ SLACK.merge({
+ html_url,
+ number,
+ title,
+ highImpact: ' :alert: High impact',
+ }),
+ process.env.SLACK_HIGH_IMPACT_PR_WEBHOOK
+ );
+ }
+ await slackNotification(
+ SLACK.merge({
+ html_url,
+ number,
+ title,
+ highImpact: isHighImpact ? ' :alert: High impact' : '',
+ })
+ );
+ }
+};
+
+const getStageToMainPR = () =>
+ github.rest.pulls
+ .list({ owner, repo, state: 'open', base: PROD })
+ .then(({ data } = {}) => data.find(({ title } = {}) => title === PR_TITLE))
+ .then((pr) => pr && addLabels({ pr, github, owner, repo }))
+ .then((pr) => pr && addFiles({ pr, github, owner, repo }))
+ .then((pr) => {
+ pr?.files.forEach((file) => (SEEN[file] = true));
+ return pr;
+ });
+
+const openStageToMainPR = async () => {
+ const { data: comparisonData } = await github.rest.repos.compareCommits({
+ owner,
+ repo,
+ base: PROD,
+ head: STAGE,
+ });
+
+ for (const commit of comparisonData.commits) {
+ const { data: pullRequestData } =
+ await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner,
+ repo,
+ commit_sha: commit.sha,
+ });
+
+ for (const pr of pullRequestData) {
+ if (!body.includes(pr.html_url))
+ body = `- [${pr.title}](${pr.html_url})\n${body}`;
+ }
+ }
+
+ try {
+ const {
+ data: { html_url, number },
+ } = await github.rest.pulls.create({
+ owner,
+ repo,
+ title: PR_TITLE,
+ head: STAGE,
+ base: PROD,
+ body,
+ });
+ await slackNotification(SLACK.openedSyncPr({ html_url, number }));
+ } catch (error) {
+ if (error.message.includes('No commits between main and stage'))
+ return console.log('No new commits, no stage->main PR opened');
+ throw error;
+ }
+};
+
+const main = async (params) => {
+ github = params.github;
+ owner = params.context.repo.owner;
+ repo = params.context.repo.repo;
+
+ const now = new Date();
+ // We need to revisit this every year
+ if (now.getFullYear() !== 2024) {
+ throw new Error('ADD NEW RCPs');
+ }
+ for (const { start, end } of RCPDates) {
+ if (start <= now && now <= end) {
+ console.log('Current date is within a RCP. Stopping execution.');
+ return;
+ }
+ }
+ try {
+ const stageToMainPR = await getStageToMainPR();
+ console.log('has Stage to Main PR:', !!stageToMainPR);
+ if (stageToMainPR?.labels.some((label) => label.includes(LABELS.SOTPrefix)))
+ return console.log('PR exists & testing started. Stopping execution.');
+ const prs = await getPRs();
+ await merge({ prs: prs.filter(({ labels }) => isHighPrio(labels)) });
+ await merge({ prs: prs.filter(({ labels }) => !isHighPrio(labels)) });
+ if (!stageToMainPR) await openStageToMainPR();
+ console.log('Process successfully executed.');
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+if (process.env.LOCAL_RUN) {
+ const { github, context } = getLocalConfigs();
+ main({
+ github,
+ context,
+ });
+}
+
+module.exports = main;
diff --git a/.github/workflows/merge-to-stage.yaml b/.github/workflows/merge-to-stage.yaml
new file mode 100644
index 0000000000..406e226527
--- /dev/null
+++ b/.github/workflows/merge-to-stage.yaml
@@ -0,0 +1,34 @@
+name: Merge to stage
+
+on:
+ schedule:
+ - cron: '0 */4 * * *' # Run every 4 hours
+ workflow_dispatch: # Allow manual trigger
+
+env:
+ MILO_RELEASE_SLACK_WH: ${{ secrets.MILO_RELEASE_SLACK_WH }}
+ REQUIRED_APPROVALS: ${{ secrets.REQUIRED_APPROVALS }}
+ SLACK_HIGH_IMPACT_PR_WEBHOOK: ${{ secrets.SLACK_HIGH_IMPACT_PR_WEBHOOK }}
+
+jobs:
+ merge-to-stage:
+ runs-on: ubuntu-latest
+ environment: milo_pr_merge
+
+ steps:
+ - uses: actions/create-github-app-token@v1.10.0
+ id: milo-pr-merge-token
+ with:
+ app-id: ${{ secrets.MILO_PR_MERGE_APP_ID }}
+ private-key: ${{ secrets.MILO_PR_MERGE_PRIVATE_KEY }}
+
+ - name: Checkout repository
+ uses: actions/checkout@v4.1.4
+
+ - name: Merge to stage or queue to merge
+ uses: actions/github-script@v7.0.1
+ with:
+ github-token: ${{ steps.milo-pr-merge-token.outputs.token }}
+ script: |
+ const main = require('./.github/workflows/merge-to-stage.js')
+ main({ github, context })
diff --git a/.github/workflows/pr-reminders.js b/.github/workflows/pr-reminders.js
index a387846f34..975ec743ae 100644
--- a/.github/workflows/pr-reminders.js
+++ b/.github/workflows/pr-reminders.js
@@ -1,4 +1,5 @@
// Run from the root of the project for local testing: node --env-file=.env .github/workflows/pr-reminders.js
+const { getLocalConfigs } = require('./helpers.js');
const main = async ({ github, context }) => {
const comment = async ({ pr, message, comments }) => {
@@ -61,7 +62,11 @@ const main = async ({ github, context }) => {
issue_number: pr.number,
});
- if (labels.some(({ name } = {}) => name === 'Ready for Stage' || name === 'Stale')) {
+ if (
+ labels.some(
+ ({ name } = {}) => name === 'Ready for Stage' || name === 'Stale'
+ )
+ ) {
console.log(
`PR #${pr.number} has the 'Ready for Stage' or 'Stale' label. Skipping...`
);
@@ -101,12 +106,13 @@ const main = async ({ github, context }) => {
continue;
}
- if(labels.some(({ name } = {}) => name === 'needs-verification')) {
+ if (labels.some(({ name } = {}) => name === 'needs-verification')) {
comment({
pr,
comments,
- message: 'This PR is currently in the `needs-verification` state. Please assign a QA engineer to verify the changes.'
- })
+ message:
+ 'This PR is currently in the `needs-verification` state. Please assign a QA engineer to verify the changes.',
+ });
continue;
}
@@ -123,7 +129,7 @@ const main = async ({ github, context }) => {
};
if (process.env.LOCAL_RUN) {
- const { github, context } = require('./localWorkflowConfigs.js')();
+ const { github, context } = getLocalConfigs();
main({
github,
context,
diff --git a/.github/workflows/update-script.js b/.github/workflows/update-script.js
index 28e28088e0..6db34cfa06 100644
--- a/.github/workflows/update-script.js
+++ b/.github/workflows/update-script.js
@@ -1,18 +1,22 @@
const https = require('https');
const { execSync } = require('child_process');
const fs = require('fs');
+const { getLocalConfigs } = require('./helpers.js');
// Run from the root of the project for local testing: node --env-file=.env .github/workflows/update-script.js
const localExecution = process.env.LOCAL_RUN || false;
const localRunConfigs = {
branch: process.env.LOCAL_RUN_BRANCH || 'update-imslib',
- title: process.env.LOCAL_RUN_TITLTE || '[AUTOMATED-PR] Update imslib.min.js dependency',
- path: process.env.LOCAL_RUN_SCRIPT || 'https://auth.services.adobe.com/imslib/imslib.min.js',
+ title:
+ process.env.LOCAL_RUN_TITLTE ||
+ '[AUTOMATED-PR] Update imslib.min.js dependency',
+ path:
+ process.env.LOCAL_RUN_SCRIPT ||
+ 'https://auth.services.adobe.com/imslib/imslib.min.js',
scriptPath: process.env.LOCAL_RUN_SCRIPT_PATH || './libs/deps/imslib.min.js',
origin: process.env.LOCAL_RUN_ORIGIN || 'origin',
};
-
const getPrDescription = ({ branch, scriptPath }) => `## Description
Update ${scriptPath} to the latest version
@@ -126,12 +130,23 @@ const main = async ({
const selfHostedScript =
fs.existsSync(scriptPath) && fs.readFileSync(scriptPath, 'utf8');
- console.log(`/libs/deps script build date: ${selfHostedScript.match(/^\/\/ Built (.*?) -/)[1]}`);
- console.log(`/libs/deps script last modified date: ${selfHostedScript.match(/- Last Modified (.*?)\n/)[1]}`);
+ console.log(
+ `/libs/deps script build date: ${
+ selfHostedScript.match(/^\/\/ Built (.*?) -/)[1]
+ }`
+ );
+ console.log(
+ `/libs/deps script last modified date: ${
+ selfHostedScript.match(/- Last Modified (.*?)\n/)[1]
+ }`
+ );
console.log(`External script last modified date: ${lastModified}`);
- const scriptIsEqual = script === selfHostedScript.replace(/^\/\/ Built .*\n/, '');
- console.log(`Validating if "${scriptPath}" has changed. Script is the same: ${scriptIsEqual}`);
+ const scriptIsEqual =
+ script === selfHostedScript.replace(/^\/\/ Built .*\n/, '');
+ console.log(
+ `Validating if "${scriptPath}" has changed. Script is the same: ${scriptIsEqual}`
+ );
if (!scriptIsEqual || localExecution) {
const { data: openPRs } = await github.rest.pulls.list({
@@ -141,7 +156,10 @@ const main = async ({
});
const hasPR = openPRs.find((pr) => pr.head.ref === branch);
- if (hasPR) return console.log(`PR already exists for branch ${branch}. Execution stopped.`);
+ if (hasPR)
+ return console.log(
+ `PR already exists for branch ${branch}. Execution stopped.`
+ );
createAndPushBranch({ script, branch, scriptPath, origin, lastModified });
@@ -175,12 +193,15 @@ const main = async ({
});
}
} catch (error) {
- console.error(`An error occurred while running workflow for ${title}`, error);
+ console.error(
+ `An error occurred while running workflow for ${title}`,
+ error
+ );
}
};
if (localExecution) {
- const { github, context } = require('./localWorkflowConfigs.js')();
+ const { github, context } = getLocalConfigs();
main({
github,
context,
diff --git a/README.md b/README.md
index 04e4887355..b8cbb78ba3 100644
--- a/README.md
+++ b/README.md
@@ -68,3 +68,4 @@ npm run test:watch
```
### Coverage
`npm run test:watch` can give misleading coverage reports. Use `npm run test` for accurate coverage reporting.
+
diff --git a/libs/blocks/aside/aside.css b/libs/blocks/aside/aside.css
index c86e77cd83..c0ad710b34 100644
--- a/libs/blocks/aside/aside.css
+++ b/libs/blocks/aside/aside.css
@@ -240,6 +240,10 @@
flex-shrink: 0;
}
+.aside.notification .text [class^="heading-"] + .action-area {
+ margin-top: var(--spacing-xs);
+}
+
.aside.notification .foreground.container img {
display: block;
}
diff --git a/libs/blocks/fragment/fragment.js b/libs/blocks/fragment/fragment.js
index 3dd2665cfc..815a05fb18 100644
--- a/libs/blocks/fragment/fragment.js
+++ b/libs/blocks/fragment/fragment.js
@@ -93,7 +93,7 @@ export default async function init(a) {
const resp = await customFetch({ resource: `${a.href}.plain.html`, withCacheRules: true })
.catch(() => ({}));
- if (!resp.ok) {
+ if (!resp?.ok) {
window.lana?.log(`Could not get fragment: ${a.href}.plain.html`);
return;
}
diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js
index 9171e75629..f0cbf74c59 100644
--- a/libs/blocks/global-navigation/global-navigation.js
+++ b/libs/blocks/global-navigation/global-navigation.js
@@ -33,6 +33,7 @@ import {
toFragment,
trigger,
yieldToMain,
+ addMepHighlight,
} from './utilities/utilities.js';
import { replaceKey, replaceKeyArray } from '../../features/placeholders.js';
@@ -550,9 +551,9 @@ class Gnav {
case 'profile|click|sign-in':
return `Sign In|gnav|${experienceName}|unav`;
case 'profile|render|component':
- return `Account|gnav|${experienceName}`;
+ return `Account|gnav|${experienceName}|unav`;
case 'profile|click|account':
- return `View Account|gnav|${experienceName}`;
+ return `View Account|gnav|${experienceName}|unav`;
case 'profile|click|sign-out':
return `Sign Out|gnav|${experienceName}|unav`;
case 'app-switcher|render|component':
@@ -908,16 +909,16 @@ class Gnav {
observer.observe(dropdownTrigger, { attributeFilter: ['aria-expanded'] });
delayDropdownDecoration({ template: triggerTemplate });
- return triggerTemplate;
+ return addMepHighlight(triggerTemplate, item);
}
case 'primaryCta':
case 'secondaryCta':
// Remove its 'em' or 'strong' wrapper
item.parentElement.replaceWith(item);
- return toFragment`
+ return addMepHighlight(toFragment`
${decorateCta({ elem: item, type: itemType, index: index + 1 })}
-
`;
+
`, item);
case 'link': {
const linkElem = item.querySelector('a');
linkElem.className = 'feds-navLink';
@@ -934,16 +935,17 @@ class Gnav {
${linkElem}
`;
- return linkTemplate;
+ return addMepHighlight(linkTemplate, item);
}
case 'text':
- return toFragment`
+ return addMepHighlight(toFragment`
${item.textContent}
-
`;
+
`, item);
default:
- return toFragment`
+ /* c8 ignore next 3 */
+ return addMepHighlight(toFragment`
${item}
-
`;
+
`, item);
}
};
diff --git a/libs/blocks/global-navigation/utilities/menu/menu.js b/libs/blocks/global-navigation/utilities/menu/menu.js
index e71b77322f..39a570175c 100644
--- a/libs/blocks/global-navigation/utilities/menu/menu.js
+++ b/libs/blocks/global-navigation/utilities/menu/menu.js
@@ -12,6 +12,7 @@ import {
toFragment,
trigger,
yieldToMain,
+ addMepHighlight,
} from '../utilities.js';
const decorateHeadline = (elem, index) => {
@@ -321,6 +322,8 @@ const decorateMenu = (config) => logErrorFor(async () => {
${menuContent}
`;
+ addMepHighlight(menuTemplate, content);
+
decorateCrossCloudMenu(menuTemplate);
await decorateColumns({ content: menuContent });
diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js
index 3fd7d7ba3b..966889fd3d 100644
--- a/libs/blocks/global-navigation/utilities/utilities.js
+++ b/libs/blocks/global-navigation/utilities/utilities.js
@@ -55,6 +55,16 @@ 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;
+ }
+ if (manifestId) el.dataset.manifestId = manifestId;
+ return el;
+}
+
export function toFragment(htmlStrings, ...values) {
const templateStr = htmlStrings.reduce((acc, htmlString, index) => {
if (values[index] instanceof HTMLElement) {
@@ -313,11 +323,22 @@ export function trigger({ element, event, type } = {}) {
export const yieldToMain = () => new Promise((resolve) => { setTimeout(resolve, 0); });
export async function fetchAndProcessPlainHtml({ url, shouldDecorateLinks = true } = {}) {
- const path = getFederatedUrl(url);
+ let path = getFederatedUrl(url);
+ const mepGnav = getConfig()?.mep?.inBlock?.['global-navigation'];
+ const mepFragment = mepGnav?.fragments?.[path];
+ if (mepFragment && mepFragment.action === 'replace') {
+ path = mepFragment.target;
+ }
const res = await fetch(path.replace(/(\.html$|$)/, '.plain.html'));
const text = await res.text();
const { body } = new DOMParser().parseFromString(text, 'text/html');
-
+ if (mepFragment?.manifestId) body.dataset.manifestId = mepFragment.manifestId;
+ const commands = mepGnav?.commands;
+ if (commands?.length) {
+ const { handleCommands, deleteMarkedEls } = await import('../../../features/personalization/personalization.js');
+ handleCommands(commands, commands[0].manifestId, body, true);
+ deleteMarkedEls(body);
+ }
const inlineFrags = [...body.querySelectorAll('a[href*="#_inline"]')];
if (inlineFrags.length) {
const { default: loadInlineFrags } = await import('../../fragment/fragment.js');
diff --git a/libs/blocks/icon-block/icon-block.css b/libs/blocks/icon-block/icon-block.css
index 2b50032e0c..22d9eae5ef 100644
--- a/libs/blocks/icon-block/icon-block.css
+++ b/libs/blocks/icon-block/icon-block.css
@@ -36,11 +36,12 @@
.icon-block .foreground {
position: relative;
text-align: center;
+ width: 100%;
}
.icon-block .foreground .text-content {
display: flex;
- flex-wrap: wrap;
+ flex-direction: column;
align-content: center;
width: 100%;
}
@@ -167,6 +168,11 @@
margin: var(--spacing-s) auto var(--spacing-xs) auto;
}
+.section[class*="-up"] .icon-block .second-column {
+ display: flex;
+ flex-direction: column;
+}
+
.icon-block.bio.center .foreground .second-column {
margin-top: var(--spacing-s);
}
@@ -246,10 +252,18 @@
margin-bottom: var(--spacing-s);
}
+.icon-block.bio .cta-container {
+ margin-top: 0;
+}
+
.icon-block .foreground .text-content p.body-m.action-area {
margin-bottom: 0;
}
+.icon-block .cta-container .action-area {
+ width: 100%;
+}
+
.icon-block.full-width .foreground .text-content .action-area {
justify-content: center;
}
@@ -298,6 +312,11 @@
margin: unset;
}
+.section[class*="-up"] .icon-block .foreground {
+ display: flex;
+ align-self: stretch;
+}
+
[class*="-up"] .icon-block.bio.inline .foreground {
max-width: unset;
width: unset;
@@ -326,6 +345,27 @@
line-height: var(--type-body-m-lh);
}
+.section[class*="-up"] .icon-block {
+ display: flex;
+ align-self: stretch;
+}
+
+.icon-block .cta-container {
+ margin-top: var(--spacing-s);
+}
+
+.icon-block.inline .cta-container .body-s.action-area {
+ padding-top: 0;
+}
+
+.section[class*="-up"] .icon-block .cta-container {
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-end;
+ flex-direction: column;
+ width: 100%;
+}
+
@media screen and (min-width: 600px) and (max-width: 1200px) {
.icon-block .foreground .icon-area {
max-width: 234px;
@@ -428,8 +468,6 @@
}
.icon-block.vertical .foreground .text-content {
- flex-grow: 1;
- flex-basis: 0;
margin-bottom: 0;
}
diff --git a/libs/blocks/icon-block/icon-block.js b/libs/blocks/icon-block/icon-block.js
index 713f2a4012..b7163a1f3d 100644
--- a/libs/blocks/icon-block/icon-block.js
+++ b/libs/blocks/icon-block/icon-block.js
@@ -75,6 +75,15 @@ function decorateContent(el) {
if (secondColumn.children.length === 1) el.classList.add('items-center');
el.querySelector('.foreground .text-content').append(secondColumn);
}
+ const lastActionArea = el.querySelector('.action-area:last-of-type');
+ if (lastActionArea) {
+ const div = createTag('div', { class: 'cta-container' });
+ lastActionArea.insertAdjacentElement('afterend', div);
+ if (lastActionArea.previousElementSibling.className.includes('action-area')) {
+ div.append(lastActionArea.previousElementSibling);
+ }
+ div.append(lastActionArea);
+ }
}
}
diff --git a/libs/blocks/marquee/marquee.css b/libs/blocks/marquee/marquee.css
index eb3f6bfc53..784135bdff 100644
--- a/libs/blocks/marquee/marquee.css
+++ b/libs/blocks/marquee/marquee.css
@@ -33,7 +33,16 @@
.marquee .icon-area picture,
.marquee .icon-area a {
- display: contents;
+ display: inline-flex;
+}
+
+.marquee .icon-area a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.marquee .icon-area a:hover {
+ text-decoration: none;
}
.marquee.mobile-light a:not(.con-button) {
@@ -102,11 +111,15 @@
}
.marquee .icon-area {
- margin-bottom: var(--spacing-s);
+ display: flex;
+ gap: var(--spacing-xs);
+ align-items: center;
+ font-size: var(--type-body-m-size);
+ font-weight: 700;
}
.marquee .icon-area img {
- height: 48px;
+ height: var(--icon-size-l);
width: auto;
display: block;
}
@@ -184,8 +197,12 @@
margin: 0 auto;
}
+.marquee.large .icon-area {
+ font-size: var(--type-body-xl-size);
+}
+
.marquee.large .icon-area img {
- height: 64px;
+ height: var(--icon-size-xl);
}
.marquee.large .foreground {
@@ -228,7 +245,9 @@
.marquee.center .foreground,
.marquee.center .action-area,
.marquee.centered .foreground,
-.marquee.centered .action-area {
+.marquee.centered .action-area,
+.marquee.center .icon-area,
+.marquee.centered .icon-area {
justify-content: center;
}
@@ -283,26 +302,24 @@
.marquee.split .icon-area img {
height: auto;
- max-height: 56px;
+ max-height: var(--icon-size-l);
max-width: 275px;
object-fit: contain;
object-position: left bottom;
}
-.marquee .icon-area.icon-area-multiple {
- display: flex;
+.marquee.small .icon-area,
+.marquee .icon-area[icon-count] {
+ font-size: var(--type-body-s-size);
}
-.marquee .icon-area.icon-area-multiple img {
- height: 40px;
- max-height: 40px;
+.marquee.small .icon-area img,
+.marquee .icon-area[icon-count] img {
+ height: var(--icon-size-m);
+ max-height: var(--icon-size-m);
max-width: 114px;
}
-.marquee .icon-area.icon-area-multiple > :not(:first-child) img {
- margin-left: var(--spacing-xxs);
-}
-
.marquee.large .text .action-area {
order: 2;
margin-bottom: var(--spacing-s);
@@ -737,6 +754,7 @@
}
}
+/* stylelint-disable no-descending-specificity */
.marquee.static-links a:not(.con-button),
.marquee.static-links a:not(.con-button):hover,
.static-links .marquee a:not(.con-button),
diff --git a/libs/blocks/marquee/marquee.js b/libs/blocks/marquee/marquee.js
index 2889bced45..39f52449c6 100644
--- a/libs/blocks/marquee/marquee.js
+++ b/libs/blocks/marquee/marquee.js
@@ -33,9 +33,12 @@ function decorateText(el, size) {
}
function decorateMultipleIconArea(iconArea) {
- iconArea.querySelectorAll(':scope > picture').forEach((picture) => {
+ let count = 0;
+ iconArea.querySelectorAll(':scope picture').forEach((picture) => {
+ count += 1;
const src = picture.querySelector('img')?.getAttribute('src');
const a = picture.nextElementSibling;
+ if (count > 1) iconArea.setAttribute('icon-count', count);
if (src?.endsWith('.svg') || a?.tagName !== 'A') return;
if (!a.querySelector('img')) {
a.innerHTML = '';
@@ -43,7 +46,6 @@ function decorateMultipleIconArea(iconArea) {
a.appendChild(picture);
}
});
- if (iconArea.childElementCount > 1) iconArea.classList.add('icon-area-multiple');
}
function extendButtonsClass(text) {
diff --git a/libs/blocks/media/media.css b/libs/blocks/media/media.css
index 0ae3ca5ed8..71e2670a01 100644
--- a/libs/blocks/media/media.css
+++ b/libs/blocks/media/media.css
@@ -70,6 +70,7 @@
display: flex;
gap: var(--spacing-s);
align-items: flex-start;
+ width: 100%;
}
.media .text .icon-area {
@@ -254,6 +255,49 @@ div[class*="-up"] .media .foreground > .media-row {
height: var(--icon-size-xxl);
}
+/* Text Alignment */
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) {
+ display: flex;
+ align-self: stretch;
+}
+
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) .foreground {
+ display: flex;
+ align-self: stretch;
+ width: 100%
+}
+
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) .foreground > div.media-row {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) .foreground .text {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) [class*="heading-"],
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) p:not([class*="detail-"]):not(.subcopy-link),
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) [class*="body-"] {
+ margin-bottom: 0;
+}
+
+.section[class*="-up"] .media.merch.bio:not(.media-reverse-mobile, .qr-code) [class*="body-"]:first-child:not(.action-area, .icon-stack-area) {
+ margin-top: 0;
+ margin-bottom: revert;
+}
+
+.section[class*="-up"] .media:not(.media-reverse-mobile, .qr-code) .cta-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ width: 100%;
+ height: 100%;
+}
+
@media screen and (min-width: 600px) {
.media > .foreground .media-row {
gap: var(--spacing-m);
@@ -318,6 +362,10 @@ div[class*="-up"] .media .foreground > .media-row {
.media .icon-stack-area {
grid-template-columns: repeat(2, 1fr);
}
+
+ .section[class*="-up"] .media:not(.media-reverse-mobile) .foreground > div.media-row {
+ gap: 0;
+ }
}
@media screen and (min-width: 1200px) {
diff --git a/libs/blocks/media/media.js b/libs/blocks/media/media.js
index 63553f4a1a..8c8ea3d3c2 100644
--- a/libs/blocks/media/media.js
+++ b/libs/blocks/media/media.js
@@ -87,6 +87,15 @@ export default function init(el) {
link.className = 'body-xxs';
});
}
+ const lastActionArea = el.querySelector('.action-area:last-of-type');
+ if (lastActionArea) {
+ const div = createTag('div', { class: 'cta-container' });
+ lastActionArea.insertAdjacentElement('afterend', div);
+ if (lastActionArea.previousElementSibling.className.includes('icon-stack-area')) {
+ div.append(lastActionArea.previousElementSibling);
+ }
+ div.append(lastActionArea);
+ }
container.append(row);
});
diff --git a/libs/blocks/merch-card-collection/merch-card-collection.js b/libs/blocks/merch-card-collection/merch-card-collection.js
index 46dbbee88c..234f3aac19 100644
--- a/libs/blocks/merch-card-collection/merch-card-collection.js
+++ b/libs/blocks/merch-card-collection/merch-card-collection.js
@@ -300,7 +300,7 @@ export default async function init(el) {
}
const cardsRoot = await cardsRootPromise;
- const overridePromises = mep?.custom?.[BLOCK_NAME]?.map(
+ const overridePromises = mep?.inBlock?.[BLOCK_NAME]?.commands.map(
(action) => fetchOverrideCard(action, config),
);
const overrides = await overrideCards(cardsRoot, overridePromises, config);
diff --git a/libs/blocks/share/share.js b/libs/blocks/share/share.js
index 5badf5fafb..d5d766963c 100644
--- a/libs/blocks/share/share.js
+++ b/libs/blocks/share/share.js
@@ -37,26 +37,53 @@ export async function getSVGsfromFile(path, selectors) {
function getPlatforms(el) {
const manualShares = el.querySelectorAll('a');
- if (manualShares.length === 0) return null;
- return [...manualShares].map((link) => {
- const { href } = link;
+ if (manualShares.length === 0) return ['facebook', 'twitter', 'linkedin', 'pinterest', 'reddit'];
+ const platforms = [];
+ [...manualShares].forEach((share) => {
+ const { href } = share;
const url = new URL(href);
const parts = url.host.split('.');
- return parts[parts.length - 2];
+ platforms.push(parts[parts.length - 2]);
+ const parentP = share.closest('p');
+ parentP?.remove();
});
+ return platforms;
}
export default async function decorate(block) {
const config = getConfig();
const base = config.miloLibs || config.codeRoot;
- const platforms = getPlatforms(block) || [
- 'facebook',
- 'twitter',
- 'linkedin',
- 'pinterest',
- 'reddit',
- ];
- block.innerHTML = '';
+ const platforms = getPlatforms(block);
+ const rows = block.querySelectorAll(':scope > div');
+ const childDiv = rows[0]?.querySelector(':scope > div');
+ const emptyRow = rows.lengh && childDiv?.innerText.trim() === '';
+ const toSentenceCase = (str) => {
+ if (!str || typeof str !== 'string') return '';
+ /* eslint-disable-next-line no-useless-escape */
+ return str.toLowerCase().replace(/(^\s*\w|[\.\!\?]\s*\w)/g, (c) => c.toUpperCase());
+ };
+
+ if (block.classList.contains('inline')) {
+ rows[0].innerHTML = '';
+ } else {
+ rows[0]?.classList.add('tracking-header');
+ // add share placeholder if empty row
+ if (!rows.length || emptyRow) {
+ const heading = toSentenceCase(await replaceKey('share-this-page', config));
+ block.append(createTag('p', null, heading));
+ }
+ }
+
+ // wrap innerHTML in tag if none are present
+ if (childDiv && !emptyRow) {
+ const innerPs = childDiv.querySelectorAll(':scope > p');
+ if (innerPs.length === 0) {
+ const text = childDiv.innerText;
+ childDiv.innerText = '';
+ childDiv.append(createTag('p', null, text));
+ }
+ }
+
const clipboardSupport = !!navigator.clipboard;
if (clipboardSupport) platforms.push('clipboard');
const svgs = await getSVGsfromFile(
@@ -64,8 +91,7 @@ export default async function decorate(block) {
platforms,
);
if (!svgs) return;
- /* eslint-disable no-confusing-arrow,no-useless-escape */
- const toSentenceCase = (str) => str && typeof str === 'string' ? str.toLowerCase().replace(/(^\s*\w|[\.\!\?]\s*\w)/g, (c) => c.toUpperCase()) : '';
+
const shareToText = toSentenceCase(await replaceKey('share-to', config));
const url = encodeURIComponent(window.location.href);
const title = document.title ?? url;
@@ -98,13 +124,11 @@ export default async function decorate(block) {
href: `https://reddit.com/submit?url=${url}&title=${title}`,
};
default:
+ /* c8 ignore next 1 */
return null;
}
};
- if (!block.classList.contains('inline')) {
- const heading = toSentenceCase(await replaceKey('share-this-page', config));
- block.append(createTag('p', { class: 'tracking-header' }, heading));
- }
+
const container = createTag('p', { class: 'icon-container' });
svgs.forEach(async (svg) => {
if (svg.name === 'clipboard') return;
diff --git a/libs/blocks/share/share.svg b/libs/blocks/share/share.svg
index 29fc982f2e..53a5ecc825 100644
--- a/libs/blocks/share/share.svg
+++ b/libs/blocks/share/share.svg
@@ -10,7 +10,14 @@
diff --git a/libs/blocks/text/text.css b/libs/blocks/text/text.css
index 4fd3abcfa5..0104689af2 100644
--- a/libs/blocks/text/text.css
+++ b/libs/blocks/text/text.css
@@ -16,6 +16,7 @@
.text-block [class^="detail"] { margin: 0 0 var(--spacing-xxs) 0; }
+.text-block .cta-container,
.text-block p.action-area { margin-top: var(--spacing-s); }
.text-block div > *:last-child { margin-bottom: 0; }
@@ -62,6 +63,10 @@
align-items: center;
}
+.text-block .cta-container .action-area {
+ width: 100%;
+}
+
.text-block hr {
border-color: #e1e1e1;
border-style: solid;
@@ -162,7 +167,8 @@
justify-content: start;
}
-.text-block.icon-inline .foreground [class^="body-"] {
+.text-block.icon-inline .foreground [class^="body-"],
+.text-block.icon-inline .foreground .cta-container {
grid-column: span 2;
margin-top: 0;
}
@@ -191,9 +197,39 @@
display: none;
}
+/* Text Alignment */
+.section[class*="-up"] .text-block:not(.legal, .link-farm) {
+ display: flex;
+ align-self: stretch;
+}
+
+.section[class*="-up"] .text-block:not(.legal, .link-farm) .foreground {
+ display: flex;
+ align-self: stretch;
+ width: 100%;
+}
+
+.section[class*="-up"] .text-block:not(.legal, .link-farm) .foreground > div {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.section[class*="-up"] .text-block:not(.legal, .link-farm) [class^="heading"],
+.section[class*="-up"] .text-block:not(.legal, .link-farm) p:not([class^="detail-"]),
+.section[class*="-up"] .text-block:not(.legal, .link-farm) [class^="body-"] {
+ margin-bottom: 0;
+}
+
+.section[class*="-up"] .text-block:not(.legal, .link-farm) .cta-container {
+ display: flex;
+ align-items: flex-end;
+ width: 100%;
+ height: 100%;
+}
+
/* Tablet up */
@media screen and (min-width: 600px) {
-
.inset.text-block .foreground::before {
display: none;
}
diff --git a/libs/blocks/text/text.js b/libs/blocks/text/text.js
index 315215d3ba..c6f14eeea3 100644
--- a/libs/blocks/text/text.js
+++ b/libs/blocks/text/text.js
@@ -110,4 +110,11 @@ export default function init(el) {
el.classList.add(...helperClasses);
decorateTextOverrides(el);
if (!hasLinkFarm) decorateMultiViewport(el);
+
+ const lastActionArea = el.querySelector('.action-area:last-of-type');
+ if (lastActionArea) {
+ const div = createTag('div', { class: 'cta-container' });
+ lastActionArea.insertAdjacentElement('afterend', div);
+ div.append(lastActionArea);
+ }
}
diff --git a/libs/blocks/video-metadata/video-metadata.js b/libs/blocks/video-metadata/video-metadata.js
index f1de77c305..482eb24efc 100644
--- a/libs/blocks/video-metadata/video-metadata.js
+++ b/libs/blocks/video-metadata/video-metadata.js
@@ -66,10 +66,9 @@ function addSeekToActionField(videoObj, blockKey, blockValue) {
}
}
-export function createVideoObject(blockMap) {
+export function createVideoObject(record) {
const video = {};
- Object.entries(blockMap).forEach(([key, val]) => {
- const blockVal = val.content && val.content.textContent.trim();
+ Object.entries(record).forEach(([key, blockVal]) => {
if (!blockVal) return;
const blockKey = key && key.replaceAll(' ', '-');
switch (true) {
@@ -110,10 +109,21 @@ export function createVideoObject(blockMap) {
return null;
}
+export function blockMapToRecord(blockMap) {
+ return blockMap && Object.entries(blockMap).reduce((rec, kv) => {
+ const [key, value] = kv;
+ const val = value?.content?.textContent?.trim();
+ if (!val) return rec;
+ rec[key] = val;
+ return rec;
+ }, {});
+}
+
export default function init(el) {
const metadata = getMetadata(el);
el.remove();
- const obj = createVideoObject(metadata);
+ const record = blockMapToRecord(metadata);
+ const obj = createVideoObject(record);
if (!obj) return;
const script = createTag('script', { type: 'application/ld+json' }, JSON.stringify(obj));
document.head.append(script);
diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js
index 5dd4b835f2..eb1ad311dc 100644
--- a/libs/features/personalization/personalization.js
+++ b/libs/features/personalization/personalization.js
@@ -5,7 +5,7 @@ import {
} from '../../utils/utils.js';
import { getEntitlementMap } from './entitlements.js';
-/* c20 ignore start */
+/* c8 ignore start */
const PHONE_SIZE = window.screen.width < 768 || window.screen.height < 768;
export const PERSONALIZATION_TAGS = {
all: () => true,
@@ -25,12 +25,13 @@ export const PERSONALIZATION_TAGS = {
loggedin: () => !!window.adobeIMS?.isSignedInUser(),
};
const PERSONALIZATION_KEYS = Object.keys(PERSONALIZATION_TAGS);
-/* c20 ignore stop */
+/* c8 ignore stop */
const CLASS_EL_DELETE = 'p13n-deleted';
const CLASS_EL_REPLACE = 'p13n-replaced';
const COLUMN_NOT_OPERATOR = 'not';
const TARGET_EXP_PREFIX = 'target-';
+const INLINE_HASH = '_inline';
const PAGE_URL = new URL(window.location.href);
export const TRACKED_MANIFEST_TYPE = 'personalization';
@@ -51,11 +52,11 @@ const DATA_TYPE = {
TEXT: 'text',
};
-export const CUSTOM_SELECTOR_PREFIX = 'in-block:';
+const IN_BLOCK_SELECTOR_PREFIX = 'in-block:';
export const appendJsonExt = (path) => (path.endsWith('.json') ? path : `${path}.json`);
-export const normalizePath = (p) => {
+export const normalizePath = (p, localize = true) => {
let path = p;
if (!path?.includes('/')) {
@@ -70,7 +71,11 @@ export const normalizePath = (p) => {
try {
const url = new URL(path);
const firstFolder = url.pathname.split('/')[1];
- if (config.locale.ietf === 'en-US' || url.hash === '#_dnt' || firstFolder in config.locales || path.includes('.json')) {
+ if (!localize
+ || config.locale.ietf === 'en-US'
+ || url.hash.includes('#_dnt')
+ || firstFolder in config.locales
+ || path.includes('.json')) {
path = url.pathname;
} else {
path = `${config.locale.prefix}${url.pathname}`;
@@ -156,7 +161,7 @@ const COMMANDS = {
};
function checkSelectorType(selector) {
- return selector?.includes('/fragments/') ? 'fragment' : 'css';
+ return selector?.startsWith('/') || selector?.startsWith('http') ? 'fragment' : 'css';
}
const fetchData = async (url, type = DATA_TYPE.JSON) => {
@@ -291,13 +296,44 @@ function getSection(rootEl, idx) {
: rootEl.querySelector(`:scope > div:nth-child(${idx})`);
}
-function registerCustomAction(cmd, manifestId) {
- const { action, selector, target } = cmd;
+function registerInBlockActions(cmd, manifestId) {
+ const { action, target, selector } = cmd;
+ const command = { action, target, manifestId };
+
+ const blockAndSelector = selector.substring(IN_BLOCK_SELECTOR_PREFIX.length).trim().split(/\s+/);
+ const [blockName] = blockAndSelector;
+
const config = getConfig();
- const blockName = selector.substring(CUSTOM_SELECTOR_PREFIX.length);
- config.mep.custom ??= {};
- config.mep.custom[blockName] ??= [];
- config.mep.custom[blockName].push({ manifestId, action, target });
+ config.mep.inBlock ??= {};
+ config.mep.inBlock[blockName] ??= {};
+
+ let blockSelector;
+ if (blockAndSelector.length > 1) {
+ blockSelector = blockAndSelector.slice(1).join(' ');
+ command.selector = blockSelector;
+ if (checkSelectorType(blockSelector) === 'fragment') {
+ config.mep.inBlock[blockName].fragments ??= {};
+ const { fragments } = config.mep.inBlock[blockName];
+ delete command.selector;
+ if (blockSelector in fragments) return;
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in fragments) {
+ if (fragments[key].target === blockSelector) fragments[key] = command;
+ }
+ fragments[blockSelector] = command;
+
+ blockSelector = normalizePath(blockSelector);
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in fragments) {
+ if (fragments[key].target === blockSelector) fragments[key] = command;
+ }
+ fragments[blockSelector] = command;
+ return;
+ }
+ }
+ config.mep.inBlock[blockName].commands ??= [];
+ config.mep.inBlock[blockName].commands.push(command);
}
function getSelectedElement(selector, action, rootEl) {
@@ -309,7 +345,7 @@ function getSelectedElement(selector, action, rootEl) {
}
if (checkSelectorType(selector) === 'fragment') {
try {
- const fragment = document.querySelector(`a[href*="${normalizePath(selector)}"]`);
+ const fragment = document.querySelector(`a[href*="${normalizePath(selector, false)}"], a[href*="${normalizePath(selector, true)}"]`);
if (fragment) return fragment.parentNode;
return null;
} catch (e) {
@@ -364,19 +400,34 @@ function getSelectedElement(selector, action, rootEl) {
return selectedEl;
}
-function handleCommands(commands, manifestId, rootEl = document) {
+const addHash = (url, newHash) => {
+ if (!newHash) return url;
+ try {
+ const { origin, pathname, search } = new URL(url);
+ return `${origin}${pathname}${search}#${newHash}`;
+ } catch (e) {
+ return `${url}#${newHash}`;
+ }
+};
+
+export function handleCommands(commands, manifestId, rootEl = document, forceInline = false) {
commands.forEach((cmd) => {
- const { action, selector, target } = cmd;
- if (selector.startsWith(CUSTOM_SELECTOR_PREFIX)) {
- registerCustomAction(cmd, manifestId);
+ const { action, selector, target: trgt } = cmd;
+ const target = forceInline ? addHash(trgt, INLINE_HASH) : trgt;
+ if (selector.startsWith(IN_BLOCK_SELECTOR_PREFIX)) {
+ registerInBlockActions(cmd, manifestId);
return;
}
+
if (action in COMMANDS) {
const el = getSelectedElement(selector, action, rootEl);
COMMANDS[action](el, target, manifestId);
} else if (action in CREATE_CMDS) {
const el = getSelectedElement(selector, action, rootEl);
- el?.insertAdjacentElement(CREATE_CMDS[action], createFrag(el, target, manifestId));
+ el?.insertAdjacentElement(
+ CREATE_CMDS[action],
+ createFrag(el, target, manifestId),
+ );
} else {
/* c8 ignore next 2 */
console.log('Invalid command found: ', cmd);
@@ -638,8 +689,8 @@ export async function getPersConfig(info, override = false) {
return config;
}
-const deleteMarkedEls = () => {
- [...document.querySelectorAll(`.${CLASS_EL_DELETE}`)]
+export const deleteMarkedEls = (rootEl = document) => {
+ [...rootEl.querySelectorAll(`.${CLASS_EL_DELETE}`)]
.forEach((el) => el.remove());
};
@@ -761,7 +812,7 @@ export async function applyPers(manifests) {
const config = getConfig();
if (!manifests?.length) return;
- if (!config?.mep) config.mep = {};
+ config.mep ??= {};
config.mep.handleFragmentCommand = handleFragmentCommand;
let experiments = manifests;
for (let i = 0; i < experiments.length; i += 1) {
diff --git a/libs/features/personalization/preview.css b/libs/features/personalization/preview.css
index b2dbdc78af..a189de4d5e 100644
--- a/libs/features/personalization/preview.css
+++ b/libs/features/personalization/preview.css
@@ -274,12 +274,12 @@ input#new-manifest {
}
/* mepHighlight */
-body[data-mep-highlight='true'] [data-manifest-id],
+body[data-mep-highlight='true'] [data-manifest-id]:not(section.feds-navItem),
+body[data-mep-highlight='true'] section.feds-navItem[data-manifest-id] > *,
body[data-mep-highlight='true'] [data-code-manifest-id],
body[data-mep-highlight='true'] [data-removed-manifest-id] {
outline: 3px #26ceef dashed !important;
box-shadow: 3px 3px 13px 0 #8f8f8f !important;
- position: relative;
}
body[data-mep-highlight='true'] [data-code-manifest-id] {
diff --git a/test/blocks/global-navigation/mocks/mep-config.js b/test/blocks/global-navigation/mocks/mep-config.js
new file mode 100644
index 0000000000..73ee0bd113
--- /dev/null
+++ b/test/blocks/global-navigation/mocks/mep-config.js
@@ -0,0 +1,21 @@
+export default {
+ inBlock: {
+ 'global-navigation': {
+ commands: [
+ {
+ action: 'replace',
+ target: '/test/blocks/global-navigation/mocks/mep-large-menu-table',
+ manifestId: 'manifest.json',
+ selector: '.large-menu',
+ },
+ ],
+ fragments: {
+ '/old/navigation': {
+ action: 'replace',
+ target: '/test/blocks/global-navigation/mocks/mep-global-navigation',
+ manifestId: 'manifest.json',
+ },
+ },
+ },
+ },
+};
diff --git a/test/blocks/global-navigation/mocks/mep-global-navigation.plain.html b/test/blocks/global-navigation/mocks/mep-global-navigation.plain.html
new file mode 100644
index 0000000000..18f2bd0ae4
--- /dev/null
+++ b/test/blocks/global-navigation/mocks/mep-global-navigation.plain.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/blocks/global-navigation/mocks/mep-large-menu-table b/test/blocks/global-navigation/mocks/mep-large-menu-table
new file mode 100644
index 0000000000..e788bb9460
--- /dev/null
+++ b/test/blocks/global-navigation/mocks/mep-large-menu-table
@@ -0,0 +1,9 @@
+
+
+
diff --git a/test/blocks/global-navigation/test-utilities.js b/test/blocks/global-navigation/test-utilities.js
index 44d398cd6a..bbc98f0bc8 100644
--- a/test/blocks/global-navigation/test-utilities.js
+++ b/test/blocks/global-navigation/test-utilities.js
@@ -66,10 +66,10 @@ export const analyticsTestData = {
'app-switcher|click|footer|all-apps': 'AppLauncher.allapps',
'app-switcher|click|footer|see-all-apps': 'AppLauncher.allapps',
'app-switcher|render|component': 'AppLauncher.appIconToggle',
- 'profile|click|account': 'View Account|gnav|milo',
+ 'profile|click|account': 'View Account|gnav|milo|unav',
'profile|click|sign-in': 'Sign In|gnav|milo|unav',
'profile|click|sign-out': 'Sign Out|gnav|milo|unav',
- 'profile|render|component': 'Account|gnav|milo',
+ 'profile|render|component': 'Account|gnav|milo|unav',
'unc|click|dismiss': 'Dismiss Notifications',
'unc|click|icon': 'Open Notifications panel',
'unc|click|link': 'Open Notification',
diff --git a/test/blocks/global-navigation/utilities/utilities.test.js b/test/blocks/global-navigation/utilities/utilities.test.js
index 3fec40a1ed..12751dbd57 100644
--- a/test/blocks/global-navigation/utilities/utilities.test.js
+++ b/test/blocks/global-navigation/utilities/utilities.test.js
@@ -1,6 +1,7 @@
import { expect } from '@esm-bundle/chai';
import sinon from 'sinon';
import {
+ fetchAndProcessPlainHtml,
toFragment,
getFedsPlaceholderConfig,
federatePictureSources,
@@ -16,14 +17,27 @@ import {
logErrorFor,
getFederatedUrl,
} from '../../../../libs/blocks/global-navigation/utilities/utilities.js';
-import { setConfig } from '../../../../libs/utils/utils.js';
+import { setConfig, getConfig } from '../../../../libs/utils/utils.js';
import { createFullGlobalNavigation, config } from '../test-utilities.js';
+import mepInBlock from '../mocks/mep-config.js';
const baseHost = 'https://www.stage.adobe.com';
describe('global navigation utilities', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
+ it('fetchAndProcessPlainHtml with MEP', () => {
+ expect(fetchAndProcessPlainHtml).to.exist;
+ const mepConfig = getConfig();
+ mepConfig.mep = mepInBlock;
+ fetchAndProcessPlainHtml({ url: '/old/navigation' }).then((fragment) => {
+ const inNewMenu = fragment.querySelector('#only-in-new-menu');
+ expect(inNewMenu).to.exist;
+ const newMenu = fragment.querySelector('a[href*="mep-large-menu-table"]');
+ expect(newMenu).to.exist;
+ });
+ });
+
it('toFragment', () => {
expect(toFragment).to.exist;
const fragment = toFragment`test
`;
diff --git a/test/blocks/icon-block/icon-block.test.js b/test/blocks/icon-block/icon-block.test.js
index 4f0f8241f2..5a1a42ba1b 100644
--- a/test/blocks/icon-block/icon-block.test.js
+++ b/test/blocks/icon-block/icon-block.test.js
@@ -43,4 +43,14 @@ describe('icon blocks', () => {
expect(heading).to.not.exist;
});
});
+ describe('cta container', () => {
+ it('is added around the only action area', () => {
+ expect(document.querySelector('.cta-container #one-cta')).to.exist;
+ });
+ it('is added around adjacent action areas', () => {
+ const parent = document.querySelector('#adjacent-cta-1').parentElement;
+ expect(parent.className.includes('cta-container')).to.be.true;
+ expect(parent.querySelector('#adjacent-cta-2')).to.exist;
+ });
+ });
});
diff --git a/test/blocks/icon-block/mocks/body.html b/test/blocks/icon-block/mocks/body.html
index 5061440e60..d3526ba7c5 100644
--- a/test/blocks/icon-block/mocks/body.html
+++ b/test/blocks/icon-block/mocks/body.html
@@ -25,8 +25,8 @@ Heading XL Bold Icon Block
Heading L Bold Icon Block
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.
- Learn more
- Body M BOLD Text link
+ Learn more
+ Body M BOLD Text link
@@ -42,7 +42,7 @@ Heading L Bold Icon Block
Heading M Bold 2-up Lorem ipsum sit amet
Body M Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.
- Learn more Body XS Text link
+ Learn more Body XS Text link
diff --git a/test/blocks/marquee/marquee.test.js b/test/blocks/marquee/marquee.test.js
index e0bf2acb9c..5874bd6bd5 100644
--- a/test/blocks/marquee/marquee.test.js
+++ b/test/blocks/marquee/marquee.test.js
@@ -14,6 +14,7 @@ const { default: init } = await import('../../../libs/blocks/marquee/marquee.js'
const { default: videoBLock } = await import('../../../libs/blocks/video/video.js');
const video = await readFile({ path: './mocks/video.html' });
const multipleIcons = await readFile({ path: './mocks/multiple-icons.html' });
+
describe('marquee', () => {
const marquees = document.querySelectorAll('.marquee');
marquees.forEach((marquee) => {
@@ -98,13 +99,13 @@ describe('marquee', () => {
it('using img', () => {
const marquee = document.getElementById('using-images');
init(marquee);
- expect(marquee.querySelector('.icon-area-multiple')).to.exist;
+ expect(marquee.querySelector('[icon-count]')).to.exist;
});
it('using svg', () => {
const marquee = document.getElementById('using-svgs');
init(marquee);
- expect(marquee.querySelector('.icon-area-multiple')).to.exist;
+ expect(marquee.querySelector('[icon-count]')).to.exist;
});
});
diff --git a/test/blocks/marquee/mocks/multiple-icons.html b/test/blocks/marquee/mocks/multiple-icons.html
index 33085e3701..9ecba85023 100644
--- a/test/blocks/marquee/mocks/multiple-icons.html
+++ b/test/blocks/marquee/mocks/multiple-icons.html
@@ -54,10 +54,18 @@ Using svgs
- https://www.adobe.com/content/dam/cc/icons/md_appicon_256.svg | 3D Modeler
- https://substance3d.adobe.com/magazine/wp-content/uploads/2021/06/sb_assets_appicon_256.svg | 3D Assets
- https://www.adobe.com/content/dam/cc/icons/md_appicon_256.svg | 3D Modeler
- https://substance3d.adobe.com/magazine/wp-content/uploads/2021/06/sb_assets_appicon_256.svg | 3D Assets
+
+
+
+
+
+
+
+
+
+
+
+
Detail (optional)
Heading Lorem ipsum dolor sit amet consect.
diff --git a/test/blocks/media/media.test.js b/test/blocks/media/media.test.js
index 9b662fedd5..9d79e7e2ae 100644
--- a/test/blocks/media/media.test.js
+++ b/test/blocks/media/media.test.js
@@ -28,6 +28,10 @@ describe('media', () => {
const blueButton = actionArea.querySelector('.con-button.blue');
expect(blueButton).to.exist;
});
+ it('has a cta container', () => {
+ const ctaArea = medias[0].querySelector('.cta-container .action-area');
+ expect(ctaArea).to.exist;
+ });
});
describe('dark media large', () => {
it('has a heading-xl', () => {
@@ -54,6 +58,10 @@ describe('media', () => {
const links = medias[4].querySelectorAll('h3.heading-xs ~ p.subcopy-link > a');
expect(links.length).to.greaterThanOrEqual(2);
});
+ it('does not have cta container around mid-body action area', () => {
+ const actionArea = medias[4].querySelector('.action-area');
+ expect(actionArea.parentElement.className.includes('cta-container')).to.be.false;
+ });
});
describe('media with qr-code', () => {
it('does have qr-code image', () => {
@@ -104,4 +112,11 @@ describe('media', () => {
expect(iconStack).to.exist;
});
});
+ describe('with merch variant', () => {
+ it('has a cta container around the icon stack and action area', () => {
+ const cta = medias[7].querySelector('.cta-container');
+ expect(cta.querySelector('.icon-stack-area')).to.exist;
+ expect(cta.querySelector('.action-area')).to.exist;
+ });
+ });
});
diff --git a/test/blocks/media/mocks/body.html b/test/blocks/media/mocks/body.html
index 63345703a0..695436456d 100644
--- a/test/blocks/media/mocks/body.html
+++ b/test/blocks/media/mocks/body.html
@@ -141,4 +141,33 @@
+
diff --git a/test/blocks/merch-card-collection/merch-card-collection.test.js b/test/blocks/merch-card-collection/merch-card-collection.test.js
index 6a3313a0e5..59b413eae4 100644
--- a/test/blocks/merch-card-collection/merch-card-collection.test.js
+++ b/test/blocks/merch-card-collection/merch-card-collection.test.js
@@ -165,19 +165,21 @@ describe('Merch Cards', async () => {
...conf,
mep: {
preview: true,
- custom: {
- 'merch-card-collection': [
- {
- action: 'replace',
- manifestId: 'promo1.json',
- target: '/override-photoshop',
- },
- {
- action: 'replace',
- manifestId: 'promo2.json',
- target: '/override-express',
- },
- ],
+ inBlock: {
+ 'merch-card-collection': {
+ commands: [
+ {
+ action: 'replace',
+ manifestId: 'promo1.json',
+ target: '/override-photoshop',
+ },
+ {
+ action: 'replace',
+ manifestId: 'promo2.json',
+ target: '/override-express',
+ },
+ ],
+ },
},
},
});
diff --git a/test/blocks/share/mocks/body.html b/test/blocks/share/mocks/body.html
index 0587a56ebb..6548ac1178 100644
--- a/test/blocks/share/mocks/body.html
+++ b/test/blocks/share/mocks/body.html
@@ -43,6 +43,13 @@
+
+
+
diff --git a/test/blocks/share/share.test.js b/test/blocks/share/share.test.js
index 2849bbf840..6492ac8b3c 100644
--- a/test/blocks/share/share.test.js
+++ b/test/blocks/share/share.test.js
@@ -61,6 +61,12 @@ describe('Share', () => {
expect(re).to.exist;
expect(tw).to.not.exist;
});
+ it('Share w/ custom title exists', async () => {
+ const shareEl = document.querySelector('.share.title');
+ await init(shareEl);
+ const p = shareEl.querySelector('.tracking-header p');
+ expect(p).to.exist;
+ });
it('Inline variant (with inline siblings) creates an inline-wrapper element', async () => {
const section = document.querySelector('.section.inline-has-siblings');
const shareEls = section.querySelectorAll('.share.inline');
diff --git a/test/blocks/text/mocks/body.html b/test/blocks/text/mocks/body.html
index 210bd8587f..f6ae6bb7ac 100644
--- a/test/blocks/text/mocks/body.html
+++ b/test/blocks/text/mocks/body.html
@@ -185,3 +185,15 @@
Tablet, Desktop
+
+
diff --git a/test/blocks/text/text.test.js b/test/blocks/text/text.test.js
index de4f16ee13..1db68d1d13 100644
--- a/test/blocks/text/text.test.js
+++ b/test/blocks/text/text.test.js
@@ -62,11 +62,20 @@ describe('text block', () => {
});
});
});
-
describe('two content rows', () => {
it('has viewport classes', () => {
const mobileEl = document.querySelector('.text-block .mobile-up');
expect(mobileEl).to.exist;
});
});
+ describe('cta container', () => {
+ it('is added around the last action area', () => {
+ const actionArea = document.querySelector('#has-container');
+ expect(actionArea.parentElement.className.includes('cta-container')).to.be.true;
+ });
+ it('is not added around action areas that are not last', () => {
+ const actionArea = document.querySelector('#no-container');
+ expect(actionArea.parentElement.className.includes('cta-container')).to.be.false;
+ });
+ });
});
diff --git a/test/features/personalization/actions.test.js b/test/features/personalization/actions.test.js
index 98242e2f33..9587fd76c2 100644
--- a/test/features/personalization/actions.test.js
+++ b/test/features/personalization/actions.test.js
@@ -235,13 +235,33 @@ describe('custom actions', async () => {
setFetchResponse(manifestJson);
await applyPers([{ manifestPath: '/path/to/manifest.json' }]);
- expect(getConfig().mep.custom).to.deep.equal({
- 'my-block': [{
- action: 'replace',
- manifestId: 'manifest.json',
- target: '/fragments/fragmentreplaced',
+ console.log(getConfig().mep.inBlock);
+ expect(getConfig().mep.inBlock).to.deep.equal({
+ 'my-block': {
+ commands: [{
+ action: 'replace',
+ target: '/fragments/fragmentreplaced',
+ manifestId: 'manifest.json',
+ },
+ {
+ action: 'replace',
+ target: '/fragments/new-large-menu',
+ manifestId: 'manifest.json',
+ selector: '.large-menu',
+ }],
+ fragments: {
+ '/fragments/sub-menu': {
+ action: 'replace',
+ target: '/fragments/even-more-new-sub-menu',
+ manifestId: 'manifest.json',
+ },
+ '/fragments/new-sub-menu': {
+ action: 'replace',
+ target: '/fragments/even-more-new-sub-menu',
+ manifestId: 'manifest.json',
+ },
+ },
},
- ],
});
});
});
diff --git a/test/features/personalization/mocks/actions/manifestCustomAction.json b/test/features/personalization/mocks/actions/manifestCustomAction.json
index bb7fec274a..fafaccbe56 100644
--- a/test/features/personalization/mocks/actions/manifestCustomAction.json
+++ b/test/features/personalization/mocks/actions/manifestCustomAction.json
@@ -12,6 +12,35 @@
"firefox": "",
"android": "",
"ios": ""
+ },
+ {
+ "action": "replace",
+ "selector": "in-block:my-block .large-menu",
+ "page filter (optional)": "",
+ "param-newoffer=123": "",
+ "chrome": "/fragments/new-large-menu",
+ "firefox": "",
+ "android": "",
+ "ios": ""
+ },
+ {
+ "action": "replace",
+ "selector": "in-block:my-block /fragments/sub-menu",
+ "page filter (optional)": "",
+ "param-newoffer=123": "",
+ "chrome": "/fragments/new-sub-menu",
+ "firefox": "",
+ "android": "",
+ "ios": ""
+ }, {
+ "action": "replace",
+ "selector": "in-block:my-block /fragments/new-sub-menu",
+ "page filter (optional)": "",
+ "param-newoffer=123": "",
+ "chrome": "/fragments/even-more-new-sub-menu",
+ "firefox": "",
+ "android": "",
+ "ios": ""
}
],
":type": "sheet"
diff --git a/test/features/personalization/mocks/actions/manifestUpdateMetadata.json b/test/features/personalization/mocks/actions/manifestUpdateMetadata.json
index cce9f7c1fa..1d0ff51688 100644
--- a/test/features/personalization/mocks/actions/manifestUpdateMetadata.json
+++ b/test/features/personalization/mocks/actions/manifestUpdateMetadata.json
@@ -7,29 +7,25 @@
"action": "updateMetadata",
"selector": "georouting",
"page filter (optional)": "",
- "param-newoffer=123": "",
- "chrome": "on"
+ "all": "on"
},
{
"action": "updateMetadata",
"selector": "mynewmetadata",
"page filter (optional)": "",
- "param-newoffer=123": "",
- "chrome": "woot"
+ "all": "woot"
},
{
"action": "updateMetadata",
"selector": "og:title",
"page filter (optional)": "",
- "param-newoffer=123": "",
- "chrome": "New Title"
+ "all": "New Title"
},
{
"action": "updateMetadata",
"selector": "og:image",
"page filter (optional)": "",
- "param-newoffer=123": "",
- "chrome": "https://adobe.com/path/to/image.jpg"
+ "all": "https://adobe.com/path/to/image.jpg"
}
],
":type": "sheet"
diff --git a/test/features/personalization/mocks/manifestInvalid.json b/test/features/personalization/mocks/manifestInvalid.json
index f39166d881..aaad833f20 100644
--- a/test/features/personalization/mocks/manifestInvalid.json
+++ b/test/features/personalization/mocks/manifestInvalid.json
@@ -32,6 +32,26 @@
"firefox": "",
"android": "",
"ios": ""
+ },
+ {
+ "action": "insertAfter",
+ "selector": "/fragments/hello-world",
+ "page filter (optional)": "",
+ "param-newoffer=123": "",
+ "chrome": "/test/features/personalization/mocks/fragments/insertafter3",
+ "firefox": "",
+ "android": "",
+ "ios": ""
+ },
+ {
+ "action": "insertAfter",
+ "selector": "http\"",
+ "page filter (optional)": "",
+ "param-newoffer=123": "",
+ "chrome": "/test/features/personalization/mocks/fragments/insertafter3",
+ "firefox": "",
+ "android": "",
+ "ios": ""
}
],
":type": "sheet"
diff --git a/test/features/personalization/normalizePath.test.js b/test/features/personalization/normalizePath.test.js
index 6b62a5a734..7fbef4e2ce 100644
--- a/test/features/personalization/normalizePath.test.js
+++ b/test/features/personalization/normalizePath.test.js
@@ -8,6 +8,11 @@ describe('normalizePath function', () => {
ietf: 'en-US',
prefix: '',
};
+ it('add forward slash when needed', async () => {
+ const path = await normalizePath('path/fragment.plain.html');
+ expect(path).to.equal('/path/fragment.plain.html');
+ });
+
it('does not localize for US page', async () => {
const path = await normalizePath('https://main--milo--adobecom.hlx.page/path/to/fragment.plain.html');
expect(path).to.equal('/path/to/fragment.plain.html');
diff --git a/test/features/personalization/personalization.test.js b/test/features/personalization/personalization.test.js
index 771e847619..4f3e84d1ed 100644
--- a/test/features/personalization/personalization.test.js
+++ b/test/features/personalization/personalization.test.js
@@ -151,6 +151,26 @@ describe('Functional Test', () => {
window.console.log.reset();
});
+ it('updateMetadata should be able to add and change metadata', async () => {
+ let manifestJson = await readFile({ path: './mocks/actions/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');
+ });
+
it('should override to param-newoffer=123', async () => {
let config = getConfig();
config.mep = { override: '/path/to/manifest.json--param-newoffer=123' };