diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml index 3267848caa..9b1e960515 100644 --- a/.github/workflows/dispatch.yml +++ b/.github/workflows/dispatch.yml @@ -17,6 +17,7 @@ jobs: - uses: dorny/paths-filter@v2 id: changes with: + base: ${{ github.ref }} filters: | src: - 'libs/**' diff --git a/libs/blocks/marquee/marquee.css b/libs/blocks/marquee/marquee.css index 5ea0ed0e54..e97ca4a1a2 100644 --- a/libs/blocks/marquee/marquee.css +++ b/libs/blocks/marquee/marquee.css @@ -432,6 +432,10 @@ padding: var(--spacing-xl) 0; } + html[dir="rtl"] .marquee.split .foreground.container { + flex-direction: row-reverse; + } + .marquee.split .foreground.container .text { max-width: calc(50% - var(--grid-column-width)); } @@ -472,6 +476,10 @@ justify-content: flex-end; } + html[dir="rtl"] .marquee.split.row-reversed .foreground.container { + justify-content: flex-start; + } + .marquee.split .asset img, .marquee.split.small .asset img, .marquee.split.large .asset img, diff --git a/libs/blocks/marquee/marquee.js b/libs/blocks/marquee/marquee.js index 2bbf3a103c..2889bced45 100644 --- a/libs/blocks/marquee/marquee.js +++ b/libs/blocks/marquee/marquee.js @@ -86,7 +86,7 @@ function decorateSplit(el, foreground, media) { let mediaCreditInner; const txtContent = media?.lastChild?.textContent?.trim(); - if (txtContent.match(/^http.*\.mp4/)) return; + if (txtContent?.match(/^http.*\.mp4/)) return; if (txtContent) { mediaCreditInner = createTag('p', { class: 'body-s' }, txtContent); } else if (media.lastElementChild?.tagName !== 'PICTURE') { diff --git a/libs/blocks/merch-card/merch-card.js b/libs/blocks/merch-card/merch-card.js index 443a651c64..603d8c74ea 100644 --- a/libs/blocks/merch-card/merch-card.js +++ b/libs/blocks/merch-card/merch-card.js @@ -1,5 +1,5 @@ import { decorateButtons, decorateBlockHrs } from '../../utils/decorate.js'; -import { getConfig, createTag } from '../../utils/utils.js'; +import { getConfig, createTag, loadStyle } from '../../utils/utils.js'; import { getMetadata } from '../section-metadata/section-metadata.js'; import { processTrackingLabels } from '../../martech/attributes.js'; import { replaceKey } from '../../features/placeholders.js'; @@ -9,6 +9,8 @@ const TAG_PATTERN = /^[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-].*$/; const CARD_TYPES = ['segment', 'special-offers', 'plans', 'catalog', 'product', 'inline-heading', 'image', 'mini-compare-chart']; +const CARD_SIZES = ['wide', 'super-wide']; + const TEXT_STYLES = { H5: 'detail-m', H4: 'body-xxs', @@ -52,10 +54,21 @@ const appendSlot = (slotEls, slotName, merchCard) => { merchCard.append(newEl); }; -const parseContent = (el, merchCard) => { - const innerElements = [ - ...el.querySelectorAll('h2, h3, h4, h5, p, ul, em'), - ]; +export async function loadMnemonicList(foreground) { + try { + const { base } = getConfig(); + const stylePromise = new Promise((resolve) => { + loadStyle(`${base}/blocks/mnemonic-list/mnemonic-list.css`, resolve); + }); + const loadModule = import(`${base}/blocks/mnemonic-list/mnemonic-list.js`) + .then(({ decorateMnemonicList }) => decorateMnemonicList(foreground)); + await Promise.all([stylePromise, loadModule]); + } catch (err) { + window.lana?.log(`Failed to load mnemonic list module: ${err}`); + } +} + +const parseContent = async (el, merchCard) => { let bodySlotName = `body-${merchCard.variant !== MINI_COMPARE_CHART ? 'xs' : 'm'}`; let headingMCount = 0; @@ -69,7 +82,13 @@ const parseContent = (el, merchCard) => { let headingSize = 3; const bodySlot = createTag('div', { slot: bodySlotName }); - + const mnemonicList = el.querySelector('.mnemonic-list'); + if (mnemonicList) { + await loadMnemonicList(mnemonicList); + } + const innerElements = [ + ...el.querySelectorAll('h2, h3, h4, h5, p, ul, em'), + ]; innerElements.forEach((element) => { let { tagName } = element; if (isHeadingTag(tagName)) { @@ -103,6 +122,7 @@ const parseContent = (el, merchCard) => { bodySlot.append(element); merchCard.append(bodySlot); } + if (mnemonicList) bodySlot.append(mnemonicList); }); if (merchCard.variant === MINI_COMPARE_CHART && merchCard.childNodes[1]) { @@ -283,6 +303,7 @@ const init = async (el) => { } const merchCard = createTag('merch-card', { class: styles.join(' '), 'data-block': '' }); merchCard.setAttribute('variant', cardType); + merchCard.setAttribute('size', styles.find((style) => CARD_SIZES.includes(style)) || ''); if (el.dataset.removedManifestId) { merchCard.dataset.removedManifestId = el.dataset.removedManifestId; } @@ -322,10 +343,11 @@ const init = async (el) => { intersectionObserver.observe(merchCard); footerRows = getMiniCompareChartFooterRows(el); } - const images = el.querySelectorAll('picture'); + const allPictures = el.querySelectorAll('picture'); + const pictures = Array.from(allPictures).filter((picture) => !picture.closest('.mnemonic-list')); let image; const icons = []; - images.forEach((img) => { + pictures.forEach((img) => { const imgNode = img.querySelector('img'); const { width, height } = imgNode; const isSquare = Math.abs(width - height) <= 10; @@ -416,7 +438,6 @@ const init = async (el) => { } } } - decorateBlockHrs(merchCard); simplifyHrs(merchCard); if (merchCard.classList.contains('has-divider')) { diff --git a/libs/blocks/mnemonic-list/mnemonic-list.css b/libs/blocks/mnemonic-list/mnemonic-list.css index 496215db6c..514fa87252 100644 --- a/libs/blocks/mnemonic-list/mnemonic-list.css +++ b/libs/blocks/mnemonic-list/mnemonic-list.css @@ -2,6 +2,15 @@ margin: var(--spacing-xs) 0 ; } +.merch-card .mnemonic-list { + padding: var(--consonant-merch-spacing-xs) 0; +} + +.merch-card .mnemonic-list .product-list { + justify-content: flex-start; + align-items: center; +} + .mnemonic-list .product-list { display: inline-flex; flex-wrap: wrap; @@ -22,6 +31,7 @@ flex: none; gap: var(--spacing-xxs); min-height: 32px; + line-height: initial; } .mnemonic-list .product-list .product-item strong { diff --git a/libs/blocks/mnemonic-list/mnemonic-list.js b/libs/blocks/mnemonic-list/mnemonic-list.js index 931e62e8ab..18fdd9b78f 100644 --- a/libs/blocks/mnemonic-list/mnemonic-list.js +++ b/libs/blocks/mnemonic-list/mnemonic-list.js @@ -1,6 +1,6 @@ import { createTag } from '../../utils/utils.js'; -export const decorateMnemonicList = (container) => { +export const decorateMnemonicList = async (container) => { const mnemonicListElement = container.querySelector('.mnemonic-list'); const targetElement = mnemonicListElement || container; const rows = targetElement.querySelectorAll(':scope p:not([class])'); diff --git a/libs/blocks/tabs/tabs.js b/libs/blocks/tabs/tabs.js index d25f39a2b3..55a5cf62db 100644 --- a/libs/blocks/tabs/tabs.js +++ b/libs/blocks/tabs/tabs.js @@ -2,9 +2,10 @@ * tabs - consonant v6 * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role */ +import { debounce } from '../../utils/action.js'; import { createTag, MILO_EVENTS } from '../../utils/utils.js'; -const RIGHT_PADDLE = '<svg viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.50001 13.25C1.22022 13.25 0.939945 13.1431 0.726565 12.9292C0.299315 12.5019 0.299315 11.8096 0.726565 11.3823L5.10938 7L0.726565 2.61768C0.299315 2.19043 0.299315 1.49805 0.726565 1.0708C1.15333 0.643068 1.84669 0.643068 2.27345 1.0708L7.4297 6.22656C7.63478 6.43164 7.75001 6.70996 7.75001 7C7.75001 7.29004 7.63478 7.56836 7.4297 7.77344L2.27345 12.9292C2.06007 13.1431 1.7798 13.2495 1.50001 13.25Z" fill="currentColor"/></svg>'; +const PADDLE = '<svg viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.50001 13.25C1.22022 13.25 0.939945 13.1431 0.726565 12.9292C0.299315 12.5019 0.299315 11.8096 0.726565 11.3823L5.10938 7L0.726565 2.61768C0.299315 2.19043 0.299315 1.49805 0.726565 1.0708C1.15333 0.643068 1.84669 0.643068 2.27345 1.0708L7.4297 6.22656C7.63478 6.43164 7.75001 6.70996 7.75001 7C7.75001 7.29004 7.63478 7.56836 7.4297 7.77344L2.27345 12.9292C2.06007 13.1431 1.7798 13.2495 1.50001 13.25Z" fill="currentColor"/></svg>'; const isTabInTabListView = (tab) => { const tabList = tab.closest('[role="tablist"]'); @@ -24,6 +25,14 @@ const scrollTabIntoView = (e, inline = 'center') => { if (!isElInView) e.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline }); }; +const setAttributes = (el, attrs) => { + Object.keys(attrs).forEach((key) => el.setAttribute(key, attrs[key])); +}; + +const removeAttributes = (el, attrsKeys) => { + attrsKeys.forEach((key) => el.removeAttribute(key)); +}; + function changeTabs(e) { const { target } = e; const parent = target.parentNode; @@ -128,6 +137,13 @@ function initPaddles(tabList, tabPaddles) { } }); + tabList.addEventListener('scroll', debounce(() => { + tabPaddles.setAttribute( + 'aria-valuenow', + ((tabList.scrollLeft / (tabList.scrollWidth - tabList.clientWidth)) * 100).toFixed(0), + ); + }, 500)); + const options = { root: tabList, rootMargin: '0px', @@ -138,15 +154,15 @@ function initPaddles(tabList, tabPaddles) { entries.forEach((entry) => { if (entry.target === firstTab) { if (entry.isIntersecting) { - left.setAttribute('disabled', ''); + setAttributes(left, { disabled: '', 'aria-hidden': true }); } else { - left.removeAttribute('disabled'); + removeAttributes(left, ['disabled', 'aria-hidden']); } } else if (entry.target === lastTab) { if (entry.isIntersecting) { - right.setAttribute('disabled', ''); + setAttributes(right, { disabled: '', 'aria-hidden': true }); } else { - right.removeAttribute('disabled'); + removeAttributes(right, ['disabled', 'aria-hidden']); } } }); @@ -243,9 +259,9 @@ const init = (block) => { } // Tab Paddles - const tabPaddles = createTag('div', { class: 'tab-paddles', role: 'scrollbar' }); - const paddleLeft = createTag('button', { class: 'paddle paddle-left', disabled: '' }, RIGHT_PADDLE); - const paddleRight = createTag('button', { class: 'paddle paddle-right', disabled: '' }, RIGHT_PADDLE); + const tabPaddles = createTag('div', { class: 'tab-paddles', role: 'scrollbar', 'aria-valuenow': 0 }); + const paddleLeft = createTag('button', { class: 'paddle paddle-left', disabled: '', 'aria-hidden': true, 'aria-label': 'Scroll tabs to left' }, PADDLE); + const paddleRight = createTag('button', { class: 'paddle paddle-right', disabled: '', 'aria-hidden': true, 'aria-label': 'Scroll tabs to right' }, PADDLE); tabPaddles.append(paddleLeft, paddleRight); tabList.after(tabPaddles); initPaddles(tabList, tabPaddles); diff --git a/libs/deps/merch-card.js b/libs/deps/merch-card.js index d937bd4307..f2cdb83c91 100644 --- a/libs/deps/merch-card.js +++ b/libs/deps/merch-card.js @@ -1,4 +1,4 @@ -// branch: stable commit: d93a08fc2123843319c48865f2b71b9ee0b4f5ed Wed, 24 Apr 2024 15:49:31 GMT +// branch: MWPW-135160 commit: d93a08fc2123843319c48865f2b71b9ee0b4f5ed Tue, 30 Apr 2024 01:31:32 GMT import{html as n,LitElement as O}from"/libs/deps/lit-all.min.js";import{css as v,unsafeCSS as x}from"/libs/deps/lit-all.min.js";var m="(max-width: 767px)";var i="(min-width: 768px)",c="(min-width: 1200px)",h="(min-width: 1600px)";var k=v` :host { position: relative; @@ -345,6 +345,7 @@ import{html as n,LitElement as O}from"/libs/deps/lit-all.min.js";import{css as v :host([size='super-wide']) { grid-column: span 2; width: 100%; + max-width: var(--consonant-merch-card-tablet-wide-width); } } @@ -376,6 +377,7 @@ import{html as n,LitElement as O}from"/libs/deps/lit-all.min.js";import{css as v /* responsive width */ --consonant-merch-card-mobile-width: 300px; + --consonant-merch-card-tablet-wide-width: 700px; /* spacing */ --consonant-merch-spacing-xxxs: 4px; diff --git a/libs/features/jarvis-chat.js b/libs/features/jarvis-chat.js index 9cd01f492b..94a201a0e0 100644 --- a/libs/features/jarvis-chat.js +++ b/libs/features/jarvis-chat.js @@ -1,6 +1,7 @@ let chatInitialized = false; let loadScript; let loadStyle; +let getMetadata; const isSilentEvent = (data) => (data['event.workflow'] === 'init' && data['event.type'] === 'request') || (data['event.workflow'] === 'Chat' && data['event.type'] === 'load' && data['event.subtype'] === 'window'); @@ -202,7 +203,7 @@ const openChat = (event) => { } }; -const startInitialization = async (config, event) => { +const startInitialization = async (config, event, onDemand) => { const asset = 'https://client.messaging.adobe.com/latest/AdobeMessagingClient'; await Promise.all([ loadStyle(`${asset}.css`), @@ -217,8 +218,8 @@ const startInitialization = async (config, event) => { } window.AdobeMessagingExperienceClient.initialize({ - appid: config.jarvis.id, - appver: config.jarvis.version, + appid: getMetadata('jarvis-surface-id') || config.jarvis.id, + appver: getMetadata('jarvis-surface-version') || config.jarvis.version, env: config.env.name !== 'prod' ? 'stage' : 'prod', clientId: window.adobeid?.client_id, accessToken: window.adobeIMS?.isSignedInUser() @@ -237,7 +238,7 @@ const startInitialization = async (config, event) => { chatInitialized = !!data?.releaseControl?.showAdobeMessaging; }, onReadyCallback: () => { - if (config.jarvis.onDemand) { + if (onDemand) { openChat(event); } }, @@ -278,23 +279,31 @@ const startInitialization = async (config, event) => { }); }; -const initJarvisChat = async (config, loadScriptFunction, loadStyleFunction) => { +const initJarvisChat = async ( + config, + loadScriptFunction, + loadStyleFunction, + getMetadataFunction, +) => { if (!config?.jarvis) return; loadScript = loadScriptFunction; loadStyle = loadStyleFunction; + getMetadata = getMetadataFunction; + + const onDemandMeta = getMetadata('jarvis-on-demand')?.toLowerCase(); + const onDemand = onDemandMeta ? onDemandMeta === 'on' : config.jarvis.onDemand; document.addEventListener('click', async (event) => { if (!event.target.closest('[href*="#open-jarvis-chat"]')) return; event.preventDefault(); - if (config.jarvis.onDemand && !chatInitialized) { - await startInitialization(config, event); + if (onDemand && !chatInitialized) { + await startInitialization(config, event, onDemand); } else { openChat(event); } }); - - if (!config.jarvis.onDemand) { + if (!onDemand) { await startInitialization(config); } }; diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 740dd94068..5dd4b835f2 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -541,9 +541,10 @@ const createDefaultExperiment = (manifest) => ({ disabled: manifest.disabled, event: manifest.event, manifest: manifest.manifestPath, - variantNames: ['all'], - selectedVariantName: 'default', selectedVariant: { commands: [], fragments: [] }, + selectedVariantName: 'default', + variantNames: ['all'], + variants: {}, }); export async function getPersConfig(info, override = false) { @@ -711,23 +712,28 @@ export function cleanAndSortManifestList(manifests) { const manifestObj = {}; const config = getConfig(); manifests.forEach((manifest) => { - if (!manifest?.manifest) return; - if (!manifest.manifestPath) manifest.manifestPath = normalizePath(manifest.manifest); - if (manifest.manifestPath in manifestObj) { - let fullManifest = manifestObj[manifest.manifestPath]; - let freshManifest = manifest; - if (manifest.name) { - fullManifest = manifest; - freshManifest = manifestObj[manifest.manifestPath]; + try { + if (!manifest?.manifest) return; + if (!manifest.manifestPath) manifest.manifestPath = normalizePath(manifest.manifest); + if (manifest.manifestPath in manifestObj) { + let fullManifest = manifestObj[manifest.manifestPath]; + let freshManifest = manifest; + if (manifest.name) { + fullManifest = manifest; + freshManifest = manifestObj[manifest.manifestPath]; + } + freshManifest.name = fullManifest.name; + freshManifest.selectedVariantName = fullManifest.selectedVariantName; + freshManifest.selectedVariant = freshManifest.variants[freshManifest.selectedVariantName]; + manifestObj[manifest.manifestPath] = freshManifest; + } else { + manifestObj[manifest.manifestPath] = manifest; } - freshManifest.name = fullManifest.name; - freshManifest.selectedVariantName = fullManifest.selectedVariantName; - freshManifest.selectedVariant = freshManifest.variants[freshManifest.selectedVariantName]; - manifestObj[manifest.manifestPath] = freshManifest; - } else { - manifestObj[manifest.manifestPath] = manifest; + if (config.mep?.override) overridePersonalizationVariant(manifest, config); + } catch (e) { + console.warn(e); + window.lana?.log(`MEP Error parsing manifests: ${e.toString()}`); } - if (config.mep?.override) overridePersonalizationVariant(manifest, config); }); return Object.values(manifestObj).sort(compareExecutionOrder); } @@ -751,43 +757,48 @@ export function handleFragmentCommand(command, a) { } export async function applyPers(manifests) { - const config = getConfig(); + try { + const config = getConfig(); - if (!manifests?.length) return; - if (!config?.mep) config.mep = {}; - config.mep.handleFragmentCommand = handleFragmentCommand; - let experiments = manifests; - for (let i = 0; i < experiments.length; i += 1) { - experiments[i] = await getPersConfig(experiments[i], config.mep?.override); - } + if (!manifests?.length) return; + if (!config?.mep) config.mep = {}; + config.mep.handleFragmentCommand = handleFragmentCommand; + let experiments = manifests; + for (let i = 0; i < experiments.length; i += 1) { + experiments[i] = await getPersConfig(experiments[i], config.mep?.override); + } - experiments = cleanAndSortManifestList(experiments); + experiments = cleanAndSortManifestList(experiments); - let results = []; + let results = []; - for (const experiment of experiments) { - const result = await runPersonalization(experiment, config); - if (result) { - results.push(result); + for (const experiment of experiments) { + const result = await runPersonalization(experiment, config); + if (result) { + results.push(result); + } } - } - results = results.filter(Boolean); - deleteMarkedEls(); + results = results.filter(Boolean); + deleteMarkedEls(); - config.experiments = experiments; - config.expBlocks = consolidateObjects(results, 'blocks'); - config.expFragments = consolidateObjects(results, 'fragments'); + config.experiments = experiments; + config.expBlocks = consolidateObjects(results, 'blocks'); + config.expFragments = consolidateObjects(results, 'fragments'); - const pznList = results.filter((r) => (r.experiment?.manifestType === TRACKED_MANIFEST_TYPE)); - if (!pznList.length) return; + const pznList = results.filter((r) => (r.experiment?.manifestType === TRACKED_MANIFEST_TYPE)); + if (!pznList.length) return; - const pznVariants = pznList.map((r) => { - const val = r.experiment.selectedVariantName.replace(TARGET_EXP_PREFIX, '').trim().slice(0, 15); - return val === 'default' ? 'nopzn' : val; - }); - const pznManifests = pznList.map((r) => { - const val = r.experiment?.manifestOverrideName || r.experiment?.manifest; - return getFileName(val).replace('.json', '').trim().slice(0, 15); - }); - config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; + const pznVariants = pznList.map((r) => { + const val = r.experiment.selectedVariantName.replace(TARGET_EXP_PREFIX, '').trim().slice(0, 15); + return val === 'default' ? 'nopzn' : val; + }); + const pznManifests = pznList.map((r) => { + const val = r.experiment?.manifestOverrideName || r.experiment?.manifest; + return getFileName(val).replace('.json', '').trim().slice(0, 15); + }); + config.mep.martech = `|${pznVariants.join('--')}|${pznManifests.join('--')}`; + } catch (e) { + console.warn(e); + window.lana?.log(`MEP Error: ${e.toString()}`); + } } diff --git a/libs/features/richresults.js b/libs/features/richresults.js index d0b1edabaf..5ed8598931 100644 --- a/libs/features/richresults.js +++ b/libs/features/richresults.js @@ -1,7 +1,7 @@ -function getRichResultsForNewsArticle(getMetadata) { +function getRichResultsForArticle(type, getMetadata) { return { '@context': 'https://schema.org', - '@type': 'NewsArticle', + '@type': type, headLine: getMetadata('og:title'), image: getMetadata('og:image'), datePublished: getMetadata('published'), @@ -44,8 +44,9 @@ function getRichResultsForOrgLogo(getMetadata) { function getRichResults(type, getMetadata) { switch (type) { + case 'Article': case 'NewsArticle': - return getRichResultsForNewsArticle(getMetadata); + return getRichResultsForArticle(type, getMetadata); case 'SiteSearchBox': return getRichResultsForSiteSearchBox(getMetadata); case 'Organization': diff --git a/libs/scripts/delayed.js b/libs/scripts/delayed.js index 81ef7a6614..5ea3c7b52d 100644 --- a/libs/scripts/delayed.js +++ b/libs/scripts/delayed.js @@ -21,7 +21,7 @@ export const loadJarvisChat = async (getConfig, getMetadata, loadScript, loadSty if (jarvis === 'desktop' && !desktopViewport) return; const { initJarvisChat } = await import('../features/jarvis-chat.js'); - initJarvisChat(config, loadScript, loadStyle); + initJarvisChat(config, loadScript, loadStyle, getMetadata); }; export const loadPrivacy = async (getConfig, loadScript) => { diff --git a/libs/utils/sidekick.js b/libs/utils/sidekick.js index 5edfe39fa3..80a92c65b4 100644 --- a/libs/utils/sidekick.js +++ b/libs/utils/sidekick.js @@ -1,13 +1,69 @@ +function stylePublish(sk) { + const pubPlg = sk.shadowRoot.querySelector('.publish.plugin'); + if (!pubPlg) return; + const style = document.createElement('style'); + const span = document.createElement('span'); + span.textContent = 'Are you sure? This will publish to production.'; + const btn = pubPlg.querySelector('button'); + const publishStyles = ` + .plugin.update { + --bg-color: rgb(129 27 14); + --text-color: #fff0f0; + + color-scheme: light dark; + display: flex; + order: 100; + } + .publish.plugin > button { + background: var(--bg-color); + border-color: #b46157; + color: var(--text-color); + } + .publish.plugin > button > span { + display: none; + background: var(--bg-color); + border-radius: 4px; + line-height: 1.2rem; + padding: 8px 12px; + position: absolute; + top: 34px; + left: 50%; + transform: translateX(-50%); + width: 150px; + white-space: pre-wrap; + } + .publish.plugin > button:hover > span { + display: block; + color: var(--text-color); + } + .publish.plugin > button > span:before { + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid var(--bg-color); + position: absolute; + text-align: center; + top: -6px; + left: 50%; + transform: translateX(-50%); + } + `; + style.append(publishStyles); + pubPlg.prepend(style); + btn.append(span); +} + // loadScript and loadStyle are passed in to avoid circular dependencies export default function init({ createTag, loadBlock, loadScript, loadStyle }) { // manifest v3 const sendToCaasListener = async (e) => { const { host, project, ref: branch, repo, owner } = e.detail.data.config; + // eslint-disable-next-line import/no-unresolved const { sendToCaaS } = await import('https://milo.adobe.com/tools/send-to-caas/send-to-caas.js'); sendToCaaS({ host, project, branch, repo, owner }, loadScript, loadStyle); }; - const checkSchemaListener = async (e) => { + const checkSchemaListener = async () => { const schema = document.querySelector('script[type="application/ld+json"]'); if (schema === null) return; const resultsUrl = 'https://search.google.com/test/rich-results?url='; @@ -35,4 +91,7 @@ export default function init({ createTag, loadBlock, loadScript, loadStyle }) { sk.addEventListener('custom:send-to-caas', sendToCaasListener); sk.addEventListener('custom:check-schema', checkSchemaListener); sk.addEventListener('custom:preflight', preflightListener); + + // Color code publish button + stylePublish(sk); } diff --git a/test/blocks/merch-card/merch-card.test.js b/test/blocks/merch-card/merch-card.test.js index 34a18a0811..ab9e7cf728 100644 --- a/test/blocks/merch-card/merch-card.test.js +++ b/test/blocks/merch-card/merch-card.test.js @@ -72,7 +72,6 @@ describe('Plans Card', () => { expect(merchCard.getAttribute('badge-background-color')).to.be.equal('#EDCC2D'); expect(merchCard.getAttribute('badge-color')).to.be.equal('#000000'); expect(merchCard.getAttribute('badge-text')).to.be.equal('LOREM IPSUM DOLOR'); - expect(JSON.parse(merchCard.getAttribute('icons'))).to.have.lengthOf(2); expect(merchCard.getAttribute('checkbox-label')).to.be.equal('Add a 30-day free trial of Adobe Stock.*'); expect(body.textContent).to.be.equal('Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim.MaecenasSee terms about lorem ipsum'); expect(detail.textContent).to.be.equal('Maecenas porttitor enim.'); @@ -100,7 +99,6 @@ describe('Plans Card', () => { expect(merchCard.getAttribute('badge-background-color')).to.be.equal('#EDCC2D'); expect(merchCard.getAttribute('badge-color')).to.be.equal('#000000'); expect(merchCard.getAttribute('badge-text')).to.be.equal('LOREM IPSUM DOLOR'); - expect(JSON.parse(merchCard.getAttribute('icons'))).to.have.lengthOf(2); expect(merchCard.getAttribute('checkbox-label')).to.be.equal('Add a 30-day free trial of Adobe Stock.*'); expect(body.textContent).to.be.equal('Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim.MaecenasSee terms about lorem ipsum'); expect(detail.textContent).to.be.equal('Maecenas porttitor enim.'); @@ -126,7 +124,6 @@ describe('Plans Card', () => { expect(detail).to.exist; expect(merchCard.getAttribute('variant')).to.be.equal('plans'); expect(merchCard.getAttribute('badge')).to.not.exist; - expect(JSON.parse(merchCard.getAttribute('icons'))).to.have.lengthOf(2); expect(body.textContent).to.be.equal('Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim.See terms about lorem ipsum'); expect(detail.textContent).to.be.equal('Maecenas porttitor enim.'); expect(buttons.length).to.be.equal(2); diff --git a/test/blocks/merch-card/mocks/plans-card.html b/test/blocks/merch-card/mocks/plans-card.html index 38c17af31a..8cd328b7d3 100644 --- a/test/blocks/merch-card/mocks/plans-card.html +++ b/test/blocks/merch-card/mocks/plans-card.html @@ -108,4 +108,51 @@ <h5>Maecenas porttitor enim.</h5> <div></div> </div> </div> + <div class="merch-card plans icons mnemonic"> + <div> + <div> </div> + <div>LOREM IPSUM DOLOR</div> + </div> + <div> + <div> + <picture> + <source type="image/webp" srcset="" media="(min-width: 600px)"> + <source type="image/webp" srcset=""> + <source type="image/png" srcset="" media="(min-width: 600px)"> + <img loading="lazy" alt="" src="" width="80" height="78"> + </picture> + <picture> + <source type="image/webp" srcset="" media="(min-width: 600px)"> + <source type="image/webp" srcset=""> + <source type="image/png" srcset="" media="(min-width: 600px)"> + <img loading="lazy" alt="" src="" width="80" height="78"> + </picture> + <h2 id="lorem-ipsum-dolor-sit-amet"><em>Lorem ipsum dolor sit amet</em></h2> + <h3 id="lorem-ipsum-dolor">Lorem ipsum dolor</h3> + <h5>Maecenas porttitor enim.</h5> + <p>Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim.</p> + <p><a href="https://adobe.com/">See terms about lorem ipsum</a></p> + <p> + <div class="mnemonic-list"> + <div> + <div data-valign="middle"> + <p><strong>Key apps:</strong></p> + <p><a href="/assets/img/acrobat-pro.svg">https://main--milo--adobecom.hlx.page/assets/img/acrobat-pro.svg | Acrobat Pro</a> <strong>Acrobat</strong></p> + <p><a href="/assets/img/photoshop.svg">https://main--milo--adobecom.hlx.page/assets/img/photoshop.svg | Photoshop</a> <strong>Photoshop</strong></p> + <p><a href="/assets/img/premiere.svg">https://main--milo--adobecom.hlx.page/assets/img/premiere.svg | Premiere Pro</a> <strong>Premiere Pro</strong></p> + <p><a href="/assets/img/illustrator.svg">https://main--milo--adobecom.hlx.page/assets/img/illustrator.svg | Illustrator</a> <strong>Illustrator</strong></p> + <p><a href="/assets/img/cc-express.svg">https://main--milo--adobecom.hlx.page/assets/img/cc-express.svg | Adobe Express</a> <strong>Adobe Express</strong></p> + <p><a href="/assets/img/mnemonic/lightroom.svg">https://main--milo--adobecom.hlx.page/assets/img/mnemonic/lightroom.svg | Lightroom</a> <strong>Lightroom</strong></p> + </div> + </div> + </div> + </p> + <p><em><a href="https://business.adobe.com/">Learn More</a></em> <strong><a href="https://business.adobe.com/">Save now</a></strong></p> + </div> + </div> + <div> + <div>Lorem ipsum dolor sit amet</div> + <div></div> + </div> + </div> </div> diff --git a/test/features/jarvis-chat/jarvis-chat.test.js b/test/features/jarvis-chat/jarvis-chat.test.js index 822d34cb70..5534f369d7 100644 --- a/test/features/jarvis-chat/jarvis-chat.test.js +++ b/test/features/jarvis-chat/jarvis-chat.test.js @@ -41,14 +41,14 @@ describe('Jarvis Chat', () => { it('should not initialize when configuration is not available', async () => { setConfig({}); const config = getConfig(); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); expect(initializeSpy.called).to.be.false; }); it('should initialize when configuration is available', async () => { setConfig(defaultConfig); const config = getConfig(); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); expect(initializeSpy.called).to.be.true; }); @@ -60,7 +60,7 @@ describe('Jarvis Chat', () => { prefix: '/africa', }, }); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); const args = initializeSpy.getCall(0).args[0]; expect(args.appid).to.equal(config.jarvis.id); expect(args.appver).to.equal(config.jarvis.version); @@ -70,11 +70,90 @@ describe('Jarvis Chat', () => { expect(args.region).to.equal('africa'); }); + it('should receive the correct custom surface id configuration', async () => { + setConfig(defaultConfig); + const config = Object.assign(getConfig(), { + locale: { + ietf: 'en', + prefix: '/africa', + }, + }); + + const testSurfaceId = 'test-id'; + const getMetadataMock = sinon.stub(); + getMetadataMock.withArgs('jarvis-surface-id').returns(testSurfaceId); + await initJarvisChat(config, sinon.stub(), sinon.stub(), getMetadataMock); + const args = initializeSpy.getCall(0).args[0]; + expect(args.appid).to.equal(testSurfaceId); + expect(args.appver).to.equal(defaultConfig.jarvis.version); + expect(args.env).to.equal(config.env.name === 'prod' ? 'prod' : 'stage'); + }); + + it('should receive the correct custom version configuration', async () => { + setConfig(defaultConfig); + const config = Object.assign(getConfig(), { + locale: { + ietf: 'en', + prefix: '/africa', + }, + }); + const testVersion = '0.123'; + const getMetadataMock = sinon.stub(); + getMetadataMock.withArgs('jarvis-surface-version').returns(testVersion); + await initJarvisChat(config, sinon.stub(), sinon.stub(), getMetadataMock); + const args = initializeSpy.getCall(0).args[0]; + expect(args.appid).to.equal(defaultConfig.jarvis.id); + expect(args.appver).to.equal(testVersion); + expect(args.env).to.equal(config.env.name === 'prod' ? 'prod' : 'stage'); + }); + + it('should receive the correct default onDemand configuration', async () => { + setConfig(defaultConfig); + const config = Object.assign(getConfig(), { + locale: { + ietf: 'en', + prefix: '/africa', + }, + jarvis: { + id: 'milo', + version: '1.0', + onDemand: true, + }, + }); + + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); + expect(initializeSpy.called).to.be.false; + }); + + it('should receive the correct custom onDemand configuration', async () => { + setConfig(defaultConfig); + const config = Object.assign(getConfig(), { + locale: { + ietf: 'en', + prefix: '/africa', + }, + jarvis: { + id: 'milo', + version: '1.0', + onDemand: true, + }, + }); + + const getMetadataMock = sinon.stub(); + getMetadataMock.withArgs('jarvis-on-demand').returns('off'); + await initJarvisChat(config, sinon.stub(), sinon.stub(), getMetadataMock); + expect(initializeSpy.called).to.be.true; + const args = initializeSpy.getCall(0).args[0]; + expect(args.appid).to.equal(defaultConfig.jarvis.id); + expect(args.appver).to.equal(defaultConfig.jarvis.version); + expect(args.env).to.equal(config.env.name === 'prod' ? 'prod' : 'stage'); + }); + it('should open a chat session upon click', async () => { document.body.innerHTML = await readFile({ path: './mocks/jarvis-chat.html' }); setConfig(defaultConfig); const config = getConfig(); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); const args = initializeSpy.getCall(0).args[0]; args.callbacks.initCallback({ releaseControl: { showAdobeMessaging: true } }); openMessagingWindowSpy.resetHistory(); @@ -89,7 +168,7 @@ describe('Jarvis Chat', () => { it('should synchronize analytics', async () => { setConfig(defaultConfig); const config = getConfig(); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); const args = initializeSpy.getCall(0).args[0]; const iconRender = await readFile({ path: './mocks/sendChatIconRenderEvent.json' }); @@ -145,7 +224,7 @@ describe('Jarvis Chat', () => { it('should initialize on demand when configured', async () => { setConfig(defaultConfig); const config = getConfig(); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); const args = initializeSpy.getCall(0).args[0]; // Set uninitialized state args.callbacks.initCallback({ releaseControl: { showAdobeMessaging: false } }); @@ -153,7 +232,7 @@ describe('Jarvis Chat', () => { config.jarvis.onDemand = true; document.body.innerHTML = await readFile({ path: './mocks/jarvis-chat.html' }); - await initJarvisChat(config, sinon.stub(), sinon.stub()); + await initJarvisChat(config, sinon.stub(), sinon.stub(), sinon.stub()); expect(initializeSpy.called).to.be.false; document.querySelector('a').click(); await new Promise((resolve) => { diff --git a/test/utils/mocks/head-rich-results-article.html b/test/utils/mocks/head-rich-results-article.html new file mode 100644 index 0000000000..6f8e584e31 --- /dev/null +++ b/test/utils/mocks/head-rich-results-article.html @@ -0,0 +1,9 @@ +<meta name="richresults" content="Article"> +<meta property="og:title" content="The war is over"> +<meta property="og:image" content="https://example.com/photos/1x1/photo.jpg"> +<meta name="published" content="2022-12-24"> +<meta name="modified" content="2022-12-25"> +<meta name="authorname" content="Emile Zola"> +<meta name="authorurl" content="https://example.com/zola"> +<link rel="icon" href="data:,"> + diff --git a/test/utils/richresults.test.js b/test/utils/richresults.test.js index 95a8788aa0..cd8f20366d 100644 --- a/test/utils/richresults.test.js +++ b/test/utils/richresults.test.js @@ -8,6 +8,27 @@ describe('Rich Results', () => { setConfig({}); }); + it('add the Article rich results', async () => { + document.head.innerHTML = await readFile({ path: './mocks/head-rich-results-article.html' }); + await loadArea(document); + const script = document.querySelector('script[type="application/ld+json"]'); + const actual = JSON.parse(script.innerHTML); + const expected = { + '@context': 'https://schema.org', + '@type': 'Article', + headLine: 'The war is over', + image: 'https://example.com/photos/1x1/photo.jpg', + datePublished: '2022-12-24', + dateModified: '2022-12-25', + author: { + '@type': 'Person', + name: 'Emile Zola', + url: 'https://example.com/zola', + }, + }; + expect(actual).to.deep.equal(expected); + }); + it('add the NewsArticle rich results', async () => { document.head.innerHTML = await readFile({ path: './mocks/head-rich-results.html' }); await loadArea(document);