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` -
-
`; - for (let i = 0; i < 3; i += 1) { - const ring = createRing(); - element.insertAdjacentElement('afterbegin', ring); - rings.push(ring); - } - // The cleanup function is added to the event queue - // some time after the end of the animation because - // the cleanup isn't high priority but it should be done - // eventually. (Animation truly ends slightly after - // animationDuration * iterationCount due to animation-delay) - const cleanup = () => { - rings.forEach((ring) => ring.remove()); - element.classList.remove('coach-indicator'); - }; - const timeoutID = setTimeout(cleanup, (iterationCount + 1) * animationDuration); - element.addEventListener('click', () => { - cleanup(); - clearTimeout(timeoutID); - }, { once: true }); -}; - const modalsActive = () => !!document.querySelector('.dialog-modal'); const waitForClosedModalsThen = (loadPEP) => { @@ -115,10 +59,7 @@ class AppPrompt { () => waitForClosedModalsThen(this.init), { once: true }, ); - this.initializationQueued = true; - return; - } - this.initializationQueued = false; + } else this.init(); } init = async () => { @@ -221,10 +162,6 @@ class AppPrompt { 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; - metadata['dismissal-animation-count'] = parseInt(metadata['dismissal-animation-count'] ?? CONFIG.animationCount, 10); - metadata['dismissal-animation-duration'] = parseInt(metadata['dismissal-animation-duration'] ?? CONFIG.animationDuration, 10); - metadata['dismissal-tooltip-message'] ??= CONFIG.tooltipMessage; - metadata['dismissal-tooltip-duration'] = parseInt(metadata['dismissal-tooltip-duration'] ?? CONFIG.tooltipDuration, 10); this.options = metadata; }; @@ -244,7 +181,7 @@ class AppPrompt { : ''; return 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 }) => `

@@ -36,21 +27,5 @@ export default ({

product-name
${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; });