diff --git a/libs/features/webapp-prompt/webapp-prompt.css b/libs/features/webapp-prompt/webapp-prompt.css
index cd421f6f8a..1fbc0baae8 100644
--- a/libs/features/webapp-prompt/webapp-prompt.css
+++ b/libs/features/webapp-prompt/webapp-prompt.css
@@ -188,118 +188,3 @@
}
}
}
-
-/* DISMISSAL TOOLTIP */
-
-[data-pep-dismissal-tooltip]::after {
- content: attr(data-pep-dismissal-tooltip);
- display: inline-flex;
- z-index: 3;
- height: fit-content;
- width: 8.875rem;
- top: 125%;
- left: -300%;
- word-break: break-word;
- border-radius: 7px;
-
- padding-inline: 0.5625rem;
- padding-block: 0.25rem 0.3125rem;
-
- font-family: var(--body-font-family);
- font-size: 0.75rem;
- line-height: 0.9375rem;
- color: white;
-}
-
-[data-pep-dismissal-tooltip]::before {
- content: '';
- z-index: 2;
- width: 0.44rem;
- height: 0.44rem;
- border-radius: 0.05rem;
- left: calc(50% - 0.22rem);
- top: 115%;
-
- transform: rotate(45deg);
-}
-
-[data-pep-dismissal-tooltip]::before,
-[data-pep-dismissal-tooltip]::after {
- background-color: #3B63FB;
- position: absolute;
- pointer-events: none;
- transition: opacity 0.5s;
-}
-
-@media (min-width: 1520px) {
- [data-pep-dismissal-tooltip]::after {
- left: calc(50% - 5rem);
- }
-}
-
-/* DISMISSAL ANIMATION */
-
-.coach-indicator {
- --coach-indicator-ring-default-color: rgba(56,146,243);
- --coach-indicator-ring-diameter: 1.25rem;
- --coach-indicator-ring-border-size: 2px;
- --coach-indicator-ring-inline-size: var(--coach-indicator-ring-diameter);
- --coach-indicator-ring-block-size: var(--coach-indicator-ring-diameter);
- --coach-indicator-first-ring-delay-fraction: 0;
- --coach-indicator-second-ring-delay-fraction: 0.33;
- --coach-indicator-third-ring-delay-fraction: 0.66;
- --animation-duration: 3000ms;
-}
-
-@keyframes pulse {
- 0% {
- transform: scale(0.8);
- opacity: 0;
- }
-
- 50% {
- transform: scale(1.5);
- opacity: 1;
- }
-
- 100% {
- transform: scale(2);
- opacity: 0;
- }
-}
-
-.coach-indicator .coach-indicator-ring {
- display: block;
- position: absolute;
- top: 14%;
- left: 13%;
-
- border-style: solid;
- border-width: var(--coach-indicator-ring-border-size);
- border-color: var(--coach-indicator-ring-default-color);
-
- inline-size: var(--coach-indicator-ring-inline-size);
- block-size: var(--coach-indicator-ring-block-size);
- animation: pulse var(--animation-duration) linear;
- animation-fill-mode: both;
-
- border-radius: 5px;
-}
-
-.coach-indicator .coach-indicator-ring:nth-child(1) {
- animation-delay: calc(var(--animation-duration)*var(--coach-indicator-first-ring-delay-fraction));
-}
-
-.coach-indicator .coach-indicator-ring:nth-child(2) {
- animation-delay: calc(var(--animation-duration)*var(--coach-indicator-second-ring-delay-fraction));
-}
-
-.coach-indicator .coach-indicator-ring:nth-child(3) {
- animation-delay: calc(var(--animation-duration)*var(--coach-indicator-third-ring-delay-fraction));
-}
-
-@media (prefers-reduced-motion: reduce) {
- .coach-indicator .coach-indicator-ring {
- animation: none;
- }
-}
diff --git a/libs/features/webapp-prompt/webapp-prompt.js b/libs/features/webapp-prompt/webapp-prompt.js
index d41207135c..418d2ad68d 100644
--- a/libs/features/webapp-prompt/webapp-prompt.js
+++ b/libs/features/webapp-prompt/webapp-prompt.js
@@ -8,21 +8,13 @@ import {
import { getConfig, decorateSVG } from '../../utils/utils.js';
import { replaceKey, replaceText } from '../placeholders.js';
-export const DISMISSAL_CONFIG = {
- animationCount: 2,
- animationDuration: 2500,
- tooltipMessage: 'Use the App Switcher to quickly find apps.',
- tooltipDuration: 5000,
-};
-
const CONFIG = {
selectors: { prompt: '.appPrompt' },
delay: 7000,
loaderColor: '#EB1000',
- ...DISMISSAL_CONFIG,
};
-const getElemText = (elem) => elem?.textContent?.trim();
+const getElemText = (elem) => elem?.textContent?.trim().toLowerCase();
const getMetadata = (el) => [...el.childNodes].reduce((acc, row) => {
if (row.children?.length === 2) {
@@ -43,54 +35,6 @@ const getIcon = (content) => {
return icons.company;
};
-const showTooltip = (
- element,
- message = CONFIG.tooltipMessage,
- time = CONFIG.tooltipDuration,
-) => {
- element.setAttribute('data-pep-dismissal-tooltip', message);
- const cleanup = () => element.removeAttribute('data-pep-dismissal-tooltip');
- const timeoutID = setTimeout(cleanup, time);
- element.addEventListener('click', () => {
- cleanup();
- clearTimeout(timeoutID);
- }, { once: true });
-};
-
-const playFocusAnimation = (
- element,
- iterationCount = CONFIG.animationCount,
- animationDuration = CONFIG.animationDuration,
-) => {
- element.classList.add('coach-indicator');
- element.style.setProperty('--animation-duration', `${animationDuration}ms`);
- const rings = [];
- const createRing = () => toFragment`
-
${this.elements.closeIcon}
@@ -263,7 +200,7 @@ class AppPrompt {
};
addEventListeners = () => {
- this.anchor?.addEventListener('click', () => this.close({ dismissalActions: false }));
+ this.anchor?.addEventListener('click', this.close);
document.addEventListener('keydown', this.handleKeyDown);
[this.elements.closeIcon, this.elements.cta]
@@ -274,11 +211,9 @@ class AppPrompt {
if (event.key === 'Escape') this.close();
};
- static redirectTo = (url) => window.location.assign(url);
-
initRedirect = () => setTimeout(() => {
- this.close({ saveDismissal: false, dismissalActions: false });
- this.redirectTo(this.options['redirect-url']);
+ this.close({ saveDismissal: false });
+ window.location.assign(this.options['redirect-url']);
}, this.options['loader-duration']);
isDismissedPrompt = () => AppPrompt.getDismissedPrompts().includes(this.id);
@@ -289,7 +224,7 @@ class AppPrompt {
document.cookie = `dismissedAppPrompts=${JSON.stringify([...dismissedPrompts])};path=/`;
};
- close = ({ saveDismissal = true, dismissalActions = true } = {}) => {
+ close = ({ saveDismissal = true } = {}) => {
const appPromptElem = document.querySelector(CONFIG.selectors.prompt);
appPromptElem?.remove();
clearTimeout(this.redirectFn);
@@ -297,19 +232,6 @@ class AppPrompt {
document.removeEventListener('keydown', this.handleKeyDown);
this.anchor?.focus();
this.anchor?.removeEventListener('click', this.close);
-
- if (dismissalActions) {
- playFocusAnimation(
- this.anchor,
- this.options['dismissal-animation-count'],
- this.options['dismissal-animation-duration'],
- );
- showTooltip(
- this.anchor,
- this.options['dismissal-tooltip-message'],
- this.options['dismissal-tooltip-duration'],
- );
- }
};
static getDismissedPrompts = () => {
@@ -324,8 +246,7 @@ class AppPrompt {
export default async function init(config) {
try {
- const appPrompt = new AppPrompt(config);
- if (!appPrompt.initializationQueued) await appPrompt.init();
+ const appPrompt = await new AppPrompt(config);
return appPrompt;
} catch (e) {
lanaLog({ message: 'Could not initialize PEP', e, tags: 'errorType=error,module=pep' });
diff --git a/test/features/webapp-prompt/mocks/pep-prompt-content.js b/test/features/webapp-prompt/mocks/pep-prompt-content.js
index 165185e344..9f32193ae4 100644
--- a/test/features/webapp-prompt/mocks/pep-prompt-content.js
+++ b/test/features/webapp-prompt/mocks/pep-prompt-content.js
@@ -1,13 +1,4 @@
-export default ({
- color,
- loaderDuration,
- redirectUrl,
- productName,
- animationCount,
- animationDuration,
- tooltipMessage,
- tooltipDuration,
-}) => `
+export default ({ color, loaderDuration, redirectUrl, productName }) => `
`}
- ${animationCount && `
-
dismissal-animation-count
-
${animationCount}
-
`}
- ${animationDuration && `
-
dismissal-animation-duration
-
${animationDuration}
-
`}
- ${tooltipMessage && `
-
dismissal-tooltip-message
-
${tooltipMessage}
-
`}
- ${tooltipDuration && `
-
dismissal-tooltip-duration
-
${tooltipDuration}
-
`}
`;
diff --git a/test/features/webapp-prompt/test-utilities.js b/test/features/webapp-prompt/test-utilities.js
index 7905390380..d74376db6c 100644
--- a/test/features/webapp-prompt/test-utilities.js
+++ b/test/features/webapp-prompt/test-utilities.js
@@ -1,8 +1,7 @@
import { setViewport } from '@web/test-runner-commands';
import sinon from 'sinon';
-import init, { DISMISSAL_CONFIG } from '../../../libs/features/webapp-prompt/webapp-prompt.js';
+import init from '../../../libs/features/webapp-prompt/webapp-prompt.js';
import { viewports, mockRes as importedMockRes } from '../../blocks/global-navigation/test-utilities.js';
-import { setUserProfile } from '../../../libs/blocks/global-navigation/utilities/utilities.js';
import { getConfig, loadStyle, setConfig, updateConfig } from '../../../libs/utils/utils.js';
export const allSelectors = {
@@ -18,8 +17,6 @@ export const allSelectors = {
progressWrapper: '.appPrompt-progressWrapper',
progress: '.appPrompt-progress',
appSwitcher: '#unav-app-switcher',
- indicatorRing: '.coach-indicator-ring',
- tooltip: '[data-pep-dismissal-tooltip]',
};
export const defaultConfig = {
@@ -27,7 +24,6 @@ export const defaultConfig = {
loaderDuration: 7500,
redirectUrl: 'https://www.adobe.com/?pep=true',
productName: 'photoshop',
- ...DISMISSAL_CONFIG,
};
export const mockRes = importedMockRes;
@@ -42,7 +38,6 @@ export const initPep = async ({ entName = 'firefly-web-usage', isAnchorOpen = fa
await setViewport(viewports.desktop);
await loadStyle('../../../libs/features/webapp-prompt/webapp-prompt.css');
- setUserProfile({});
const pep = await init({
promptPath: 'https://pep-mocks.test/pep-prompt-content.plain.html',
getAnchorState: getAnchorStateMock || (async () => ({ id: 'unav-app-switcher', isOpen: isAnchorOpen })),
@@ -50,10 +45,6 @@ export const initPep = async ({ entName = 'firefly-web-usage', isAnchorOpen = fa
parent: document.querySelector('div.feds-utilities'),
});
- Object.setPrototypeOf(pep, {
- ...Object.getPrototypeOf(pep),
- redirectTo: sinon.stub().returns({}),
- });
-
+ 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
index 38a89acfbc..c3e6bf6c6c 100644
--- a/test/features/webapp-prompt/webapp-prompt.test.js
+++ b/test/features/webapp-prompt/webapp-prompt.test.js
@@ -3,12 +3,17 @@ 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.
@@ -28,6 +33,7 @@ describe('PEP', () => {
afterEach(() => {
sinon.restore();
+ clock.restore();
document.body.innerHTML = '';
document.cookie = `${document.cookie};expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
});
@@ -35,17 +41,20 @@ describe('PEP', () => {
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;
});
@@ -62,6 +71,7 @@ describe('PEP', () => {
return null;
});
await initPep({});
+ await clock.runAllAsync();
expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
});
@@ -72,22 +82,27 @@ describe('PEP', () => {
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;
});
it('should not render PEP when the GRM is open', async () => {
- const clock = sinon.useFakeTimers();
document.body.insertAdjacentHTML('afterbegin', '
');
document.body.insertAdjacentHTML('afterbegin', '
');
await initPep({});
- expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ try {
+ clock.runAll();
+ } catch (e) {
+ expect(document.querySelector(allSelectors.pepWrapper)).to.not.exist;
+ }
const event = new CustomEvent('milo:modal:closed');
window.dispatchEvent(event);
@@ -95,135 +110,70 @@ describe('PEP', () => {
document.querySelector('.locale-modal-v2')?.remove();
document.querySelector('.dialog-modal')?.remove();
- await clock.tickAsync(300);
-
+ await clock.runAllAsync();
expect(document.querySelector(allSelectors.pepWrapper)).to.exist;
- clock.uninstall();
});
});
describe('PEP configuration tests', () => {
- it('should use config values when metadata loader color, duration, or dismissal options are not provided', async () => {
+ 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,
- animationCount: false,
- animationDuration: false,
- tooltipMessage: false,
- tooltipDuration: false,
- }),
- });
- }
+ if (url.includes('pep-prompt-content.plain.html')) return mockRes({ payload: pepPromptContent({ ...defaultConfig, color: false, loaderDuration: false }) });
return null;
});
const pep = await initPep({});
- const {
- 'loader-color': pepColor,
- 'loader-duration': pepDuration,
- 'dismissal-animation-count': animCount,
- 'dismissal-animation-duration': animDuration,
- 'dismissal-tooltip-message': tooltipMessage,
- 'dismissal-tooltip-duration': tooltipDuration,
- } = pep.options;
- const configPresent = [
- pepColor,
- pepDuration,
- animCount,
- animDuration,
- tooltipMessage,
- tooltipDuration,
- ].reduce((acc, x) => acc && !!x, true);
- expect(configPresent).to.equal(true);
+ 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;
});
-
- it('redirects when the PEP timer runs out', async () => {
- const clock = sinon.useFakeTimers();
- const pep = await initPep({});
-
- clock.tick(10000);
- // redirectTo is mocked in test-utilities inside the initPep procedure
- expect(pep.redirectTo.calledOnce).to.equal(true);
- clock.uninstall();
- });
});
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 dismissal tests', () => {
- it('adds three rings to the app switcher and removes them after the required amount of time', async () => {
- const clock = sinon.useFakeTimers();
- await initPep({});
-
- document.querySelector(allSelectors.closeIcon).click();
- expect([...document.querySelectorAll(allSelectors.indicatorRing)].length).to.equal(3);
- clock.tick(7500);
- expect([...document.querySelectorAll(allSelectors.indicatorRing)].length).to.equal(0);
- clock.uninstall();
- });
-
- it('adds a data attribute to the app switcher with the correct data and removes it after the allotted time', async () => {
- const clock = sinon.useFakeTimers();
- await initPep({});
-
- document.querySelector(allSelectors.closeIcon).click();
- expect(document.querySelector(allSelectors.tooltip)).to.exist;
-
- clock.tick(5000);
- expect(document.querySelector(allSelectors.tooltip)).to.not.exist;
- clock.uninstall();
- });
-
- it('removes the dismissal animation and the tooltip upon clicking the anchor element', async () => {
- await initPep({});
- document.querySelector(allSelectors.closeIcon).click();
- expect(document.querySelector(allSelectors.tooltip)).to.exist;
- document.querySelector(allSelectors.appSwitcher).click();
- expect(document.querySelector(allSelectors.tooltip)).to.not.exist;
- });
- });
-
describe('PEP logging tests', () => {
beforeEach(() => {
window.lana.log = sinon.spy();
@@ -235,6 +185,7 @@ describe('PEP', () => {
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;
});
@@ -252,6 +203,7 @@ describe('PEP', () => {
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;
});