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 @@ + +

+ +
+
+
+
+
+

Creative Cloud

+
+
+
+
+
+
+
+
+

PDF & E-signatures

+
+
+
+
+
+
+
+
+
+
+
+ buy-now +
+
+ Buy Now +
+
+
+
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 @@ +
+
+
+
+

MEP Large Menu Table Title

+
+
+
+
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 + + 3D Modeler + + + 3D Assets + + + 3D Modeler + + + 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 @@

Heading M 24/30 Media

+
+
+
+ + + +
+
+

Detail M 12/15

+

Heading XS 18/22 Media (merch, small)

+

Body S 16/24 Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.

+ +

Learn More Watch the Video

+
+
+
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 @@ + +
+
+
Custom Title
+
+
+
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

+ +
+
+
+

Text

+

Section Metadata - Two-Up

+

Learn about

+

Kick things off with hundreds of premium and free presets you can access with your Lightroom subscription.

+

Learn about Body XS 14/21 Text link

+
+
+
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' };