Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test-html-results/
test-results/
test-a11y-results/
tmp
nala/utils/auth.json
libs/navigation/dist/
*.mdc
/.cursor*/
Expand Down
8 changes: 8 additions & 0 deletions libs/blocks/modal/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,14 @@ html[dir="rtl"] .dialog-modal button.dialog-close {
max-width: calc((100% - 6px) - 2em);
}

.dialog-modal.commerce-frame.crm,
.dialog-modal.commerce-frame.d2p,
.dialog-modal.commerce-frame.twp {
width: 1030px;
max-width: 90vw;
}


.dialog-modal.commerce-frame {
width: 1024px;
height: 850px;
Expand Down
23 changes: 21 additions & 2 deletions libs/blocks/modal/modal.merch.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,31 @@ export function sendViewportDimensionsOnRequest(source) {
window.addEventListener('resize', debounce(() => sendViewportDimensionsToIframe(source), 10));
}

function reactToMessage({ data, source }) {
function reactToMessage({ data, source }, iframe) {
if (data === 'viewportWidth' && source) {
/* If the page inside iframe comes from another domain, it won't be able to retrieve
the viewport dimensions, so it sends a request to receive the viewport dimensions
from the parent window. */
sendViewportDimensionsOnRequest(source);
}

/* The height of the CRM modal is not calculated properly on Titan side so it looks better
if we simply set the fixed height 875px for CRM on desktop
*/
if (iframe.src.includes('.modal.html')) { // if crm
const dialogModal = iframe.closest('.dialog-modal');
const miloIframe = iframe.closest('.milo-iframe');
if (!dialogModal || !miloIframe) return;
if (document.body.offsetWidth > TABLET_MAX) {
miloIframe.style.height = 'auto';
dialogModal.style.height = 'auto';
dialogModal.style.maxHeight = '875px';
} else {
dialogModal.style.maxHeight = 'none';
}
return;
}

if (data?.contentHeight) {
/* If the page inside iframe sends the postMessage with its content height,
we activate the height auto adjustment to eliminate the blank space at the bottom of the modal.
Expand Down Expand Up @@ -84,5 +101,7 @@ export function adjustStyles({ dialog, iframe }) {
export default async function enableCommerceFrameFeatures({ dialog, iframe }) {
if (!dialog || !iframe) return;
adjustStyles({ dialog, iframe });
window.addEventListener('message', reactToMessage);
window.addEventListener('message', (e) => {
reactToMessage(e, iframe);
});
}
84 changes: 84 additions & 0 deletions libs/blocks/preflight/checks/merch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { getConfig } from '../../../utils/utils.js';
import { SEVERITY } from './constants.js';

const API_BASE = 'https://www.adobe.com/mas/io/fragment';
const API_KEY = 'wcms-commerce-ims-ro-user-milo';
const HYDRATION_DELAY_MS = 3000;

export function findFragmentElements(area = document) {
const els = area.querySelectorAll('aem-fragment[fragment]');
const entries = [];
els.forEach((el) => {
const uuid = el.getAttribute('fragment');
if (!uuid) return;
const card = el.closest('merch-card, merch-card-collection, mas-field') || el;
entries.push({ uuid, el, card });
});
return entries;
}

export async function checkFragmentPublished(uuid, locale) {
const params = new URLSearchParams({ id: uuid, api_key: API_KEY, locale });
const url = `${API_BASE}?${params.toString()}`;
try {
const res = await fetch(url);
return { uuid, httpStatus: res.status, published: res.status === 200 };
} catch {
return { uuid, httpStatus: 0, published: false };
}
}

function resolveLocale(locale) {
if (locale) return locale;
const ietf = getConfig()?.locale?.ietf || 'en-US';
return ietf.replace('-', '_');
}

export async function checkUnpublishedFragments({ area = document, locale } = {}) {
const resolvedLocale = resolveLocale(locale);
const entries = findFragmentElements(area);
const byUuid = new Map();
entries.forEach((entry) => {
if (!byUuid.has(entry.uuid)) byUuid.set(entry.uuid, []);
byUuid.get(entry.uuid).push(entry);
});

const uuids = [...byUuid.keys()];
const results = await Promise.all(
uuids.map((uuid) => checkFragmentPublished(uuid, resolvedLocale)),
);

const unpublished = results
.filter((r) => !r.published)
.map((r) => {
const group = byUuid.get(r.uuid);
return {
uuid: r.uuid,
httpStatus: r.httpStatus,
elements: group.map((g) => g.el),
cards: group.map((g) => g.card),
};
});

if (unpublished.length > 0) {
window.lana?.log?.(`[preflight][mas] ${unpublished.length} unpublished fragment(s) out of ${uuids.length} scanned: ${unpublished.map((u) => u.uuid).join(', ')}`, { tags: 'preflight', errorType: 'i' });
}

return { unpublished, scanned: uuids.length };
}

export function runChecks({ area = document, locale, delayMs = HYDRATION_DELAY_MS } = {}) {
return [(async () => {
if (delayMs > 0) {
await new Promise((resolve) => { setTimeout(resolve, delayMs); });
}
const { unpublished, scanned } = await checkUnpublishedFragments({ area, locale });
const failed = unpublished.length > 0;
return {
name: 'M@S Unpublished Fragments',
status: failed ? 'fail' : 'pass',
severity: SEVERITY.CRITICAL,
details: failed ? { unpublished, scanned } : { scanned },
};
})()];
}
5 changes: 5 additions & 0 deletions libs/blocks/preflight/checks/preflightApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
runChecks as runChecksSeo,
} from './seo.js';
import { runChecks as runChecksStructure } from './structure.js';
import { runChecks as runChecksMerch } from './merch.js';
import { SEVERITY } from './constants.js';

let checksSuite = null;
Expand Down Expand Up @@ -64,6 +65,7 @@ export default {
runChecks: runChecksSeo,
},
structure: { runChecks: runChecksStructure },
merch: { runChecks: runChecksMerch },
};

export const getChecksSuite = () => {
Expand Down Expand Up @@ -108,12 +110,14 @@ const runChecks = async (url, area, injectVisualMetadata = false) => {
const performance = await Promise.all(runChecksPerformance(url, area));
const seo = isASO ? await fetchPreflightChecks() : runChecksSeo({ url, area });
const structure = await Promise.all(runChecksStructure({ area }));
const merch = await Promise.all(runChecksMerch({ area }));
return {
accessibility,
assets,
performance,
seo,
structure,
merch,
};
};

Expand Down Expand Up @@ -150,6 +154,7 @@ export async function getPreflightResults(options = {}) {
...(res.performance || []),
...(res.seo || []),
...(res.structure || []),
...(res.merch || []),
];

const result = {
Expand Down
69 changes: 68 additions & 1 deletion libs/blocks/preflight/panels/merch.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { html, signal, useEffect } from '../../../deps/htm-preact.js';
import { checkUnpublishedFragments } from '../checks/merch.js';

const wcsElements = signal([]);
const masFieldsMultipleFragmentWarnings = signal([]);
const unpublishedFragments = signal([]);
const loading = signal(true);

const MAS_UNPUBLISHED_HIGHLIGHT = 'preflight-mas-unpublished';

const ALLOWED_MAS_HOSTS = ['mas.adobe.com'];

function isMasUrl(href) {
Expand Down Expand Up @@ -94,6 +98,23 @@ function checkMasFieldsMultipleFragments() {
masFieldsMultipleFragmentWarnings.value = warnings;
}

async function checkUnpublishedFragmentsForPanel() {
const main = document.querySelector('main');
main?.querySelectorAll(`.${MAS_UNPUBLISHED_HIGHLIGHT}`).forEach((el) => {
el.classList.remove(MAS_UNPUBLISHED_HIGHLIGHT);
});
const { unpublished } = await checkUnpublishedFragments({ area: document });
unpublishedFragments.value = unpublished.map((u) => {
u.cards?.forEach((c) => c.classList.add(MAS_UNPUBLISHED_HIGHLIGHT));
const firstCard = u.cards?.[0];
return {
uuid: u.uuid,
httpStatus: u.httpStatus,
location: firstCard ? getBlockLocation(firstCard) : 0,
};
});
}

function getService() {
return document.getElementsByTagName('mas-commerce-service')?.[0];
}
Expand Down Expand Up @@ -442,6 +463,42 @@ function MasFieldsMultipleFragmentSection() {
`;
}

function UnpublishedFragmentItem({ entry }) {
return html`
<div class="preflight-item merch-item merch-error">
<div class="preflight-item-text">
<p class="preflight-item-title">
<span class="result-icon red"></span>
Unpublished M@S fragment
</p>
<p class="preflight-item-description">
<strong>Fragment ID:</strong> <code class="wcs-osi-code">${entry.uuid}</code>
<br/><strong>HTTP status:</strong> ${entry.httpStatus}
</p>
<button
class="preflight-action merch-scroll-btn"
onclick=${() => scrollToElement(entry.location)}>
Scroll to card
</button>
</div>
</div>
`;
}

function UnpublishedFragmentsSection() {
const entries = unpublishedFragments.value;
if (entries.length === 0) return null;
return html`
<div class="merch-section">
<h3 class="merch-section-title">M@S Unpublished Fragments</h3>
<p class="merch-section-description">These fragments are referenced on this page but are not published. Publishing now will break the live page.</p>
<div class="merch-blocks-list">
${entries.map((entry) => html`<${UnpublishedFragmentItem} entry=${entry} />`)}
</div>
</div>
`;
}

function MerchSummary() {
const totalElements = wcsElements.value.length;
const passedCount = wcsElements.value.filter((elem) => (elem.urlStatus === 'success' || !elem.href)
Expand All @@ -451,8 +508,9 @@ function MerchSummary() {
|| elem.promoCodeStatus === 'not-found').length;
const undeterminedCount = wcsElements.value.filter((elem) => elem.urlStatus === 'undetermined').length;
const masWarningsCount = masFieldsMultipleFragmentWarnings.value.length;
const unpublishedCount = unpublishedFragments.value.length;

if (totalElements === 0 && masWarningsCount === 0) {
if (totalElements === 0 && masWarningsCount === 0 && unpublishedCount === 0) {
return html`
<div class="merch-summary no-blocks">
<h3>No Merch Elements Found</h3>
Expand Down Expand Up @@ -487,6 +545,12 @@ function MerchSummary() {
<span class="merch-stat-label">Blocks w/ multiple fragment IDs</span>
</div>
`}
${unpublishedCount > 0 && html`
<div class="merch-summary-stat has-errors">
<span class="merch-stat-number">${unpublishedCount}</span>
<span class="merch-stat-label">Unpublished M@S fragments</span>
</div>
`}
</div>
`;
}
Expand All @@ -496,6 +560,7 @@ export default function Merch() {
checkMasFieldsMultipleFragments();
setTimeout(() => {
checkWcsElements();
checkUnpublishedFragmentsForPanel();
}, 3000);
}, []);

Expand All @@ -512,6 +577,7 @@ export default function Merch() {
<div class="merch-panel">
<${MerchSummary} />
<${MasFieldsMultipleFragmentSection} />
<${UnpublishedFragmentsSection} />
</div>
`;
}
Expand All @@ -520,6 +586,7 @@ export default function Merch() {
<div class="merch-panel">
<${MerchSummary} />
<${MasFieldsMultipleFragmentSection} />
<${UnpublishedFragmentsSection} />
<div class="merch-section">
<h3 class="merch-section-title">Elements</h3>
<div class="merch-blocks-list">
Expand Down
36 changes: 35 additions & 1 deletion libs/blocks/preflight/preflight.css
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ span.preflight-time {
transition: outline 300ms;
border: none;
height: 36px;
font-family: 'Adobe Clean', adobe-clean, sans-serif;
font-family: var(--body-font-family);
font-size: 16px;
padding: 0 18px;
clip-path: polygon(0% 0%, var(--notch-size) 0%, calc(100% - var(--notch-size)) 0%, 100% var(--notch-size), 100% calc(100% - var(--notch-size)), 100% 100%, 0% 100%, 0% 100%);
Expand Down Expand Up @@ -1280,4 +1280,38 @@ img:hover ~ .asset-meta,
border: 3px solid #e68600 !important;
}

/* Highlight cards whose M@S fragment is unpublished */
.preflight-mas-unpublished {
position: relative !important;
outline: 4px solid #f44 !important;
background-image: repeating-linear-gradient(
135deg,
rgb(255 68 68 / 8%) 0,
rgb(255 68 68 / 8%) 8px,
transparent 8px,
transparent 16px
) !important;
}

.preflight-mas-unpublished::before {
content: 'Not published';
position: absolute;
top: -1px;
right: -1px;
z-index: 2;
padding: 4px 10px;
background: #f44;
color: #fff;
font-family: var(--body-font-family);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
border-top-right-radius: 8px;
border-bottom-left-radius: 8px;
box-shadow: 0 1px 4px rgb(0 0 0 / 20%);
pointer-events: none;
line-height: 2.5;
}


Loading
Loading