From 21a839a7fa308857b5aa409cd109ca323c6ae69e Mon Sep 17 00:00:00 2001 From: Blaine Gunn Date: Tue, 14 Nov 2023 16:18:52 -0700 Subject: [PATCH 1/8] Syncing Stage (#1538) Getting stage in sync w/ main --- .github/workflows/run-nala-circleci.yml | 18 ++ libs/blocks/aside/aside.css | 13 +- .../utilities/keyboard/utils.js | 1 + libs/blocks/gnav/gnav-contextual-search.js | 19 +- libs/blocks/gnav/gnav.css | 6 + libs/blocks/gnav/gnav.js | 26 +- libs/blocks/quiz/quiz.css | 16 + libs/blocks/quiz/quiz.js | 100 +++---- robots.txt | 5 - .../global-footer/global-footer.test.js | 275 ++++++++++++++++++ .../blocks/global-footer/mocks/base-footer.js | 1 + .../global-footer/mocks/fetched-footer.js | 144 +++++++++ test/blocks/global-footer/mocks/icons.js | 54 ++++ .../blocks/global-footer/mocks/media-icon.png | Bin 0 -> 786 bytes test/blocks/global-footer/test-utilities.js | 98 +++++++ .../global-navigation.test.js | 2 +- web-test-runner.config.mjs | 1 + 17 files changed, 688 insertions(+), 91 deletions(-) create mode 100644 .github/workflows/run-nala-circleci.yml delete mode 100644 robots.txt create mode 100644 test/blocks/global-footer/global-footer.test.js create mode 100644 test/blocks/global-footer/mocks/base-footer.js create mode 100644 test/blocks/global-footer/mocks/fetched-footer.js create mode 100644 test/blocks/global-footer/mocks/icons.js create mode 100644 test/blocks/global-footer/mocks/media-icon.png create mode 100644 test/blocks/global-footer/test-utilities.js diff --git a/.github/workflows/run-nala-circleci.yml b/.github/workflows/run-nala-circleci.yml new file mode 100644 index 0000000000..a2a7e963bf --- /dev/null +++ b/.github/workflows/run-nala-circleci.yml @@ -0,0 +1,18 @@ +name: Nala Tests on CircleCI + +on: + push: + branches: + - stage + +jobs: + trigger-circleci: + name: Trigger CircleCI Job + if: github.repository == 'adobecom/milo' + runs-on: [self-hosted, Linux, X64] + steps: + - run: | + curl -X POST 'https://circle.ci.adobe.com/api/v2/project/gh/wcms/nala/pipeline' \ + -H 'Circle-Token: ${{ secrets.CCI_TOKEN }}' \ + -H 'content-type: application/json' \ + -d "{\"branch\":\"main\"}" \ No newline at end of file diff --git a/libs/blocks/aside/aside.css b/libs/blocks/aside/aside.css index a8e0612ca0..4c9362d53f 100644 --- a/libs/blocks/aside/aside.css +++ b/libs/blocks/aside/aside.css @@ -997,15 +997,16 @@ flex-direction: row; } + .aside.promobar .foreground.container .icon-area, + .aside.promobar .foreground.container .icon-area img { + height: var(--icon-size-xxl); + } + .aside.promobar.popup .promo-text .icon-area, .aside.promobar.popup .promo-text .icon-area img { height: var(--icon-size-m); } - .aside.promobar .foreground.container .icon-area img { - height: var(--icon-size-xxl); - } - .aside.center:not(.notification) .foreground.container .text .icon-area img { max-width: 400px; } @@ -1065,10 +1066,6 @@ margin-right: 0; } - .aside.promobar .foreground.container .icon-area { - height: var(--icon-size-xxl); - } - .aside.promobar .promo-text .content-area { gap: var(--spacing-m); } diff --git a/libs/blocks/global-navigation/utilities/keyboard/utils.js b/libs/blocks/global-navigation/utilities/keyboard/utils.js index e4c4c484cd..6eb106a259 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/utils.js +++ b/libs/blocks/global-navigation/utilities/keyboard/utils.js @@ -49,6 +49,7 @@ selectors.popupItems = ` ${selectors.privacyLink} `; +// This method covers focusable elements only, so we aren’t interested in SVGs for example. const isElementVisible = (elem) => !!( elem && elem instanceof HTMLElement diff --git a/libs/blocks/gnav/gnav-contextual-search.js b/libs/blocks/gnav/gnav-contextual-search.js index bd06fd6f7c..1ff01119e2 100644 --- a/libs/blocks/gnav/gnav-contextual-search.js +++ b/libs/blocks/gnav/gnav-contextual-search.js @@ -1,6 +1,7 @@ import { fetchBlogArticleIndex } from '../article-feed/article-feed.js'; import { getArticleTaxonomy, buildArticleCard } from '../article-feed/article-helpers.js'; -import { createTag } from '../../utils/utils.js'; +import { createTag, getConfig } from '../../utils/utils.js'; +import { replaceKey } from '../../features/placeholders.js'; let abortController; let articles = []; @@ -97,11 +98,17 @@ export default async function onSearchInput({ value, resultsEl, searchInputEl, a if (currentSearch === lastSearch) { if (!hits.length) { - const advancedLink = advancedSearchEl.querySelector('a'); - const href = new URL(advancedLink.href); - href.searchParams.set('q', value); - advancedLink.href = href.toString(); - resultsEl.replaceChildren(advancedSearchEl); + const noResults = await replaceKey('no-results', getConfig()); + const emptyMessage = createTag('p', {}, noResults); + let emptyList = createTag('li', null, emptyMessage); + if (advancedSearchEl) { + const advancedLink = advancedSearchEl.querySelector('a'); + const href = new URL(advancedLink.href); + href.searchParams.set('q', value); + advancedLink.href = href.toString(); + emptyList = advancedSearchEl; + } + resultsEl.replaceChildren(emptyList); resultsEl.classList.add('no-results'); return; } diff --git a/libs/blocks/gnav/gnav.css b/libs/blocks/gnav/gnav.css index 51584a3787..eb662ca2e0 100644 --- a/libs/blocks/gnav/gnav.css +++ b/libs/blocks/gnav/gnav.css @@ -392,6 +392,7 @@ header .gnav-search-field:hover .gnav-search-input::placeholder { height: 98%; } +.gnav-search-results > ul li > p, .gnav-search-results > ul li > a { color: #2d2d2d; display: block; @@ -399,6 +400,7 @@ header .gnav-search-field:hover .gnav-search-input::placeholder { font-weight: 700; line-height: 1; padding: 5px 10px; + margin: 0; } .gnav-search-results > ul li > a span { @@ -1183,6 +1185,10 @@ header .app-launcher { box-sizing: border-box; } + .gnav .gnav-logo svg { + width: 100%; + } + /* Search Bar */ .gnav-search.is-open .gnav-search-bar { position: fixed; diff --git a/libs/blocks/gnav/gnav.js b/libs/blocks/gnav/gnav.js index d19b223514..d192182155 100644 --- a/libs/blocks/gnav/gnav.js +++ b/libs/blocks/gnav/gnav.js @@ -109,6 +109,7 @@ class Gnav { decorateToggle = () => { const toggle = createTag('button', { class: 'gnav-toggle', 'aria-label': 'Navigation menu', 'aria-expanded': false }); + let onMediaChange; const closeToggleOnDocClick = ({ target }) => { if (target !== toggle && !target.closest('.mainnav-wrapper')) { this.el.classList.remove(IS_OPEN); @@ -116,7 +117,7 @@ class Gnav { document.removeEventListener('click', closeToggleOnDocClick); } }; - const onMediaChange = (e) => { + onMediaChange = (e) => { if (e.matches) { this.el.classList.remove(IS_OPEN); document.removeEventListener('click', closeToggleOnDocClick); @@ -186,7 +187,7 @@ class Gnav { buildMainNav = (mainNav, navLinks) => { navLinks.forEach((navLink, idx) => { if (navLink.parentElement.nodeName === 'STRONG') { - const cta = this.decorateCta(navLink); + const cta = Gnav.decorateCta(navLink); mainNav.append(cta); return; } @@ -203,7 +204,7 @@ class Gnav { const id = `navmenu-${idx}`; menu.id = id; navItem.classList.add('has-menu'); - this.setNavLinkAttributes(id, navLink); + Gnav.setNavLinkAttributes(id, navLink); } // Small and medium menu types if (menu.childElementCount > 0) { @@ -223,7 +224,7 @@ class Gnav { return mainNav; }; - setNavLinkAttributes = (id, navLink) => { + static setNavLinkAttributes = (id, navLink) => { navLink.setAttribute('role', 'button'); navLink.setAttribute('aria-expanded', false); navLink.setAttribute('aria-controls', id); @@ -231,7 +232,7 @@ class Gnav { navLink.setAttribute('daa-lh', 'header|Open'); }; - decorateLinkGroups = (menu) => { + static decorateLinkGroups = (menu) => { const linkGroups = menu.querySelectorAll('.link-group'); linkGroups.forEach((linkGroup) => { const image = linkGroup.querySelector('picture'); @@ -280,7 +281,7 @@ class Gnav { decorateAnalytics = (menu) => [...menu.children].forEach((child) => this.setMenuAnalytics(child)); - decorateButtons = (menu) => { + static decorateButtons = (menu) => { const buttons = menu.querySelectorAll('strong a'); buttons.forEach((btn) => { btn.classList.add('con-button', 'filled', 'blue', 'button-m'); @@ -302,7 +303,7 @@ class Gnav { decorateLinks(container); menu.append(container); } - this.decorateLinkGroups(menu); + Gnav.decorateLinkGroups(menu); this.decorateAnalytics(menu); navLink.addEventListener('focus', () => { window.addEventListener('keydown', this.toggleOnSpace); @@ -314,7 +315,7 @@ class Gnav { e.preventDefault(); this.toggleMenu(navItem); }); - this.decorateButtons(menu); + Gnav.decorateButtons(menu); return menu; }; @@ -343,7 +344,7 @@ class Gnav { }); }; - decorateCta = (cta) => { + static decorateCta = (cta) => { if (cta) { const { origin } = new URL(cta.href); if (origin !== window.location.origin) { @@ -420,7 +421,7 @@ class Gnav { }); searchInput.addEventListener('keydown', (e) => { - if (e.code === 'Enter') { + if (advancedSearchEl && e.code === 'Enter') { window.open(this.getHelpxLink(e.target.value, locale.prefix, locale.geo)); } }); @@ -430,7 +431,7 @@ class Gnav { return searchBar; }; - getNoResultsEl = (advancedSearchEl) => createTag('li', null, advancedSearchEl); + static getNoResultsEl = (advancedSearchEl) => createTag('li', null, advancedSearchEl); /* c8 ignore start */ getAppLauncher = async (profileEl) => { @@ -495,7 +496,7 @@ class Gnav { profileEl.insertAdjacentElement('beforeend', dropDown); this.decorateMenu(profileEl, signIn, dropDown); - this.setNavLinkAttributes(id, signIn); + Gnav.setNavLinkAttributes(id, signIn); } signInEl.addEventListener('click', (e) => { e.preventDefault(); @@ -658,6 +659,7 @@ export default async function init(header) { header.setAttribute('daa-lh', `gnav${name}`); return gnav; } catch (e) { + // eslint-disable-next-line no-console console.log('Could not create global navigation:', e); return null; } diff --git a/libs/blocks/quiz/quiz.css b/libs/blocks/quiz/quiz.css index cf90a2386e..c1aa49bcf2 100644 --- a/libs/blocks/quiz/quiz.css +++ b/libs/blocks/quiz/quiz.css @@ -80,6 +80,10 @@ margin-right: 16px; } +html[dir="rtl"] .quiz-option-icon { + margin-left: 16px; +} + .quiz-option-icon img { height: var(--icon-size-m); width: var(--icon-size-m); @@ -94,6 +98,10 @@ width: 46px; } +html[dir="rtl"] .quiz-option-image { + margin-left: 16px; +} + .quiz-option-title { color: var(--text-color); display: none; @@ -213,6 +221,10 @@ width: calc(100% - 23px) /* width of .quiz-step minus the width of a single dot */; } +html[dir="rtl"] .quiz-step::after { + margin-right: 20px; +} + .quiz-step.current::before { background-color: var(--color-black); border-color: var(--color-black); @@ -315,6 +327,10 @@ min-height: 126px; } + html[dir="rtl"] .quiz-option-icon { + margin-left: 0; + } + .quiz-option-icon img { height: var(--icon-size-xl); width: var(--icon-size-xl); diff --git a/libs/blocks/quiz/quiz.js b/libs/blocks/quiz/quiz.js index 8f47adf4c1..834dcf5672 100644 --- a/libs/blocks/quiz/quiz.js +++ b/libs/blocks/quiz/quiz.js @@ -23,7 +23,6 @@ const App = ({ preQuestions = {}, initialStrings = {}, }) => { const [btnAnalytics, setBtnAnalytics] = useState(null); - const [isBtnClicked, setIsBtnClicked] = useState(false); const [countSelectedCards, setCountOfSelectedCards] = useState(0); const [currentStep, setCurrentStep] = useState(0); const [isDataLoaded, setDataLoaded] = useState(initialIsDataLoaded); @@ -37,11 +36,10 @@ const App = ({ const [stringQList, setStringQList] = useState(preQuestions.stringQList || {}); const [totalSteps, setTotalSteps] = useState(3); const initialUrlParams = getUrlParams(); - const [urlParam, setUrlParam] = useState(initialUrlParams); const [userSelection, updateUserSelection] = useState([]); const [userFlow, setUserFlow] = useState([]); const validQuestions = useMemo(() => [], []); - const knownParams = useMemo(() => ['martech', 'milolibs', 'quiz-data'], []); + const [debugBuild, setDebugBuild] = useState(null); useEffect(() => { (async () => { @@ -70,17 +68,25 @@ const App = ({ }, [setQuestionData, setStringData, setStringQList, setQuestionList]); useEffect(() => { - function handlePopState() { - window.location.reload(); - } + const quizDebugValue = initialUrlParams['debug-results']; + const handleDebugResults = () => { + if (quizDebugValue && quizDebugValue.length > 1) { + const quizDebugValueDecodedJSON = JSON.parse(decodeURIComponent(quizDebugValue)); + if (userSelection.length > 0) { + setNextQuizViewsExist(false); + } else { + updateUserSelection(quizDebugValueDecodedJSON); + } + } + }; if (isDataLoaded) { - window.addEventListener('popstate', handlePopState); - return () => { - window.removeEventListener('popstate', handlePopState); - }; + if (debugBuild === false) { + handleDebugResults(); + } } return () => {}; - }, [knownParams, isDataLoaded]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDataLoaded, debugBuild, userSelection]); useEffect(() => { if (userFlow && userFlow.length) { @@ -119,60 +125,38 @@ const App = ({ */ useEffect(() => { if (!nextQuizViewsExist && userSelection.length) { + const debugParam = initialUrlParams['debug-results']; + if (debugParam) { + const userSelectionString = JSON.stringify(userSelection); + const userSelectionStringEncoded = encodeURIComponent(userSelectionString); + const cleanURL = window.location.href.split('?')[0]; + const debugURL = `${cleanURL}?debug-results=${userSelectionStringEncoded}`; + window.history.replaceState('', '', debugURL); + navigator.clipboard.writeText(debugURL).then(() => { + // eslint-disable-next-line no-console + console.log(debugURL); + }).catch((err) => { + // eslint-disable-next-line no-console + console.log(`Error copying URL: ${err} URL: ${debugURL}`); + }); + } handleResultFlow(transformToFlowData(userSelection)); } - }, [userSelection, nextQuizViewsExist]); - - /** - * Updates the url param when user selects the options. - * Happens with each option click/tap. - */ - useEffect(() => { - if (!selectedQuestion) return; - const { questions } = selectedQuestion; - const cardValues = Object.getOwnPropertyNames(selectedCards); - setUrlParam((prevUrlParam) => { - const newParam = { ...prevUrlParam }; - if (selectedQuestion && cardValues.length === 0) { - delete newParam[questions]; - } else if (!urlParam[questions]) { - newParam[questions] = new Set( - [...(urlParam[questions] || []), ...cardValues], - ); - } else { - newParam[questions] = new Set(cardValues); - } - return newParam; - }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedQuestion, selectedCards, JSON.stringify(urlParam)]); + }, [userSelection, nextQuizViewsExist]); /** * Updates the url when the url param is updated as part of the option click. */ useLayoutEffect(() => { - if (Object.keys(urlParam).length > 0 && isBtnClicked === true) { - let urlParamList = Object.keys(urlParam).map((key) => { - const paramList = [...urlParam[key]]; - if (paramList.length) { - return `${key}=${paramList.join(',')}`; - } - return null; // Explicitly return null if the condition is not met - }).filter((item) => !!item && !knownParams.includes(item.split('=')[0])); - const knownParamsList = knownParams - .filter((key) => key in urlParam) - .map((key) => `${key}=${urlParam[key].join(',')}`); - urlParamList = [...urlParamList, ...knownParamsList]; - if (knownParamsList.length === 1 && isBtnClicked === false) { - const newURL = knownParamsList && knownParamsList.length > 0 ? `?${knownParamsList.join('&')}` : ''; - window.history.pushState('', '', newURL); - } else { - window.history.pushState('', '', `?${urlParamList.join('&')}`); - } - setIsBtnClicked(false); + const quizDebug = initialUrlParams['debug-results']; + if (quizDebug && quizDebug.length < 1) { + setDebugBuild(true); + } else { + setDebugBuild(false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [urlParam]); + }, [initialUrlParams]); /** * Updates the prevStepIndicator when user selects the options. @@ -202,7 +186,6 @@ const App = ({ * @returns {void} */ const handleOnNextClick = (selCards) => { - setIsBtnClicked(true); const { nextQuizViews } = handleNext( questionData, selectedQuestion, @@ -249,7 +232,6 @@ const App = ({ * @returns {void} * */ const onOptionClick = (option) => () => { - setIsBtnClicked(true); const newState = { ...selectedCards }; if (Object.keys(newState).length >= maxSelections && !newState[option.options]) { @@ -295,14 +277,14 @@ const App = ({ }; return html`
- ${selectedQuestion.questions && getStringValue('background') !== '' && html`<${StepIndicator} + ${selectedQuestion.questions && html`<${StepIndicator} currentStep=${currentStep} totalSteps=${totalSteps} prevStepIndicator=${prevStepIndicator} top="${true}" /> `} - ${selectedQuestion.questions && html`
+ ${selectedQuestion.questions && getStringValue('background') !== '' && html`
${DecorateBlockBackground(getStringValue)}
`} diff --git a/robots.txt b/robots.txt deleted file mode 100644 index 48d737c169..0000000000 --- a/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -User-agent: Googlebot -Disallow: /nogooglebot/ - -User-agent: * -Allow: / diff --git a/test/blocks/global-footer/global-footer.test.js b/test/blocks/global-footer/global-footer.test.js new file mode 100644 index 0000000000..6296161082 --- /dev/null +++ b/test/blocks/global-footer/global-footer.test.js @@ -0,0 +1,275 @@ +import { expect } from '@esm-bundle/chai'; +import sinon, { stub } from 'sinon'; +import { readFile } from '@web/test-runner-commands'; +import { + allElementsVisible, + visibleSelectorsDesktop, + visibleSelectorsMobile, + createFullGlobalFooter, + insertDummyElementOnTop, + waitForFooterToDecorate, + allSelectors, +} from './test-utilities.js'; +import baseFooter from './mocks/base-footer.js'; +import fetchedFooter from './mocks/fetched-footer.js'; +import icons from './mocks/icons.js'; +import { isElementVisible, mockRes } from '../global-navigation/test-utilities.js'; +import placeholders from '../global-navigation/mocks/placeholders.js'; + +describe('global footer', () => { + let clock = null; + beforeEach(async () => { + document.body.innerHTML = baseFooter; + clock = sinon.useFakeTimers({ + toFake: ['setTimeout'], + shouldAdvanceTime: true, + }); + + stub(window, 'fetch').callsFake(async (url) => { + if (url.includes('/footer')) { + return mockRes({ + payload: fetchedFooter( + { regionPickerHash: '/fragments/regions#langnav' }, + ), + }); + } + if (url.includes('/placeholders')) return mockRes({ payload: placeholders }); + if (url.includes('icons.svg')) return mockRes({ payload: icons }); + if (url.includes('/regions.plain.html')) return mockRes({ payload: await readFile({ path: '../region-nav/mocks/regions.html' }) }); + return null; + }); + }); + + afterEach(() => { + clock.restore(); + window.fetch.restore(); + document.body.innerHTML = ''; + }); + + describe('wide screen', async () => { + it('should render the footer on wide screen', async () => { + await createFullGlobalFooter({ waitForDecoration: true, viewport: 'wide' }); + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + + expect(allElementsVisible( + visibleSelectorsDesktop, + document.querySelector(allSelectors.container), + )).to.equal(true); + }); + }); + + describe('desktop', () => { + describe('basic sanity tests', () => { + it('should have footer', async () => { + await createFullGlobalFooter({ waitForDecoration: true }); + expect(document.querySelector('footer')).to.exist; + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + + expect(allElementsVisible( + visibleSelectorsDesktop, + document.querySelector(allSelectors.container), + )).to.equal(true); + }); + + it('should handle failed fetch for footer content', async () => { + window.fetch.restore(); + stub(window, 'fetch').callsFake((url) => { + if (url.includes('/footer')) { + return mockRes({ + payload: null, + ok: false, + status: 400, + }); + } + if (url.includes('/placeholders')) return mockRes({ payload: placeholders }); + if (url.includes('icons.svg')) return mockRes({ payload: icons }); + return null; + }); + + const globalFooter = await createFullGlobalFooter({ waitForDecoration: false }); + expect(await globalFooter.decorateContent()).to.equal(undefined); + }); + + it('should handle missing elements', async () => { + const globalFooter = await createFullGlobalFooter({ waitForDecoration: true }); + globalFooter.body = document.createElement('div'); + expect(await globalFooter.decorateGrid()).to.equal(''); + expect(await globalFooter.decorateProducts()).to.equal(''); + expect(await globalFooter.decorateRegionPicker()).to.equal(''); + expect(await globalFooter.decorateSocial()).to.equal(''); + expect(await globalFooter.decoratePrivacy()).to.equal(''); + }); + }); + + describe('conditional render tests', () => { + const { container, ...childSelectors } = allSelectors; + it('should render the footer when in viewport', async () => { + await createFullGlobalFooter({ waitForDecoration: true }); + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + + expect(allElementsVisible( + visibleSelectorsDesktop, + document.querySelector(allSelectors.container), + )).to.equal(true); + }); + + it('should render the footer after 3s when outside of the 300px range of the viewport', async () => { + insertDummyElementOnTop({ height: window.innerHeight + 400 }); + + await createFullGlobalFooter({ waitForDecoration: false }); + + Object.keys(childSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(false)); + + clock.tick(3000); + await waitForFooterToDecorate(); + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + }); + + it('should render the footer when outside of the viewport, but within 300px range', async () => { + insertDummyElementOnTop({ height: window.innerHeight + 200 }); + const startTime = performance.now(); + await createFullGlobalFooter({ waitForDecoration: true }); + const endTime = performance.now(); + const timeDiff = endTime - startTime; + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + // footer decoration should take less than 3s if within 300px range of viewport + expect(timeDiff < 3000).to.equal(true); + }); + + it('should render the footer when outside of the 300px viewport range, but scrolled into view earlier than 3s', async () => { + insertDummyElementOnTop({ height: window.innerHeight + 400 }); + const startTime = performance.now(); + await createFullGlobalFooter({ waitForDecoration: false }); + + Object.keys(childSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(false)); + + window.scrollBy(0, window.innerHeight); + await waitForFooterToDecorate(); + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + + const endTime = performance.now(); + const timeDiff = endTime - startTime; + // footer decoration should take less than 3s when scrolled into view + expect(timeDiff < 3000).to.equal(true); + + expect(allElementsVisible( + visibleSelectorsDesktop, + document.querySelector(allSelectors.container), + )).to.equal(true); + }); + }); + + describe('region picker tests', () => { + it('should handle non-empty hash', async () => { + await createFullGlobalFooter({ waitForDecoration: true }); + + const regionPickerElem = document.querySelector(allSelectors.regionPicker); + regionPickerElem.dispatchEvent(new Event('click')); + + expect(regionPickerElem.getAttribute('href') === '#langnav').to.equal(true); + expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('true'); + + window.dispatchEvent(new Event('milo:modal:closed')); + expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('false'); + }); + + it('should handle empty hash', async () => { + window.fetch.restore(); + stub(window, 'fetch').callsFake(async (url) => { + if (url.includes('/footer')) { + return mockRes({ + payload: fetchedFooter( + { regionPickerHash: '' }, + ), + }); + } + if (url.includes('/placeholders')) return mockRes({ payload: placeholders }); + if (url.includes('icons.svg')) return mockRes({ payload: icons }); + return null; + }); + + await createFullGlobalFooter({ waitForDecoration: true }); + const regionPickerElem = document.querySelector(allSelectors.regionPicker); + expect(regionPickerElem.getAttribute('href') === '#').to.equal(true); + + regionPickerElem.dispatchEvent(new Event('click')); + expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('true'); + + document.body.dispatchEvent(new Event('click', { bubbles: true })); + expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('false'); + }); + }); + }); + + describe('small desktop', async () => { + it('should render the footer on small desktop', async () => { + await createFullGlobalFooter({ waitForDecoration: true, viewport: 'smallDesktop' }); + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + + expect(allElementsVisible( + visibleSelectorsDesktop, + document.querySelector(allSelectors.container), + )).to.equal(true); + }); + }); + + describe('mobile', () => { + it('should render the footer on mobile', async () => { + await createFullGlobalFooter({ waitForDecoration: true, viewport: 'mobile' }); + + Object.keys(allSelectors).forEach((key) => expect( + document.querySelector(allSelectors[key]) instanceof HTMLElement, + ).to.equal(true)); + + expect(allElementsVisible( + visibleSelectorsMobile, + document.querySelector(allSelectors.container), + )).to.equal(true); + }); + + it('should open/close dropdowns on click', async () => { + await createFullGlobalFooter({ waitForDecoration: true, viewport: 'mobile' }); + + for (const dropdown of Array.from(document.getElementsByClassName('feds-menu-section'))) { + const header = dropdown.querySelector('.feds-menu-headline'); + const links = Array.from(dropdown.getElementsByClassName('feds-navLink')); + + // open the dropdown + header.dispatchEvent(new Event('click')); + for (const link of links) { + expect(isElementVisible(link)).to.equal(true); + } + // close the dropdown + header.dispatchEvent(new Event('click')); + for (const link of links) { + expect(isElementVisible(link)).to.equal(false); + } + } + }); + }); +}); diff --git a/test/blocks/global-footer/mocks/base-footer.js b/test/blocks/global-footer/mocks/base-footer.js new file mode 100644 index 0000000000..b0e5fb370b --- /dev/null +++ b/test/blocks/global-footer/mocks/base-footer.js @@ -0,0 +1 @@ +export default '
'; diff --git a/test/blocks/global-footer/mocks/fetched-footer.js b/test/blocks/global-footer/mocks/fetched-footer.js new file mode 100644 index 0000000000..b8f113db44 --- /dev/null +++ b/test/blocks/global-footer/mocks/fetched-footer.js @@ -0,0 +1,144 @@ +export default ({ regionPickerHash = '/fragments/regions#langnav' }) => `
+

Contact Us

+ +

Why Adobe

+ +
+
+

About Experience Cloud

+ +
+
+

Our solutions

+ +
+
+

Resources

+ +
+
+

Company

+ +
+
+ + + + +
+
+ +
+
+ +
+
+

All rights reserved. / Privacy / Terms of Use / Cookie preferences / Do not sell my personal information / AdChoices

+
`; diff --git a/test/blocks/global-footer/mocks/icons.js b/test/blocks/global-footer/mocks/icons.js new file mode 100644 index 0000000000..58a1051ade --- /dev/null +++ b/test/blocks/global-footer/mocks/icons.js @@ -0,0 +1,54 @@ +export default ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/test/blocks/global-footer/mocks/media-icon.png b/test/blocks/global-footer/mocks/media-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ae39a1539343b04c34cc6bb043cf85b4760ea919 GIT binary patch literal 786 zcmV+t1MU1$Nk&Er0{{S5MM6+kP&il$0000G0001E003bC06|PpNYelS00D4Y+qM#2 zDzFAd>ma27Iv|3Iv=xmAA#$mOalJ2`H4!lZc=+fANxKYRKM$$z9r0o(U8$zdE3A5@ zTU$>BE9KxZqg+Xs#sg}z(r(my)k?g&6R*_c8|F&h1fX>6It8#-`ZiGTE3jDw+_)0B zkpxjLLEcNyPZCUn1jFBdKk6n4re1`Yw8 z++y6Wm{qu|HoO}39*mMzY2e|2Q-Ei!non)vg-*&Pb@1Y&=cmIweErfUfp^5i09H^q zAP@oo0PqX|odGIj0Ac_>kw~6QC8VOFAy^6Euo4Mr01kXsA1PiAX*FiO;LI}6wp6d1 zYj33fAVHHz_<4{#vU2Jup=c~4Z0XPN#0q+&W}QCXFXd_`Kz$NJ{DQLSUo+!b6dNRx z8k#-bas3X0%x+_q=(Yg8V@cwju+%r~WHBjI7q95O-!%`p{$xhXwYLBW2 zgiRV=_JkU@+o**b%r8uLlrvo3sWS433^H!qnE7IL`X-b_t*1%t(j^c=-bk=luybYx z3W?Ct1t%`#XwU*eP!|r2_72ouuCSB@Uhmx#!{Guwo-7UsBbc61faq%Q@>nC^6@mzoho2b*gkOnPT0Fop Qu-<_5Z{|!5TPOek05>*cD*ylh literal 0 HcmV?d00001 diff --git a/test/blocks/global-footer/test-utilities.js b/test/blocks/global-footer/test-utilities.js new file mode 100644 index 0000000000..a0f4a6db72 --- /dev/null +++ b/test/blocks/global-footer/test-utilities.js @@ -0,0 +1,98 @@ +import { setViewport } from '@web/test-runner-commands'; +import { setConfig } from '../../../libs/utils/utils.js'; +import { config, isElementVisible, loadStyles, viewports } from '../global-navigation/test-utilities.js'; +import { waitForElement } from '../../helpers/waitfor.js'; +import { selectors as keyboardSelectors } from '../../../libs/blocks/global-navigation/utilities/keyboard/utils.js'; +import { selectors as gnavSelectors } from '../../../libs/blocks/global-navigation/utilities/utilities.js'; + +export const allSelectors = { + container: keyboardSelectors.globalFooter, + footerIcons: '.feds-footer-icons', + footerWrapper: '.feds-footer-wrapper', + menuContent: keyboardSelectors.menuContent, + menuColumn: gnavSelectors.menuColumn, + menuSection: gnavSelectors.menuSection, + menuHeadline: '.feds-menu-headline', + menuItems: '.feds-menu-items', + featuredProducts: '.feds-featuredProducts', + featuredProductsLabel: '.feds-featuredProducts-label', + navLink: gnavSelectors.navLink, + navLinkImage: '.feds-navLink-image', + navLinkContent: '.feds-navLink-content', + navLinkTitle: '.feds-navLink-title', + social: '.feds-social', + socialItem: '.feds-social-item', + regionPickerWrapper: '.feds-regionPicker-wrapper', + regionPicker: keyboardSelectors.regionPicker, + legalWrapper: '.feds-footer-legalWrapper', + privacySection: '.feds-footer-privacySection', + copyright: '.feds-footer-copyright', + privacyLink: keyboardSelectors.privacyLink, +}; + +const getMobileVisibleSelectors = () => { + const { + container, + footerIcons, + featuredProducts, + featuredProductsLabel, + navLink, + navLinkImage, + navLinkContent, + navLinkTitle, + menuItems, + menuHeadline, + ...mobileSelectors + } = allSelectors; + return mobileSelectors; +}; + +const getDesktopVisibleSelectors = () => { + const { container, footerIcons, ...desktopSelectors } = allSelectors; + return desktopSelectors; +}; + +export const visibleSelectorsMobile = getMobileVisibleSelectors(); + +// for small desktop and above +export const visibleSelectorsDesktop = getDesktopVisibleSelectors(); + +export const allElementsVisible = (givenSelectors, parentEl) => { + if (typeof givenSelectors !== 'object' || givenSelectors === null || !(parentEl instanceof Element)) { + console.warn('Invalid arguments passed to allElementsVisible'); + return false; + } + return Object + .keys(givenSelectors) + .map((key) => isElementVisible(parentEl.querySelector(givenSelectors[key]))) + .every((el) => el); +}; + +export const waitForFooterToDecorate = () => Promise.all( + Object + .keys(allSelectors) + .map((key) => waitForElement(allSelectors[key])), +); + +export const createFullGlobalFooter = async ({ waitForDecoration, viewport = 'desktop' }) => { + await setViewport(viewports[viewport]); + setConfig(config); + // we need to import the footer class in here so it can use the config we have set above + // if we import it at the top of the file, an empty config will be defined and used by the footer + + const [, , footerModule] = await Promise.all([ + loadStyles('../../../../libs/styles/styles.css'), + loadStyles('../../../../libs/blocks/global-footer/global-footer.css'), + import('../../../libs/blocks/global-footer/global-footer.js'), + ]); + + const instance = footerModule.default(document.querySelector('footer')); + if (waitForDecoration) await waitForFooterToDecorate(); + return instance; +}; + +export const insertDummyElementOnTop = ({ height }) => { + const dummyElement = document.createElement('div'); + dummyElement.style.height = `${height}px`; + document.body.prepend(dummyElement); +}; diff --git a/test/blocks/global-navigation/global-navigation.test.js b/test/blocks/global-navigation/global-navigation.test.js index 70c83efdb2..6351d9c8ef 100644 --- a/test/blocks/global-navigation/global-navigation.test.js +++ b/test/blocks/global-navigation/global-navigation.test.js @@ -19,7 +19,7 @@ const ogFetch = window.fetch; describe('global navigation', () => { before(() => { - document.head.innerHTML = ''; + document.head.innerHTML = ''; }); describe('basic sanity tests', () => { diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs index 8f8bf0acea..5f952931fd 100644 --- a/web-test-runner.config.mjs +++ b/web-test-runner.config.mjs @@ -44,6 +44,7 @@ export default { testRunnerHtml: (testFramework) => ` +