diff --git a/.github/workflows/email-release.yaml b/.github/workflows/email-release.yaml
deleted file mode 100644
index 767a33be59..0000000000
--- a/.github/workflows/email-release.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Milo Bot Release Email
-
-on:
- pull_request_target:
- types:
- - closed
- branches:
- - main
-
-jobs:
- action:
- if: github.event.pull_request.merged == true
- runs-on: ubuntu-latest
-
- steps:
- - name: Check out repository
- uses: actions/checkout@v3
- - name: Use email bot
- uses: adobecom/milo-email-bot@main
- env:
- TO_EMAIL_NEW_FEATURE: ${{ secrets.TO_EMAIL_NEW_FEATURE }}
- TO_EMAIL_HIGH_IMPACT: ${{ secrets.TO_EMAIL_HIGH_IMPACT }}
- FROM_EMAIL: 'bot@em2344.milo.pink'
- FROM_NAME: 'Milo Bot'
- SG_KEY: ${{ secrets.SG_KEY }}
- SG_TEMPLATE: 'd-44d50e7138c341959fa1ecf5374fb8e1'
diff --git a/libs/blocks/global-navigation/global-navigation.css b/libs/blocks/global-navigation/global-navigation.css
index f446ef6e25..d18af79c8a 100644
--- a/libs/blocks/global-navigation/global-navigation.css
+++ b/libs/blocks/global-navigation/global-navigation.css
@@ -370,6 +370,7 @@ header.global-navigation {
}
.feds-utilities {
+ position: relative;
display: flex;
align-items: center;
padding: 0 var(--feds-gutter);
diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js
index 5d638011c5..9171e75629 100644
--- a/libs/blocks/global-navigation/global-navigation.js
+++ b/libs/blocks/global-navigation/global-navigation.js
@@ -8,38 +8,37 @@ import {
loadStyle,
} from '../../utils/utils.js';
import {
- toFragment,
- getFedsPlaceholderConfig,
- getAnalyticsValue,
+ closeAllDropdowns,
decorateCta,
+ fetchAndProcessPlainHtml,
+ getActiveLink,
+ getAnalyticsValue,
getExperienceName,
- loadDecorateMenu,
+ getFedsPlaceholderConfig,
+ hasActiveLink,
+ icons,
+ isDesktop,
+ isTangentToViewport,
+ lanaLog,
+ loadBaseStyles,
loadBlock,
+ loadDecorateMenu,
loadStyles,
- trigger,
+ logErrorFor,
+ selectors,
setActiveDropdown,
- closeAllDropdowns,
- loadBaseStyles,
- yieldToMain,
- isDesktop,
- isTangentToViewport,
- setCurtainState,
- hasActiveLink,
setActiveLink,
- getActiveLink,
- selectors,
- logErrorFor,
- lanaLog,
- fetchAndProcessPlainHtml,
+ setCurtainState,
+ setUserProfile,
+ toFragment,
+ trigger,
+ yieldToMain,
} from './utilities/utilities.js';
import { replaceKey, replaceKeyArray } from '../../features/placeholders.js';
-const CONFIG = {
- icons: {
- company: '',
- search: '',
- },
+export const CONFIG = {
+ icons,
delays: {
mainNavDropdowns: 800,
loadDelayed: 3000,
@@ -63,6 +62,13 @@ const CONFIG = {
config: {
enableLocalSection: true,
miniAppContext: {
+ onMessage: (name, payload) => {
+ if (name === 'System' && payload.subType === 'AppInitiated') {
+ window.adobeProfile?.getUserProfile()
+ .then((data) => { setUserProfile(data); })
+ .catch(() => { setUserProfile({}); });
+ }
+ },
logger: {
trace: () => {},
debug: () => {},
@@ -425,6 +431,8 @@ class Gnav {
};
imsReady = async () => {
+ if (!window.adobeIMS.isSignedInUser() || !this.useUniversalNav) setUserProfile({});
+
const tasks = [this.useUniversalNav ? this.decorateUniversalNav : this.decorateProfile];
try {
@@ -589,6 +597,9 @@ class Gnav {
locale,
imsClientId: window.adobeid?.client_id,
theme: 'light',
+ onReady: () => {
+ this.decorateAppPrompt({ getAnchorState: () => window.UniversalNav.getComponent?.('app-switcher') });
+ },
analyticsContext: {
consumer: {
name: 'adobecom',
@@ -610,6 +621,27 @@ class Gnav {
});
};
+ decorateAppPrompt = async ({ getAnchorState } = {}) => {
+ const state = getMetadata('app-prompt')?.toLowerCase();
+ const entName = getMetadata('app-prompt-entitlement')?.toLowerCase();
+ const promptPath = getMetadata('app-prompt-path')?.toLowerCase();
+ if (state === 'off'
+ || !window.adobeIMS?.isSignedInUser()
+ || !isDesktop.matches
+ || !entName?.length
+ || !promptPath?.length) return;
+
+ const { base } = getConfig();
+ const [
+ webappPrompt,
+ ] = await Promise.all([
+ import('../../features/webapp-prompt/webapp-prompt.js'),
+ loadStyle(`${base}/features/webapp-prompt/webapp-prompt.css`),
+ ]);
+
+ webappPrompt.default({ promptPath, entName, parent: this.blocks.universalNav, getAnchorState });
+ };
+
loadSearch = () => {
if (this.blocks?.search?.instance) return null;
diff --git a/libs/blocks/global-navigation/utilities/menu/menu.js b/libs/blocks/global-navigation/utilities/menu/menu.js
index dd4dba107c..e71b77322f 100644
--- a/libs/blocks/global-navigation/utilities/menu/menu.js
+++ b/libs/blocks/global-navigation/utilities/menu/menu.js
@@ -1,20 +1,19 @@
import {
decorateCta,
+ fetchAndProcessPlainHtml,
getActiveLink,
getAnalyticsValue,
- logErrorFor,
- setActiveDropdown,
- trigger,
+ icons,
isDesktop,
+ lanaLog,
+ logErrorFor,
selectors,
+ setActiveDropdown,
toFragment,
+ trigger,
yieldToMain,
- fetchAndProcessPlainHtml,
- lanaLog,
} from '../utilities.js';
-const homeIcon = '';
-
const decorateHeadline = (elem, index) => {
if (!(elem instanceof HTMLElement)) return null;
@@ -279,7 +278,7 @@ const decorateCrossCloudMenu = (content) => {
crossCloudMenuEl.className = 'feds-crossCloudMenu-wrapper';
crossCloudMenuEl.querySelector('div').className = 'feds-crossCloudMenu';
crossCloudMenuEl.querySelectorAll('ul li').forEach((el, index) => {
- if (index === 0) el.querySelector('a')?.prepend(toFragment`${homeIcon}`);
+ if (index === 0) el.querySelector('a')?.prepend(toFragment`${icons.home}`);
el.className = 'feds-crossCloudMenu-item';
});
diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js
index 74ebf0d30e..3fd7d7ba3b 100644
--- a/libs/blocks/global-navigation/utilities/utilities.js
+++ b/libs/blocks/global-navigation/utilities/utilities.js
@@ -32,6 +32,12 @@ export const selectors = {
columnBreak: '.column-break',
};
+export const icons = {
+ company: '',
+ search: '',
+ home: '',
+};
+
export const lanaLog = ({ message, e = '', tags = 'errorType=default' }) => {
const url = getMetadata('gnav-source');
window.lana.log(`${message} | gnav-source: ${url} | href: ${window.location.href} | ${e.reason || e.error || e.message || e}`, {
@@ -337,3 +343,29 @@ export async function fetchAndProcessPlainHtml({ url, shouldDecorateLinks = true
body.innerHTML = await replaceText(body.innerHTML, getFedsPlaceholderConfig());
return body;
}
+
+export const [setUserProfile, getUserProfile] = (() => {
+ let profileData;
+ let profileResolve;
+ let profileTimeout;
+
+ const profilePromise = new Promise((resolve) => {
+ profileResolve = resolve;
+
+ profileTimeout = setTimeout(() => {
+ profileData = {};
+ resolve(profileData);
+ }, 5000);
+ });
+
+ return [
+ (data) => {
+ if (data && !profileData) {
+ profileData = data;
+ clearTimeout(profileTimeout);
+ profileResolve(profileData);
+ }
+ },
+ () => profilePromise,
+ ];
+})();
diff --git a/libs/blocks/marketo/marketo.js b/libs/blocks/marketo/marketo.js
index ddbbd32bb5..e496345174 100644
--- a/libs/blocks/marketo/marketo.js
+++ b/libs/blocks/marketo/marketo.js
@@ -13,7 +13,7 @@
/*
* Marketo Form
*/
-import { parseEncodedConfig, loadScript, createTag, createIntersectionObserver } from '../../utils/utils.js';
+import { parseEncodedConfig, loadScript, localizeLink, createTag, createIntersectionObserver } from '../../utils/utils.js';
const ROOT_MARGIN = 1000;
const FORM_ID = 'form id';
@@ -48,7 +48,10 @@ export const decorateURL = (destination, baseURL = window.location) => {
destinationUrl.pathname = `${pathname}.html`;
}
- return destinationUrl;
+ const localized = localizeLink(destinationUrl.href, null, true);
+ destinationUrl.pathname = new URL(localized, baseURL.origin).pathname;
+
+ return destinationUrl.href;
} catch (e) {
window.lana?.log(`Error with Marketo destination URL: ${destination} ${e.message}`);
}
@@ -95,7 +98,7 @@ const readyForm = (form) => {
};
const setPreference = (key, value) => {
- if (key && key.includes('.')) {
+ if (value && key?.includes('.')) {
const keyParts = key.split('.');
const lastKey = keyParts.pop();
const formDataObject = keyParts.reduce((obj, part) => {
@@ -168,7 +171,7 @@ export default function init(el) {
if (destinationUrl) {
formData['form.success.type'] = 'redirect';
- formData['form.success.content'] = destinationUrl.href;
+ formData['form.success.content'] = destinationUrl;
}
}
diff --git a/libs/blocks/mobile-app-banner/README.md b/libs/blocks/mobile-app-banner/README.md
new file mode 100644
index 0000000000..5b11b80d34
--- /dev/null
+++ b/libs/blocks/mobile-app-banner/README.md
@@ -0,0 +1,7 @@
+# Mobile App Banner
+
+## Performance Impact
+This block has a known issue of CLS and can add anywhere from 0.03 to 0.07 CLS (the CWV metric for CLS starts failing at 0.1).
+Please make an informed decision based on the business requirements and current CLS on your page when using this block.
+
+This CLS impact can be mitigated if branch banner is configured to load at the bottom of the page in branch dashboard.
diff --git a/libs/blocks/ost/ost.js b/libs/blocks/ost/ost.js
index 00ed19d305..d1faa839ee 100644
--- a/libs/blocks/ost/ost.js
+++ b/libs/blocks/ost/ost.js
@@ -25,6 +25,20 @@ export const WCS_LANDSCAPE = 'PUBLISHED';
*/
const METADATA_MAPPINGS = { 'checkout-workflow': 'workflow' };
+const priceDefaultOptions = {
+ term: true,
+ seat: true,
+ tax: false,
+ old: false,
+ exclusive: false,
+};
+
+const updateParams = (params, key, value) => {
+ if (value !== priceDefaultOptions[key]) {
+ params.set(key, value);
+ }
+};
+
document.body.classList.add('tool', 'tool-ost');
/**
@@ -65,11 +79,11 @@ export const createLinkMarkup = (
displayOldPrice,
forceTaxExclusive,
} = options;
- params.set('term', displayRecurrence);
- params.set('seat', displayPerUnit);
- params.set('tax', displayTax);
- params.set('old', displayOldPrice);
- params.set('exclusive', forceTaxExclusive);
+ updateParams(params, 'term', displayRecurrence);
+ updateParams(params, 'seat', displayPerUnit);
+ updateParams(params, 'tax', displayTax);
+ updateParams(params, 'old', displayOldPrice);
+ updateParams(params, 'exclusive', forceTaxExclusive);
}
return `https://milo.adobe.com/tools/ost?${params.toString()}`;
};
diff --git a/libs/blocks/review/review.js b/libs/blocks/review/review.js
index 5879243166..484945ec81 100644
--- a/libs/blocks/review/review.js
+++ b/libs/blocks/review/review.js
@@ -4,7 +4,10 @@ import { getMetadata, loadStyle, getConfig } from '../../utils/utils.js';
import HelixReview from './components/helixReview/HelixReview.js';
import { checkPostUrl } from './utils/utils.js';
-const COMMENT_THRESHOLD = 3;
+const getCommentThreshold = (defaultThreshold = 3) => {
+ const threshold = parseInt(getMetadata('comment-threshold'), 10);
+ return (Number.isInteger(threshold) && threshold > 0) ? threshold : defaultThreshold;
+};
const getReviewPath = (url) => {
try {
@@ -44,7 +47,7 @@ const getProductJson = () => {
const App = ({ strings }) => html`
<${HelixReview}
clickTimeout="5000"
- commentThreshold=${COMMENT_THRESHOLD}
+ commentThreshold=${getCommentThreshold()}
hideTitleOnReload=${strings.hideTitleOnReload}
lang=${getPageLocale()}
reviewTitle=${strings.reviewTitle}
diff --git a/libs/blocks/table/table.css b/libs/blocks/table/table.css
index d502b5786c..ff126a0298 100644
--- a/libs/blocks/table/table.css
+++ b/libs/blocks/table/table.css
@@ -160,11 +160,7 @@
}
.table.header-left .row-heading .col.col-heading .buttons-wrapper > * {
- margin-left: 0;
-}
-
-[dir='rtl'] .table.header-left .row-heading .col.col-heading .buttons-wrapper > * {
- margin-right: 0;
+ margin-inline-start: 0;
}
.table .row-heading .col-heading.top-left-rounded {
diff --git a/libs/features/personalization/entitlements.js b/libs/features/personalization/entitlements.js
index de8e76a315..831fefcf9f 100644
--- a/libs/features/personalization/entitlements.js
+++ b/libs/features/personalization/entitlements.js
@@ -17,6 +17,11 @@ const ENTITLEMENT_MAP = {
'015c52cb-30b0-4ac9-b02e-f8716b39bfb6': 'not-q-always-on-promo',
'42e06851-64cd-4684-a54a-13777403487a': '3d-substance-collection',
'eda8c774-420b-44c2-9006-f9a8d0fb5168': '3d-substance-texturing',
+ // PEP segments
+ '6cb0d58c-3a65-47e2-b459-c52bb158d5b6': 'lightroom-web-usage',
+ 'caa3de84-6336-4fa8-8db2-240fc88106cc': 'photoshop-web-usage',
+ '5c6a4bb8-a2f3-4202-8cca-f5e918b969dc': 'firefly-web-usage',
+ '3df0b0b0-d06e-4fcc-986e-cc97f54d04d8': 'acrobat-web-usage',
};
export const getEntitlementMap = async () => {
diff --git a/libs/features/personalization/stage-entitlements.js b/libs/features/personalization/stage-entitlements.js
index 7dc2b58e2d..b690fe2241 100644
--- a/libs/features/personalization/stage-entitlements.js
+++ b/libs/features/personalization/stage-entitlements.js
@@ -9,6 +9,11 @@ const STAGE_ENTITLEMENTS = {
'569f0f9d-83e8-45b4-adbf-07ef08a83398': 'any-cc-product-with-stock',
'47e204a3-220a-4e53-a95e-94b6eded0d26': '3d-substance-collection',
'4ec7b469-42c9-4367-a7da-39f11a32d880': '3d-substance-texturing',
+ // PEP segments
+ '9202b767-77dc-4e6e-8d74-488d9ef08900': 'lightroom-web-usage',
+ '3a7ffcce-11b8-4242-8cdf-8c8d059ae1cd': 'photoshop-web-usage',
+ 'cbe1d7ab-db7d-49cb-969e-a6a2bbe8c660': 'firefly-web-usage',
+ '96adf81f-97ca-4943-81ff-c41fbe8f3af7': 'acrobat-web-usage',
};
export default STAGE_ENTITLEMENTS;
diff --git a/libs/features/webapp-prompt/webapp-prompt.css b/libs/features/webapp-prompt/webapp-prompt.css
new file mode 100644
index 0000000000..1fbc0baae8
--- /dev/null
+++ b/libs/features/webapp-prompt/webapp-prompt.css
@@ -0,0 +1,190 @@
+/* Hide banner if user shrinks to mobile after being displayed */
+.appPrompt {
+ --pep-background-prompt: #ffffff;
+ --pep-background-progress: #e9e9e9;
+ display: none;
+}
+
+@media (min-width: 900px) {
+ .appPrompt {
+ position: absolute;
+ top: 90%;
+ right: 0;
+ width: 400px;
+ padding: 24px 24px 28px;
+ display: flex;
+ flex-direction: column;
+ row-gap: 16px;
+ border-radius: 16px;
+ background: var(--pep-background-prompt);
+ overflow: hidden;
+ box-sizing: border-box;
+ box-shadow: 0 0 3px 0 rgb(0 0 0 / 20%);
+ }
+
+ [dir = "rtl"] .appPrompt {
+ right: unset;
+ left: 0;
+ }
+
+ .appPrompt-icon {
+ min-height: 32px;
+ }
+
+ .appPrompt-icon img,
+ .appPrompt-icon svg {
+ width: 32px;
+ display: block;
+ }
+
+ .appPrompt-title {
+ font-size: 18px;
+ line-height: 1.3;
+ font-weight: 800;
+ }
+
+ .appPrompt-profile {
+ display: flex;
+ column-gap: 12px;
+ }
+
+ .appPrompt-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ flex-shrink: 0;
+ }
+
+ .appPrompt-avatar-image {
+ display: block;
+ object-fit: cover;
+ }
+
+ .appPrompt-user {
+ overflow: hidden;
+ }
+
+ .appPrompt-name {
+ font-size: 16px;
+ line-height: 1.3;
+ font-weight: 600;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .appPrompt-email {
+ font-size: 14px;
+ line-height: 1.5;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .appPrompt-footer {
+ margin: 8px 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ column-gap: 12px;
+ }
+
+ .appPrompt-text {
+ font-size: 14px;
+ line-height: 1.5;
+ }
+
+ .appPrompt-cta {
+ display: flex;
+ flex-shrink: 0;
+ height: 32px;
+ min-width: 72px;
+ padding: 0 14px;
+ border-width: 2px;
+ border-style: solid;
+ border-radius: 16px;
+ font-size: 15px;
+ font-weight: 700;
+ line-height: 0;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
+ overflow: visible;
+ white-space: nowrap;
+ transition-property: color, border-color, background-color;
+ transition-duration: 130ms;
+ transition-timing-function: ease-out;
+ cursor: pointer;
+ }
+
+ .appPrompt-cta--close {
+ background-color: rgb(255, 255, 255);
+ border-color: rgb(75, 75, 75);
+ color: rgb(75, 75, 75);
+ }
+
+ .appPrompt-cta--close:hover,
+ .appPrompt-cta--close:focus {
+ background-color: rgb(75, 75, 75);
+ color: rgb(255, 255, 255);
+ }
+
+ .appPrompt-close {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ border: 0;
+ font: bold 15px Trebuchet MS, sans-serif;
+ background: transparent;
+ cursor: pointer;
+ }
+
+ .appPrompt-close:before {
+ content: '\2715';
+ }
+
+ .appPrompt-close:focus {
+ background-color: var(--pep-background-progress);
+ border-radius: 50%;
+ border: 3px solid var(--pep-background-prompt);
+ }
+
+ [dir = "rtl"] .appPrompt-close {
+ right: unset;
+ left: 12px;
+ }
+
+ .appPrompt-close:focus {
+ /* For Firefox */
+ outline: auto;
+ /* For Chrome, Edge, and Safari */
+ outline: 2px solid -webkit-focus-ring-color;
+ }
+
+ .appPrompt-progressWrapper {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 12px;
+ background-color: var(--pep-background-progress);
+ }
+
+ .appPrompt-progress {
+ width: 100%;
+ height: 100%;
+ animation: appPrompt-animate 7s linear forwards;
+ transform-origin: 0 0;
+ transform: scaleX(0) translateZ(0);
+ }
+
+ @keyframes appPrompt-animate {
+ 100% {
+ transform: scaleX(1);
+ }
+ }
+}
diff --git a/libs/features/webapp-prompt/webapp-prompt.js b/libs/features/webapp-prompt/webapp-prompt.js
new file mode 100644
index 0000000000..94181e390d
--- /dev/null
+++ b/libs/features/webapp-prompt/webapp-prompt.js
@@ -0,0 +1,236 @@
+import {
+ getFedsPlaceholderConfig,
+ getUserProfile,
+ icons,
+ lanaLog,
+ toFragment,
+} from '../../blocks/global-navigation/utilities/utilities.js';
+import { getConfig, decorateSVG } from '../../utils/utils.js';
+import { replaceKey, replaceText } from '../placeholders.js';
+
+const CONFIG = {
+ selectors: { prompt: '.appPrompt' },
+ delay: 7000,
+ loaderColor: '#EB1000',
+};
+
+const getElemText = (elem) => elem?.textContent?.trim().toLowerCase();
+
+const getMetadata = (el) => [...el.childNodes].reduce((acc, row) => {
+ if (row.children?.length === 2) {
+ const key = getElemText(row.children[0]);
+ const val = getElemText(row.children[1]);
+ if (key && val) acc[key] = val;
+ }
+ return acc;
+}, {});
+
+const getIcon = (content) => {
+ const picture = content.querySelector('picture');
+ if (picture) return picture;
+
+ const svg = content.querySelector('a[href$=".svg"]');
+ if (svg) return decorateSVG(svg);
+
+ return icons.company;
+};
+
+class AppPrompt {
+ constructor({ promptPath, entName, parent, getAnchorState } = {}) {
+ this.promptPath = promptPath;
+ this.entName = entName;
+ this.parent = parent;
+ this.getAnchorState = getAnchorState;
+ this.id = this.promptPath.split('/').pop();
+ this.elements = {};
+
+ this.init();
+ }
+
+ init = async () => {
+ if (this.isDismissedPrompt() || !this.parent) return;
+
+ const entMatch = await this.doesEntitlementMatch();
+ if (!entMatch) return;
+
+ const content = await this.fetchContent();
+ if (!content) return;
+
+ await this.getData(content);
+ if (!this.options['redirect-url'] || !this.options['product-name']) return;
+
+ ({ id: this.anchorId, isOpen: this.isAnchorExpanded } = await this.getAnchorState()
+ .catch((e) => {
+ lanaLog({
+ message: 'Error on getting anchor state',
+ e,
+ tags: 'errorType=error,module=pep',
+ });
+ return {};
+ }));
+ if (this.isAnchorExpanded) return;
+
+ if (this.anchorId) this.anchor = document.querySelector(`#${this.anchorId}`);
+ this.offset = this.anchor
+ ? this.parent.offsetWidth - (this.anchor.offsetWidth + this.anchor.offsetLeft) : 0;
+ this.template = this.decorate();
+
+ this.addEventListeners();
+
+ this.parent.prepend(this.template);
+ this.elements.closeIcon.focus();
+
+ this.redirectFn = this.initRedirect();
+ };
+
+ doesEntitlementMatch = async () => {
+ const entitlements = await getConfig().entitlements();
+ return entitlements?.length && entitlements.includes(this.entName);
+ };
+
+ fetchContent = async () => {
+ const res = await fetch(`${this.promptPath}.plain.html`);
+
+ if (!res.ok) {
+ lanaLog({
+ message: `Error fetching content for prompt: ${this.promptPath}.plain.html`,
+ e: `Status ${res.status} when trying to fetch content for prompt`,
+ tags: 'errorType=error,module=pep',
+ });
+ return '';
+ }
+
+ const text = await res.text();
+ const content = await replaceText(text, getFedsPlaceholderConfig());
+ const html = new DOMParser().parseFromString(content, 'text/html');
+
+ return html;
+ };
+
+ getData = async (content) => {
+ this.icon = getIcon(content);
+
+ const selectors = {
+ title: 'h2',
+ subtitle: 'h3',
+ cancel: 'em > a',
+ };
+
+ await Promise.all(Object.keys(selectors).map(async (key) => {
+ const element = content.querySelector(selectors[key]);
+ if (element?.innerText) this[key] = element.innerText;
+ else {
+ const label = await replaceKey(`pep-prompt-${key}`, getFedsPlaceholderConfig());
+ this[key] = label === `pep prompt ${key}` ? '' : label;
+ }
+ }));
+
+ await getUserProfile()
+ .then((data) => {
+ const requiredFields = ['display_name', 'email', 'avatar'];
+ const hasRequiredFields = requiredFields.every((field) => !!data[field]);
+ if (!hasRequiredFields) return;
+
+ this.profile = data;
+ }).catch((e) => {
+ lanaLog({
+ message: 'Error fetching user profile',
+ e,
+ tags: 'errorType=error,module=pep',
+ });
+ });
+
+ const metadata = getMetadata(content.querySelector('.section-metadata'));
+ metadata['loader-duration'] = parseInt(metadata['loader-duration'] || CONFIG.delay, 10);
+ metadata['loader-color'] = metadata['loader-color'] || CONFIG.loaderColor;
+ this.options = metadata;
+ };
+
+ decorate = () => {
+ this.elements.closeIcon = toFragment``;
+ this.elements.cta = toFragment``;
+ this.elements.profile = this.profile
+ ? toFragment`
+
+
+
+
+
${this.profile.display_name}
+
${this.profile.email}
+
+
`
+ : '';
+
+ return toFragment`
+ ${this.elements.closeIcon}
+
+ ${this.icon}
+
+
${this.title}
+ ${this.elements.profile}
+
+
+
`;
+ };
+
+ addEventListeners = () => {
+ this.anchor?.addEventListener('click', this.close);
+ document.addEventListener('keydown', this.handleKeyDown);
+
+ [this.elements.closeIcon, this.elements.cta]
+ .forEach((elem) => elem.addEventListener('click', this.close));
+ };
+
+ handleKeyDown = (event) => {
+ if (event.key === 'Escape') this.close();
+ };
+
+ initRedirect = () => setTimeout(() => {
+ this.close({ saveDismissal: false });
+ window.location.assign(this.options['redirect-url']);
+ }, this.options['loader-duration']);
+
+ isDismissedPrompt = () => AppPrompt.getDismissedPrompts().includes(this.id);
+
+ setDismissedPrompt = () => {
+ const dismissedPrompts = new Set(AppPrompt.getDismissedPrompts());
+ dismissedPrompts.add(this.id);
+ document.cookie = `dismissedAppPrompts=${JSON.stringify([...dismissedPrompts])};path=/`;
+ };
+
+ close = ({ saveDismissal = true } = {}) => {
+ const appPromptElem = document.querySelector(CONFIG.selectors.prompt);
+ appPromptElem?.remove();
+ clearTimeout(this.redirectFn);
+ if (saveDismissal) this.setDismissedPrompt();
+ document.removeEventListener('keydown', this.handleKeyDown);
+ this.anchor?.focus();
+ this.anchor?.removeEventListener('click', this.close);
+ };
+
+ static getDismissedPrompts = () => {
+ const cookie = document.cookie
+ .split(';')
+ .find((item) => item.trim().startsWith('dismissedAppPrompts='))
+ ?.split('=')[1];
+
+ return cookie ? JSON.parse(cookie) : [];
+ };
+}
+
+export default async function init(config) {
+ try {
+ const appPrompt = await new AppPrompt(config);
+ return appPrompt;
+ } catch (e) {
+ lanaLog({ message: 'Could not initialize PEP', e, tags: 'errorType=error,module=pep' });
+ return null;
+ }
+}
diff --git a/libs/scripts/delayed.js b/libs/scripts/delayed.js
index 5ea3c7b52d..5b7425061e 100644
--- a/libs/scripts/delayed.js
+++ b/libs/scripts/delayed.js
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Adobe. All rights reserved.
+ * Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
diff --git a/libs/utils/sidekick-decorate.js b/libs/utils/sidekick-decorate.js
new file mode 100644
index 0000000000..ec125ef650
--- /dev/null
+++ b/libs/utils/sidekick-decorate.js
@@ -0,0 +1,59 @@
+export default function stylePublish(sk) {
+ const style = new CSSStyleSheet();
+ style.replaceSync(`
+ :host {
+ --bg-color: rgb(129 27 14);
+ --text-color: #fff0f0;
+ color-scheme: light dark;
+ }
+ .publish.plugin {
+ order: 100;
+ }
+ .publish.plugin button {
+ background: var(--bg-color);
+ border-color: #b46157;
+ color: var(--text-color);
+ position: relative;
+ }
+ .publish.plugin button:hover {
+ background-color: var(--hlx-sk-button-hover-bg);
+ border-color: unset;
+ color: var(--hlx-sk-button-hover-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%);
+ }
+ `);
+ sk.shadowRoot.adoptedStyleSheets = [style];
+ setTimeout(() => {
+ const btn = sk.shadowRoot.querySelector('.publish.plugin button');
+ btn?.insertAdjacentHTML('beforeend', `
+ Are you sure? This will publish to production.
+ `);
+ }, 500);
+}
diff --git a/libs/utils/sidekick.js b/libs/utils/sidekick.js
index 80a92c65b4..2d2060026b 100644
--- a/libs/utils/sidekick.js
+++ b/libs/utils/sidekick.js
@@ -1,57 +1,4 @@
-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);
-}
+import stylePublish from './sidekick-decorate.js';
// loadScript and loadStyle are passed in to avoid circular dependencies
export default function init({ createTag, loadBlock, loadScript, loadStyle }) {
diff --git a/test/blocks/global-navigation/global-navigation.test.js b/test/blocks/global-navigation/global-navigation.test.js
index 13270b7ac2..7e50960332 100644
--- a/test/blocks/global-navigation/global-navigation.test.js
+++ b/test/blocks/global-navigation/global-navigation.test.js
@@ -71,12 +71,15 @@ describe('global navigation', () => {
});
it("should log when there's issues within onReady", async () => {
+ const ogIms = window.adobeIMS;
const gnav = await createFullGlobalNavigation({});
sinon.stub(gnav, 'decorateProfile').callsFake(() => {
throw new Error('error');
});
+ window.adobeIMS = { isSignedInUser: () => true };
await gnav.imsReady();
expect(window.lana.log.getCalls().find((c) => c.args[0].includes('issues within onReady'))).to.exist;
+ window.adobeIMS = ogIms;
});
it('should log when IMS signIn method is not available', async () => {
@@ -1273,6 +1276,7 @@ describe('global navigation', () => {
it('should reload unav on viewport change', async () => {
await createFullGlobalNavigation({ unavContent: 'on' });
await setViewport(viewports.mobile);
+ isDesktop.dispatchEvent(new Event('change'));
await clock.runAllAsync();
expect(window.UniversalNav.reload.getCall(0)).to.exist;
});
diff --git a/test/blocks/marketo/marketo.test.js b/test/blocks/marketo/marketo.test.js
index 3d3f66e7c3..f3ab41549c 100644
--- a/test/blocks/marketo/marketo.test.js
+++ b/test/blocks/marketo/marketo.test.js
@@ -1,6 +1,7 @@
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { delay } from '../../helpers/waitfor.js';
+import { setConfig } from '../../../libs/utils/utils.js';
import init, { setPreferences, decorateURL } from '../../../libs/blocks/marketo/marketo.js';
const innerHTML = await readFile({ path: './mocks/body.html' });
@@ -45,31 +46,31 @@ describe('marketo decorateURL', () => {
it('decorates absolute URL with local base URL', () => {
const baseURL = new URL('http://localhost:6456/marketo-block');
const result = decorateURL('https://main--milo--adobecom.hlx.page/marketo-block/thank-you', baseURL);
- expect(result.href).to.equal('http://localhost:6456/marketo-block/thank-you');
+ expect(result).to.equal('http://localhost:6456/marketo-block/thank-you');
});
it('decorates relative URL with absolute base URL', () => {
const baseURL = new URL('https://main--milo--adobecom.hlx.page/marketo-block');
const result = decorateURL('/marketo-block/thank-you', baseURL);
- expect(result.href).to.equal('https://main--milo--adobecom.hlx.page/marketo-block/thank-you');
+ expect(result).to.equal('https://main--milo--adobecom.hlx.page/marketo-block/thank-you');
});
it('decorates absolute URL with matching base URL', () => {
const baseURL = new URL('https://main--milo--adobecom.hlx.page/marketo-block');
const result = decorateURL('https://main--milo--adobecom.hlx.page/marketo-block/thank-you', baseURL);
- expect(result.href).to.equal('https://main--milo--adobecom.hlx.page/marketo-block/thank-you');
+ expect(result).to.equal('https://main--milo--adobecom.hlx.page/marketo-block/thank-you');
});
it('decorates absolute URL with .html base URL', () => {
const baseURL = new URL('https://business.adobe.com/marketo-block.html');
const result = decorateURL('https://main--milo--adobecom.hlx.page/marketo-block/thank-you', baseURL);
- expect(result.href).to.equal('https://business.adobe.com/marketo-block/thank-you.html');
+ expect(result).to.equal('https://business.adobe.com/marketo-block/thank-you.html');
});
it('keeps identical absolute URL with .html base URL', () => {
const baseURL = new URL('https://business.adobe.com/marketo-block.html');
const result = decorateURL('https://business.adobe.com/marketo-block/thank-you.html', baseURL);
- expect(result.href).to.equal('https://business.adobe.com/marketo-block/thank-you.html');
+ expect(result).to.equal('https://business.adobe.com/marketo-block/thank-you.html');
});
it('returns null when provided a malformed URL', () => {
@@ -81,6 +82,19 @@ describe('marketo decorateURL', () => {
it('Does not add .html to ending slash', () => {
const baseURL = new URL('https://business.adobe.com/marketo-block.html');
const result = decorateURL('https://business.adobe.com/', baseURL);
- expect(result.href).to.equal('https://business.adobe.com/');
+ expect(result).to.equal('https://business.adobe.com/');
+ });
+
+ it('localizes URL with .html base URL', () => {
+ setConfig({
+ pathname: '/uk/marketo-block.html',
+ locales: {
+ '': {},
+ uk: {},
+ },
+ });
+ const baseURL = new URL('https://business.adobe.com/uk/marketo-block.html');
+ const result = decorateURL('/marketo-block/thank-you', baseURL);
+ expect(result).to.equal('https://business.adobe.com/uk/marketo-block/thank-you.html');
});
});
diff --git a/test/blocks/marketo/mocks/marketo-utils.js b/test/blocks/marketo/mocks/marketo-utils.js
index 28bb451b7a..0c98787010 100644
--- a/test/blocks/marketo/mocks/marketo-utils.js
+++ b/test/blocks/marketo/mocks/marketo-utils.js
@@ -61,3 +61,5 @@ export function createIntersectionObserver({ el, callback /* , once = true, opti
// fire immediately
callback(el, { target: el });
}
+
+export const localizeLink = (href) => href;
diff --git a/test/blocks/ost/mocks/ost-utils.js b/test/blocks/ost/mocks/ost-utils.js
index eb333e66c2..e8f4ff9b28 100644
--- a/test/blocks/ost/mocks/ost-utils.js
+++ b/test/blocks/ost/mocks/ost-utils.js
@@ -101,5 +101,5 @@ function unmockOstDeps() {
}
export {
- getConfig, getLocale, getMetadata, loadScript, loadStyle, mockOstDeps, mockRes, unmockOstDeps,
+ getConfig, getLocale, getMetadata, loadScript, loadStyle, mockOstDeps, unmockOstDeps, mockRes,
};
diff --git a/test/blocks/ost/ost.test.html.js b/test/blocks/ost/ost.test.html.js
index 15f31b0ec3..55ecb5e68a 100644
--- a/test/blocks/ost/ost.test.html.js
+++ b/test/blocks/ost/ost.test.html.js
@@ -1,15 +1,59 @@
import { expect } from '@esm-bundle/chai';
import { mockOstDeps, unmockOstDeps } from './mocks/ost-utils.js';
+import { CheckoutWorkflow, CheckoutWorkflowStep } from '../../../libs/deps/commerce.js';
+import { DEFAULT_CTA_TEXT, createLinkMarkup } from '../../../libs/blocks/ost/ost.js';
+
+const { perpM2M } = await fetch('./mocks/wcs-artifacts-mock.json').then((res) => res.json());
+const defaults = {
+ checkoutWorkflow: 'UCv3',
+ checkoutWorkflowStep: 'email',
+};
+const osi = 'cea462e983f649bca2293325c9894bdd';
+const promo = 'test-promo';
+const texts = {
+ buy: DEFAULT_CTA_TEXT,
+ try: 'free-trial',
+};
+const types = {
+ checkoutUrl: 'checkoutUrl',
+ price: 'price',
+ opticalPrice: 'opticalPrice',
+};
+
+function assertLink(link, offer, params, text = texts.buy) {
+ const { searchParams } = new URL(link.href);
+ Object.entries(params).forEach(([key, value]) => {
+ expect(searchParams.get(key)).to.equal(String(value));
+ });
+ if (params.type === types.checkoutUrl) {
+ expect(searchParams.get('text')).to.equal(text);
+ expect(link.text).to.equal(`CTA {{${text}}}`);
+ } else {
+ expect(link.text).to.equal(`PRICE - ${offer.planType} - ${offer.name}`);
+ }
+}
+
+function createLink(params = {}) {
+ return createLinkMarkup(
+ defaults,
+ params.osi ?? osi,
+ params.type,
+ perpM2M,
+ params,
+ params.promo,
+ );
+}
+
+beforeEach(() => {
+ sessionStorage.clear();
+});
afterEach(() => {
unmockOstDeps();
});
-describe('loadOstEnv', async () => {
- beforeEach(() => {
- sessionStorage.clear();
- });
+describe('OST: loadOstEnv', async () => {
it('fetches and returns page status and metadata', async () => {
const {
options: { country, language, workflow },
@@ -81,10 +125,7 @@ describe('loadOstEnv', async () => {
});
});
-describe('init', () => {
- beforeEach(() => {
- sessionStorage.clear();
- });
+describe('OST: init', () => {
it('opens OST without waiting for IMS if query string includes token', async () => {
const {
options: { country, language, workflow },
@@ -179,3 +220,69 @@ describe('init', () => {
});
});
});
+
+describe('OST: merch link creation', () => {
+ describe('checkout-link', () => {
+ const type = types.checkoutUrl;
+
+ it('with default params', async () => {
+ const link = createLink({ type });
+ assertLink(link, perpM2M, { osi, type });
+ expect({ ...link.dataset }).to.eql({});
+ });
+
+ it('with promo and custom text', async () => {
+ const ctaText = texts.try;
+ const link = createLink({ ctaText, promo, type });
+ assertLink(link, perpM2M, { osi, promo, type }, ctaText);
+ });
+
+ it('to UCv2 workflow', async () => {
+ const workflow = CheckoutWorkflow.V2;
+ const workflowStep = CheckoutWorkflowStep.CHECKOUT_EMAIL;
+ const link = createLink({ type, workflow, workflowStep });
+ assertLink(link, perpM2M, { osi, type, workflow, workflowStep });
+ });
+ });
+
+ describe('inline-price', () => {
+ const type = types.price;
+
+ it('with default params', async () => {
+ const link = createLink({ type });
+ assertLink(link, perpM2M, { osi, type });
+ });
+
+ it('with default params from OST', async () => {
+ const link = createLink({
+ type,
+ displayRecurrence: true,
+ displayPerUnit: true,
+ displayTax: false,
+ displayOldPrice: false,
+ forceTaxExclusive: false,
+ });
+ expect(link.href).to.eql('https://milo.adobe.com/tools/ost?osi=cea462e983f649bca2293325c9894bdd&type=price&perp=true');
+ });
+
+ it('with custom options', async () => {
+ const displayRecurrence = true;
+ const displayPerUnit = true;
+ const displayTax = true;
+ const forceTaxExclusive = true;
+ const link = createLink({
+ displayRecurrence,
+ displayPerUnit,
+ displayTax,
+ forceTaxExclusive,
+ type,
+ });
+ assertLink(link, perpM2M, {
+ tax: displayTax,
+ exclusive: forceTaxExclusive,
+ osi,
+ type,
+ });
+ });
+ });
+});
diff --git a/test/blocks/ost/ost.test.js b/test/blocks/ost/ost.test.js
deleted file mode 100644
index 0934d7058d..0000000000
--- a/test/blocks/ost/ost.test.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import { expect } from '@esm-bundle/chai';
-import { readFile } from '@web/test-runner-commands';
-
-const { CheckoutWorkflow, CheckoutWorkflowStep } = await import('../../../libs/deps/commerce.js');
-const { DEFAULT_CTA_TEXT, createLinkMarkup } = await import('../../../libs/blocks/ost/ost.js');
-
-const data = await readFile({ path: './mocks/wcs-artifacts-mock.json' });
-const { perpM2M } = JSON.parse(data);
-const defaults = {
- checkoutWorkflow: 'UCv3',
- checkoutWorkflowStep: 'email',
-};
-const osi = 'cea462e983f649bca2293325c9894bdd';
-const promo = 'test-promo';
-const texts = {
- buy: DEFAULT_CTA_TEXT,
- try: 'free-trial',
-};
-const types = {
- checkoutUrl: 'checkoutUrl',
- price: 'price',
- opticalPrice: 'opticalPrice',
-};
-
-function assertLink(link, offer, params, text = texts.buy) {
- const { searchParams } = new URL(link.href);
- Object.entries(params).forEach(([key, value]) => {
- expect(searchParams.get(key)).to.equal(String(value));
- });
- if (params.type === types.checkoutUrl) {
- expect(searchParams.get('text')).to.equal(text);
- expect(link.text).to.equal(`CTA {{${text}}}`);
- } else {
- expect(link.text).to.equal(`PRICE - ${offer.planType} - ${offer.name}`);
- }
-}
-
-function createLink(params = {}) {
- return createLinkMarkup(
- defaults,
- params.osi ?? osi,
- params.type,
- perpM2M,
- params,
- params.promo,
- );
-}
-
-describe('function "createLinkMarkup"', () => {
- describe('creates "cta" link', () => {
- const type = types.checkoutUrl;
-
- it('with default params', async () => {
- const link = createLink({ type });
- assertLink(link, perpM2M, { osi, type });
- });
-
- it('with promo and custom text', async () => {
- const ctaText = texts.try;
- const link = createLink({ ctaText, promo, type });
- assertLink(link, perpM2M, { osi, promo, type }, ctaText);
- });
-
- it('to UCv2 workflow', async () => {
- const workflow = CheckoutWorkflow.V2;
- const workflowStep = CheckoutWorkflowStep.CHECKOUT_EMAIL;
- const link = createLink({ type, workflow, workflowStep });
- assertLink(link, perpM2M, { osi, type, workflow, workflowStep });
- });
- });
-
- describe('creates "price" link', () => {
- const type = types.price;
-
- it('with default params', async () => {
- const link = createLink({ type });
- assertLink(link, perpM2M, { osi, type });
- });
-
- it('with custom options', async () => {
- const displayRecurrence = true;
- const displayPerUnit = true;
- const displayTax = true;
- const forceTaxExclusive = true;
- const link = createLink({
- displayRecurrence,
- displayPerUnit,
- displayTax,
- forceTaxExclusive,
- type,
- });
- assertLink(link, perpM2M, {
- term: displayRecurrence,
- seat: displayPerUnit,
- tax: displayTax,
- exclusive: forceTaxExclusive,
- osi,
- type,
- });
- });
- });
-});
diff --git a/test/blocks/review/components/review/Review.test.js b/test/blocks/review/components/review/Review.test.js
index 02f5f07a17..0dd4ab4a79 100644
--- a/test/blocks/review/components/review/Review.test.js
+++ b/test/blocks/review/components/review/Review.test.js
@@ -7,9 +7,12 @@ import Review from '../../../../../libs/blocks/review/components/review/Review.j
describe('Review', () => {
beforeEach(() => {
window.localStorage.setItem('/data/review', JSON.stringify({ rating: 5 }));
- const review = html`<${Review} averageRating="4" initialRating="4" />`;
+ const review = html`<${Review} averageRating="5" initialRating="5" />`;
render(review, document.body);
});
+ afterEach(() => {
+ localStorage.removeItem('/data/review');
+ });
it('should display review', async () => {
const reviewElement = await waitForElement('.hlx-ReviewWrapper');
@@ -18,22 +21,45 @@ describe('Review', () => {
expect(titleElement).to.exist;
});
- it('should test ratings click above comment threshold', async () => {
+ it('should test ratings active decoration ', async () => {
+ await delay(100);
const reviewElement = await waitForElement('.hlx-ReviewWrapper');
- const ratingsElement = reviewElement.querySelectorAll(
- '.hlx-Review-ratingFields input',
- )[4];
- ratingsElement.dispatchEvent(new Event('click'));
- expect(ratingsElement.classList.contains('is-active')).to.be.false;
+ const ratingElements = reviewElement.querySelectorAll('.hlx-Review-ratingFields input');
+ await delay(100);
+ ratingElements.forEach((rating) => {
+ expect(rating.classList.contains('is-Active')).to.be.true;
+ });
+ await delay(100);
});
it('should test ratings click above comment threshold', async () => {
const reviewElement = await waitForElement('.hlx-ReviewWrapper');
- const ratingsElement = reviewElement.querySelectorAll(
- '.hlx-Review-ratingFields input',
- )[1];
- ratingsElement.dispatchEvent(new Event('click'));
- expect(ratingsElement.classList.contains('is-active')).to.be.false;
+ const ratingElement = reviewElement.querySelectorAll('.hlx-Review-ratingFields input')[4];
+ ratingElement.dispatchEvent(new Event('click'));
+ await delay(100);
+ const comments = reviewElement.querySelector('#rating-comments');
+ expect(ratingElement.getAttribute('aria-checked')).to.equal('true');
+ expect(comments).not.to.exist;
+ });
+
+ it('should test click ratings below comment threshold', async () => {
+ const reviewElement = await waitForElement('.hlx-ReviewWrapper');
+ const ratingElement = reviewElement.querySelectorAll('.hlx-Review-ratingFields input')[1];
+ ratingElement.dispatchEvent(new Event('click'));
+ await delay(100);
+ const comments = reviewElement.querySelector('#rating-comments');
+ expect(ratingElement.getAttribute('aria-checked')).to.equal('true');
+ expect(comments).to.exist;
+ });
+
+ it('should test click ratings equal comment threshold', async () => {
+ const reviewElement = await waitForElement('.hlx-ReviewWrapper');
+ const ratingElement = reviewElement.querySelectorAll('.hlx-Review-ratingFields input')[2];
+ ratingElement.dispatchEvent(new Event('click'));
+ await delay(100);
+ const comments = reviewElement.querySelector('#rating-comments');
+ expect(ratingElement.getAttribute('aria-checked')).to.equal('true');
+ expect(comments).to.exist;
});
it('should test for input change', async () => {
diff --git a/test/blocks/review/mocks/body.html b/test/blocks/review/mocks/body.html
index f79bbcf7b0..ae37542fd9 100644
--- a/test/blocks/review/mocks/body.html
+++ b/test/blocks/review/mocks/body.html
@@ -44,3 +44,26 @@
5
+
diff --git a/test/blocks/review/review.test.js b/test/blocks/review/review.test.js
index 91af4b419a..951d65fda6 100644
--- a/test/blocks/review/review.test.js
+++ b/test/blocks/review/review.test.js
@@ -1,19 +1,99 @@
+import sinon from 'sinon';
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
-import { waitForElement } from '../../helpers/waitfor.js';
+import { waitForElement, delay } from '../../helpers/waitfor.js';
import init from '../../../libs/blocks/review/review.js';
-describe('Review Comp', () => {
+describe('Review Component Ratings vs. Thresholds', () => {
+ let fetchStub;
beforeEach(async () => {
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
- window.localStorage.setItem('/data/review', JSON.stringify({ rating: 5 }));
window.s_adobe = { visitor: { getMarketingCloudVisitorID: () => 'abcd' } };
+ fetchStub = sinon.stub(window, 'fetch');
});
- it('could be initialized', async () => {
- const div = document.querySelector('.review');
- await init(div);
- const review = await waitForElement('.hlx-ReviewWrapper');
- expect(review).to.exist;
+ afterEach(() => {
+ sinon.restore();
+ localStorage.removeItem('/data/review');
+ const metaTag = document.querySelector('meta[name="comment-threshold"]');
+ if (metaTag) {
+ metaTag.remove();
+ }
+ });
+
+ const thresholds = [4, 5];
+ const ratings = [5, 4, 3, 2, 1];
+
+ describe('Reviews loaded', () => {
+ it('could be initialized', async () => {
+ const div = document.querySelector('.review');
+ await init(div);
+ fetchStub.resolves(
+ new Response(JSON.stringify({
+ total: 4,
+ offset: 0,
+ limit: 4,
+ data: [
+ { country: 'all', total: '17', average: '3.5' },
+ { country: 'en', total: '6', average: '3.3' },
+ { country: 'fr', total: '3', average: '3.3' },
+ { country: 'de', total: '3', average: '3.3' },
+ ],
+ ':type': 'sheet',
+ }), {
+ status: 200,
+ headers: { 'Content-type': 'application/json' },
+ }),
+ );
+ const review = await waitForElement('.hlx-ReviewWrapper');
+ expect(review).to.exist;
+ });
+ });
+
+ thresholds.forEach((threshold) => {
+ describe(`with a comment threshold of ${threshold}`, () => {
+ ratings.forEach((rating) => {
+ it(`tests rating ${rating}`, async () => {
+ const metaTag = document.createElement('meta');
+ metaTag.setAttribute('name', 'comment-threshold');
+ metaTag.setAttribute('content', threshold);
+ document.head.appendChild(metaTag);
+
+ fetchStub.resolves(
+ new Response(JSON.stringify({
+ total: 4,
+ offset: 0,
+ limit: 4,
+ data: [
+ { country: 'all', total: '17', average: '3.5' },
+ { country: 'en', total: '6', average: '3.3' },
+ { country: 'fr', total: '3', average: '3.3' },
+ { country: 'de', total: '3', average: '3.3' },
+ ],
+ ':type': 'sheet',
+ }), {
+ status: 200,
+ headers: { 'Content-type': 'application/json' },
+ }),
+ );
+
+ await init(document.querySelector('.review'));
+ const review = await waitForElement('.hlx-ReviewWrapper');
+ const ratingInputs = review.querySelectorAll('.hlx-Review-ratingFields input');
+ await delay(125);
+ await ratingInputs[rating - 1].dispatchEvent(new Event('click'));
+ await delay(125);
+ expect(ratingInputs[rating - 1].getAttribute('aria-checked')).to.equal('true');
+ await delay(125);
+ const comment = document.querySelectorAll('#rating-comments');
+ if (rating <= threshold) {
+ expect(comment.length).to.equal(1);
+ } else {
+ expect(comment.length).to.equal(0);
+ }
+ await delay(125);
+ });
+ });
+ });
});
});
diff --git a/test/features/webapp-prompt/mocks/media-icon.png b/test/features/webapp-prompt/mocks/media-icon.png
new file mode 100644
index 0000000000..ae39a15393
Binary files /dev/null and b/test/features/webapp-prompt/mocks/media-icon.png differ
diff --git a/test/features/webapp-prompt/mocks/pep-prompt-content.js b/test/features/webapp-prompt/mocks/pep-prompt-content.js
new file mode 100644
index 0000000000..9f32193ae4
--- /dev/null
+++ b/test/features/webapp-prompt/mocks/pep-prompt-content.js
@@ -0,0 +1,31 @@
+export default ({ color, loaderDuration, redirectUrl, productName }) => `
+
+
+
+
Taking you to Creative Cloud
+
Cancel to stay on web page
+
Stay on this page
+
+
`;
diff --git a/test/features/webapp-prompt/test-utilities.js b/test/features/webapp-prompt/test-utilities.js
new file mode 100644
index 0000000000..d74376db6c
--- /dev/null
+++ b/test/features/webapp-prompt/test-utilities.js
@@ -0,0 +1,50 @@
+import { setViewport } from '@web/test-runner-commands';
+import sinon from 'sinon';
+import init from '../../../libs/features/webapp-prompt/webapp-prompt.js';
+import { viewports, mockRes as importedMockRes } from '../../blocks/global-navigation/test-utilities.js';
+import { getConfig, loadStyle, setConfig, updateConfig } from '../../../libs/utils/utils.js';
+
+export const allSelectors = {
+ fedsUtilities: '.feds-utilities',
+ pepWrapper: '.appPrompt',
+ closeIcon: '.appPrompt-close',
+ promptIcon: '.appPrompt-icon',
+ avatarImage: '.appPrompt-avatar-image',
+ title: '.appPrompt-title',
+ footer: '.appPrompt-footer',
+ subtitle: '.appPrompt-text',
+ cta: '.appPrompt-cta--close',
+ progressWrapper: '.appPrompt-progressWrapper',
+ progress: '.appPrompt-progress',
+ appSwitcher: '#unav-app-switcher',
+};
+
+export const defaultConfig = {
+ color: '#b30b00',
+ loaderDuration: 7500,
+ redirectUrl: 'https://www.adobe.com/?pep=true',
+ productName: 'photoshop',
+};
+
+export const mockRes = importedMockRes;
+
+export const initPep = async ({ entName = 'firefly-web-usage', isAnchorOpen = false, getAnchorStateMock = false }) => {
+ setConfig({
+ imsClientId: 'milo',
+ codeRoot: '/libs',
+ locales: { '': { ietf: 'en-US', tk: 'hah7vzn.css' } },
+ });
+ updateConfig({ ...getConfig(), entitlements: () => ['firefly-web-usage'] });
+ await setViewport(viewports.desktop);
+ await loadStyle('../../../libs/features/webapp-prompt/webapp-prompt.css');
+
+ const pep = await init({
+ promptPath: 'https://pep-mocks.test/pep-prompt-content.plain.html',
+ getAnchorState: getAnchorStateMock || (async () => ({ id: 'unav-app-switcher', isOpen: isAnchorOpen })),
+ entName,
+ parent: document.querySelector('div.feds-utilities'),
+ });
+
+ sinon.stub(pep, 'initRedirect').callsFake(() => null);
+ return pep;
+};
diff --git a/test/features/webapp-prompt/webapp-prompt.test.js b/test/features/webapp-prompt/webapp-prompt.test.js
new file mode 100644
index 0000000000..635b8ff6d8
--- /dev/null
+++ b/test/features/webapp-prompt/webapp-prompt.test.js
@@ -0,0 +1,189 @@
+import { expect } from '@esm-bundle/chai';
+import sinon, { stub } from 'sinon';
+import pepPromptContent from './mocks/pep-prompt-content.js';
+
+describe('PEP', () => {
+ let clock;
+ let allSelectors;
+ let defaultConfig;
+ let mockRes;
+ let initPep;
+
+ beforeEach(async () => {
+ clock = sinon.useFakeTimers({
+ toFake: ['setTimeout'],
+ shouldAdvanceTime: true,
+ });
+ // We need to import the utilities after mocking setTimeout to ensure
+ // their setTimeout calls use Sinon's mocked implementation.
+ // Importing before mocking would lead to a 5s PEP timeout, exceeding the 2s test limit.
+ const { allSelectors: importedAllSelectors, defaultConfig: importedDefaultConfig, mockRes: importedMockRes } = await import('./test-utilities.js');
+ allSelectors = importedAllSelectors;
+ defaultConfig = importedDefaultConfig;
+ mockRes = importedMockRes;
+ initPep = (await import('./test-utilities.js')).initPep;
+ document.body.innerHTML = ``;
+ stub(window, 'fetch').callsFake(async (url) => {
+ if (url.includes('pep-prompt-content.plain.html')) return mockRes({ payload: pepPromptContent({ ...defaultConfig }) });
+ return null;
+ });
+ });
+
+ afterEach(() => {
+ sinon.restore();
+ clock.restore();
+ document.body.innerHTML = '';
+ document.cookie = `${document.cookie};expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
+ });
+
+ describe('PEP rendering tests', () => {
+ it('should render PEP', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.exist;
+ });
+
+ it('should not render PEP when previously dismissed', async () => {
+ document.cookie = 'dismissedAppPrompts=["pep-prompt-content.plain.html"]';
+ await initPep({});
+ await clock.runAllAsync();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should not render PEP when the entitlement does not match', async () => {
+ await initPep({ entName: 'not-matching-entitlement' });
+ await clock.runAllAsync();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should not render PEP when there is no prompt content', async () => {
+ sinon.restore();
+ stub(window, 'fetch').callsFake(async (url) => {
+ if (url.includes('pep-prompt-content.plain.html')) {
+ return mockRes({
+ payload: null,
+ ok: false,
+ status: 400,
+ });
+ }
+ return null;
+ });
+ await initPep({});
+ await clock.runAllAsync();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should not render PEP when the redirect url or product name are not provided', async () => {
+ sinon.restore();
+ stub(window, 'fetch').callsFake(async (url) => {
+ if (url.includes('pep-prompt-content.plain.html')) return mockRes({ payload: pepPromptContent({ ...defaultConfig, redirectUrl: false, productName: false }) });
+ return null;
+ });
+ await initPep({});
+ await clock.runAllAsync();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should not render PEP when the anchor element is open', async () => {
+ await initPep({ isAnchorOpen: true });
+ await clock.runAllAsync();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+ });
+
+ describe('PEP configuration tests', () => {
+ it('should use config values when metadata loader color or duration are not provided', async () => {
+ sinon.restore();
+ stub(window, 'fetch').callsFake(async (url) => {
+ if (url.includes('pep-prompt-content.plain.html')) return mockRes({ payload: pepPromptContent({ ...defaultConfig, color: false, loaderDuration: false }) });
+ return null;
+ });
+ const pep = await initPep({});
+ await clock.runAllAsync();
+ const { 'loader-color': pepColor, 'loader-duration': pepDuration } = pep.options;
+ expect(!!pepColor && !!pepDuration).to.equal(true);
+ });
+ });
+
+ describe('PEP interaction tests', () => {
+ it('should close PEP on Escape key', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should close PEP on clicking the close icon', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ document.querySelector(allSelectors.closeIcon).click();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should close PEP on clicking the CTA', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ document.querySelector(allSelectors.cta).click();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+
+ it('should close PEP on clicking the anchor element', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ document.querySelector(allSelectors.appSwitcher).click();
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ });
+ });
+
+ describe('PEP focus tests', () => {
+ it('should focus on the close icon on initial render', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ expect(document.activeElement).to.equal(document.querySelector(allSelectors.closeIcon));
+ });
+
+ it('should focus on the anchor element after closing', async () => {
+ await initPep({});
+ await clock.runAllAsync();
+ document.querySelector(allSelectors.closeIcon).click();
+ expect(document.activeElement).to.equal(document.querySelector(allSelectors.appSwitcher));
+ });
+ });
+
+ describe('PEP logging tests', () => {
+ beforeEach(() => {
+ window.lana.log = sinon.spy();
+ });
+
+ it('should send log when not getting anchor state', async () => {
+ await initPep({
+ getAnchorStateMock: () => new Promise((resolve, reject) => {
+ reject(new Error('Cannot get anchor state'));
+ }),
+ });
+ await clock.runAllAsync();
+ expect(window.lana.log.getCalls().find((c) => c.args[0].includes('Error on getting anchor state'))).to.exist;
+ expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('errorType=error,module=pep'))).to.exist;
+ });
+
+ it('should send log when cannot fetch content for prompt', async () => {
+ sinon.restore();
+ stub(window, 'fetch').callsFake(async (url) => {
+ if (url.includes('pep-prompt-content.plain.html')) {
+ return mockRes({
+ payload: null,
+ ok: false,
+ status: 400,
+ });
+ }
+ return null;
+ });
+ await initPep({});
+ await clock.runAllAsync();
+ expect(window.lana.log.getCalls().find((c) => c.args[0].includes('Error fetching content for prompt'))).to.exist;
+ expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('errorType=error,module=pep'))).to.exist;
+ });
+ });
+});