From a234e2b61d3b69271f933678d41b8011b3d79c70 Mon Sep 17 00:00:00 2001 From: Mira Fedas <30750556+mirafedas@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:16:06 +0200 Subject: [PATCH 01/22] MWPW-156788: Unable to scroll page with active Sort filter (#2758) * moved merch-card class toggling to other methods * added unit test for merch card collection on phones & tablets * added unit test for closing the filters modal * corrected lit import for merch sidenav --- libs/deps/mas/merch-sidenav.js | 10 +++--- libs/features/mas/web-components/build.mjs | 16 ++++++++- .../src/sidenav/merch-sidenav.js | 9 ++--- .../test/merch-card-collection.test.html.js | 34 ++++++++++++++++--- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/libs/deps/mas/merch-sidenav.js b/libs/deps/mas/merch-sidenav.js index ec6cb670c6..09057e8ecc 100644 --- a/libs/deps/mas/merch-sidenav.js +++ b/libs/deps/mas/merch-sidenav.js @@ -1,4 +1,4 @@ -import{html as k,css as H,LitElement as P}from"../lit-all.min.js";var r=class{constructor(e,t){this.key=Symbol("match-media-key"),this.matches=!1,this.host=e,this.host.addController(this),this.media=window.matchMedia(t),this.matches=this.media.matches,this.onChange=this.onChange.bind(this),e.addController(this)}hostConnected(){var e;(e=this.media)==null||e.addEventListener("change",this.onChange)}hostDisconnected(){var e;(e=this.media)==null||e.removeEventListener("change",this.onChange)}onChange(e){this.matches!==e.matches&&(this.matches=e.matches,this.host.requestUpdate(this.key,!this.matches))}};import{css as L}from"../lit-all.min.js";var c=L` +import{html as k,css as H,LitElement as P}from"/libs/deps/lit-all.min.js";var r=class{constructor(e,t){this.key=Symbol("match-media-key"),this.matches=!1,this.host=e,this.host.addController(this),this.media=window.matchMedia(t),this.matches=this.media.matches,this.onChange=this.onChange.bind(this),e.addController(this)}hostConnected(){var e;(e=this.media)==null||e.addEventListener("change",this.onChange)}hostDisconnected(){var e;(e=this.media)==null||e.removeEventListener("change",this.onChange)}onChange(e){this.matches!==e.matches&&(this.matches=e.matches,this.host.requestUpdate(this.key,!this.matches))}};import{css as L}from"/libs/deps/lit-all.min.js";var c=L` h2 { font-size: 11px; font-style: normal; @@ -9,7 +9,7 @@ import{html as k,css as H,LitElement as P}from"../lit-all.min.js";var r=class{co line-height: 32px; color: #747474; } -`;import{html as N,LitElement as R}from"../lit-all.min.js";function d(s,e){let t;return function(){let o=this,i=arguments;clearTimeout(t),t=setTimeout(()=>s.apply(o,i),e)}}var x="merch-search:change";var v="merch-sidenav:select";var g="hashchange";function n(s=window.location.hash){let e=[],t=s.replace(/^#/,"").split("&");for(let o of t){let[i,l=""]=o.split("=");i&&e.push([i,decodeURIComponent(l.replace(/\+/g," "))])}return Object.fromEntries(e)}function a(s,e){if(s.deeplink){let t={};t[s.deeplink]=e,A(t)}}function A(s){let e=new URLSearchParams(window.location.hash.slice(1));Object.entries(s).forEach(([i,l])=>{l?e.set(i,l):e.delete(i)}),e.sort();let t=e.toString();if(t===window.location.hash)return;let o=window.scrollY||document.documentElement.scrollTop;window.location.hash=t,window.scrollTo(0,o)}function b(s){let e=()=>{if(window.location.hash&&!window.location.hash.includes("="))return;let t=n(window.location.hash);s(t)};return e(),window.addEventListener(g,e),()=>{window.removeEventListener(g,e)}}var p=class extends R{static properties={deeplink:{type:String}};get search(){return this.querySelector("sp-search")}constructor(){super(),this.handleInput=()=>{a(this,this.search.value),this.search.value&&this.dispatchEvent(new CustomEvent(x,{bubbles:!0,composed:!0,detail:{type:"search",value:this.search.value}}))},this.handleInputDebounced=d(this.handleInput.bind(this))}connectedCallback(){super.connectedCallback(),this.search&&(this.search.addEventListener("input",this.handleInputDebounced),this.search.addEventListener("submit",this.handleInputSubmit),this.updateComplete.then(()=>{this.setStateFromURL()}),this.startDeeplink())}disconnectedCallback(){super.disconnectedCallback(),this.search.removeEventListener("input",this.handleInputDebounced),this.search.removeEventListener("submit",this.handleInputSubmit),this.stopDeeplink?.()}setStateFromURL(){let t=n()[this.deeplink];t&&(this.search.value=t)}startDeeplink(){this.stopDeeplink=b(({search:e})=>{this.search.value=e??""})}handleInputSubmit(e){e.preventDefault()}render(){return N``}};customElements.define("merch-search",p);import{html as C,LitElement as D,css as M}from"../lit-all.min.js";var m=class extends D{static properties={sidenavListTitle:{type:String},label:{type:String},deeplink:{type:String,attribute:"deeplink"},selectedText:{type:String,reflect:!0,attribute:"selected-text"},selectedValue:{type:String,reflect:!0,attribute:"selected-value"}};static styles=[M` +`;import{html as N,LitElement as R}from"/libs/deps/lit-all.min.js";function d(s,e){let t;return function(){let o=this,i=arguments;clearTimeout(t),t=setTimeout(()=>s.apply(o,i),e)}}var x="merch-search:change";var v="merch-sidenav:select";var g="hashchange";function n(s=window.location.hash){let e=[],t=s.replace(/^#/,"").split("&");for(let o of t){let[i,l=""]=o.split("=");i&&e.push([i,decodeURIComponent(l.replace(/\+/g," "))])}return Object.fromEntries(e)}function a(s,e){if(s.deeplink){let t={};t[s.deeplink]=e,A(t)}}function A(s){let e=new URLSearchParams(window.location.hash.slice(1));Object.entries(s).forEach(([i,l])=>{l?e.set(i,l):e.delete(i)}),e.sort();let t=e.toString();if(t===window.location.hash)return;let o=window.scrollY||document.documentElement.scrollTop;window.location.hash=t,window.scrollTo(0,o)}function b(s){let e=()=>{if(window.location.hash&&!window.location.hash.includes("="))return;let t=n(window.location.hash);s(t)};return e(),window.addEventListener(g,e),()=>{window.removeEventListener(g,e)}}var p=class extends R{static properties={deeplink:{type:String}};get search(){return this.querySelector("sp-search")}constructor(){super(),this.handleInput=()=>{a(this,this.search.value),this.search.value&&this.dispatchEvent(new CustomEvent(x,{bubbles:!0,composed:!0,detail:{type:"search",value:this.search.value}}))},this.handleInputDebounced=d(this.handleInput.bind(this))}connectedCallback(){super.connectedCallback(),this.search&&(this.search.addEventListener("input",this.handleInputDebounced),this.search.addEventListener("submit",this.handleInputSubmit),this.updateComplete.then(()=>{this.setStateFromURL()}),this.startDeeplink())}disconnectedCallback(){super.disconnectedCallback(),this.search.removeEventListener("input",this.handleInputDebounced),this.search.removeEventListener("submit",this.handleInputSubmit),this.stopDeeplink?.()}setStateFromURL(){let t=n()[this.deeplink];t&&(this.search.value=t)}startDeeplink(){this.stopDeeplink=b(({search:e})=>{this.search.value=e??""})}handleInputSubmit(e){e.preventDefault()}render(){return N``}};customElements.define("merch-search",p);import{html as C,LitElement as D,css as M}from"/libs/deps/lit-all.min.js";var m=class extends D{static properties={sidenavListTitle:{type:String},label:{type:String},deeplink:{type:String,attribute:"deeplink"},selectedText:{type:String,reflect:!0,attribute:"selected-text"},selectedValue:{type:String,reflect:!0,attribute:"selected-value"}};static styles=[M` :host { display: block; contain: content; @@ -36,7 +36,7 @@ import{html as k,css as H,LitElement as P}from"../lit-all.min.js";var r=class{co > ${this.sidenavListTitle?C`

${this.sidenavListTitle}

`:""} - `}};customElements.define("merch-sidenav-list",m);import{html as V,LitElement as O,css as I}from"../lit-all.min.js";var u=class extends O{static properties={sidenavCheckboxTitle:{type:String},label:{type:String},deeplink:{type:String},selectedValues:{type:Array,reflect:!0},value:{type:String}};static styles=I` + `}};customElements.define("merch-sidenav-list",m);import{html as V,LitElement as O,css as I}from"/libs/deps/lit-all.min.js";var u=class extends O{static properties={sidenavCheckboxTitle:{type:String},label:{type:String},deeplink:{type:String},selectedValues:{type:Array,reflect:!0},value:{type:String}};static styles=I` :host { display: block; contain: content; @@ -66,7 +66,7 @@ import{html as k,css as H,LitElement as P}from"../lit-all.min.js";var r=class{co > - `}};customElements.define("merch-sidenav-checkbox-group",u);var y="(max-width: 700px)";var S="(max-width: 1199px)";var T=/iP(ad|hone|od)/.test(window?.navigator?.platform)||window?.navigator?.platform==="MacIntel"&&window.navigator.maxTouchPoints>1,E=!1,h,w=s=>{s&&(T?(document.body.style.position="fixed",s.ontouchmove=e=>{e.targetTouches.length===1&&e.stopPropagation()},E||(document.addEventListener("touchmove",e=>e.preventDefault()),E=!0)):(h=document.body.style.overflow,document.body.style.overflow="hidden"))},_=s=>{s&&(T?(s.ontouchstart=null,s.ontouchmove=null,document.body.style.position="",document.removeEventListener("touchmove",e=>e.preventDefault()),E=!1):h!==void 0&&(document.body.style.overflow=h,h=void 0))};document.addEventListener("sp-opened",()=>{document.body.classList.add("merch-modal")});document.addEventListener("sp-closed",()=>{document.body.classList.remove("merch-modal")});var f=class extends P{static properties={sidenavTitle:{type:String},closeText:{type:String,attribute:"close-text"},modal:{type:Boolean,attribute:"modal",reflect:!0}};#e;constructor(){super(),this.modal=!1}static styles=[H` + `}};customElements.define("merch-sidenav-checkbox-group",u);var y="(max-width: 700px)";var S="(max-width: 1199px)";var T=/iP(ad|hone|od)/.test(window?.navigator?.platform)||window?.navigator?.platform==="MacIntel"&&window.navigator.maxTouchPoints>1,E=!1,h,w=s=>{s&&(T?(document.body.style.position="fixed",s.ontouchmove=e=>{e.targetTouches.length===1&&e.stopPropagation()},E||(document.addEventListener("touchmove",e=>e.preventDefault()),E=!0)):(h=document.body.style.overflow,document.body.style.overflow="hidden"))},_=s=>{s&&(T?(s.ontouchstart=null,s.ontouchmove=null,document.body.style.position="",document.removeEventListener("touchmove",e=>e.preventDefault()),E=!1):h!==void 0&&(document.body.style.overflow=h,h=void 0))};var f=class extends P{static properties={sidenavTitle:{type:String},closeText:{type:String,attribute:"close-text"},modal:{type:Boolean,attribute:"modal",reflect:!0}};#e;constructor(){super(),this.modal=!1}static styles=[H` :host { display: block; } @@ -138,4 +138,4 @@ import{html as k,css as H,LitElement as P}from"../lit-all.min.js";var r=class{co `}get asAside(){return k`

${this.sidenavTitle}

`}get dialog(){return this.shadowRoot.querySelector("sp-dialog-base")}closeModal(e){e.preventDefault(),this.dialog?.close()}openModal(){this.updateComplete.then(async()=>{w(this.dialog);let e={trigger:this.#e,notImmediatelyClosable:!0,type:"auto"},t=await window.__merch__spectrum_Overlay.open(this.dialog,e);t.addEventListener("close",()=>{this.modal=!1,_(this.dialog)}),this.shadowRoot.querySelector("sp-theme").append(t)})}updated(){this.modal&&this.openModal()}showModal({target:e}){this.#e=e,this.modal=!0}};customElements.define("merch-sidenav",f);export{f as MerchSideNav}; + >`}get dialog(){return this.shadowRoot.querySelector("sp-dialog-base")}closeModal(e){e.preventDefault(),this.dialog?.close(),document.body.classList.remove("merch-modal")}openModal(){this.updateComplete.then(async()=>{w(this.dialog),document.body.classList.add("merch-modal");let e={trigger:this.#e,notImmediatelyClosable:!0,type:"auto"},t=await window.__merch__spectrum_Overlay.open(this.dialog,e);t.addEventListener("close",()=>{this.modal=!1,_(this.dialog)}),this.shadowRoot.querySelector("sp-theme").append(t)})}updated(){this.modal&&this.openModal()}showModal({target:e}){this.#e=e,this.modal=!0}};customElements.define("merch-sidenav",f);export{f as MerchSideNav}; diff --git a/libs/features/mas/web-components/build.mjs b/libs/features/mas/web-components/build.mjs index 3b21388f48..5283a4bc9a 100644 --- a/libs/features/mas/web-components/build.mjs +++ b/libs/features/mas/web-components/build.mjs @@ -57,7 +57,7 @@ Promise.all([ minify: true, outfile: `${outfolder}/merch-sidenav.js`, format: 'esm', - plugins: [rewriteImports()], + plugins: [rewriteImportsToLibsFolder()], external: ['lit'], }), buildLitComponent('merch-icon'), @@ -84,3 +84,17 @@ function rewriteImports(rew) { }, }; } + +function rewriteImportsToLibsFolder(rew) { + return { + name: 'rewrite-imports-to-libs-folder', + setup(build) { + build.onResolve({ filter: /^lit(\/.*)?$/ }, (args) => { + return { + path: '/libs/deps/lit-all.min.js', + external: true, + }; + }); + }, + }; +} diff --git a/libs/features/mas/web-components/src/sidenav/merch-sidenav.js b/libs/features/mas/web-components/src/sidenav/merch-sidenav.js index 9fefd5033c..737c741e37 100644 --- a/libs/features/mas/web-components/src/sidenav/merch-sidenav.js +++ b/libs/features/mas/web-components/src/sidenav/merch-sidenav.js @@ -7,13 +7,6 @@ import './merch-sidenav-checkbox-group.js'; import { SPECTRUM_MOBILE_LANDSCAPE, TABLET_DOWN } from '../media.js'; import { disableBodyScroll, enableBodyScroll } from '../bodyScrollLock.js'; -document.addEventListener('sp-opened', () => { - document.body.classList.add('merch-modal'); -}); -document.addEventListener('sp-closed', () => { - document.body.classList.remove('merch-modal'); -}); - export class MerchSideNav extends LitElement { static properties = { sidenavTitle: { type: String }, @@ -137,11 +130,13 @@ export class MerchSideNav extends LitElement { closeModal(e) { e.preventDefault(); this.dialog?.close(); + document.body.classList.remove('merch-modal'); } openModal() { this.updateComplete.then(async () => { disableBodyScroll(this.dialog); + document.body.classList.add('merch-modal'); const options = { trigger: this.#target, notImmediatelyClosable: true, diff --git a/libs/features/mas/web-components/test/merch-card-collection.test.html.js b/libs/features/mas/web-components/test/merch-card-collection.test.html.js index ee0dfcbfc5..5b7cd5a477 100644 --- a/libs/features/mas/web-components/test/merch-card-collection.test.html.js +++ b/libs/features/mas/web-components/test/merch-card-collection.test.html.js @@ -72,14 +72,40 @@ let merchCards; const shouldSkipTests = sessionStorage.getItem('skipTests') ? 'true' : 'false'; runTests(async () => { - await toggleLargeDesktop(); + let render; mockLana(); await mockFetch(withWcs); await mas(); if (shouldSkipTests !== 'true') { - describe('merch-card-collection web component', () => { - let render; - beforeEach(() => { + describe("merch-card-collection web component on phones and tablets", () => { + beforeEach(async () => { + [merchCards, render] = prepareTemplate('catalogCards', false); + }); + + it('sets the class for modal when opening filters in a modal', async () => { + render(); + await delay(100); + expect(document.body.classList.contains('merch-modal')).to.be.false; + merchCards.shadowRoot.querySelector('#filtersButton').click(); + await delay(100); + expect(document.body.classList.contains('merch-modal')).to.be.true; + }); + + it('removes the class for modal when closing the filters modal', async () => { + render(); + await delay(100); + merchCards.shadowRoot.querySelector('#filtersButton').click(); + await delay(100); + expect(document.body.classList.contains('merch-modal')).to.be.true; + document.querySelector('merch-sidenav').shadowRoot.querySelector('#sidenav').querySelector('sp-link').click(); + await delay(100); + expect(document.body.classList.contains('merch-modal')).to.be.false; + }); + }); + + describe('merch-card-collection web component on desktop', () => { + beforeEach(async () => { + await toggleLargeDesktop(); document.location.hash = ''; [merchCards, render] = prepareTemplate('catalogCards', false); }); From 7921bf87c42491fbce899c8d850e323a342eea65 Mon Sep 17 00:00:00 2001 From: Bozo Jovicic <37440641+bozojovicic@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:16:14 +0200 Subject: [PATCH 02/22] MWPW-157210 STE Promo card not appearing (#2837) * MWPW-157210 STE Promo card not appearing * MWPW-157210 STE Promo card not appearing * MWPW-157210 STE Promo card not appearing * Trigger Build --------- Co-authored-by: Bozo Jovicic --- .../merch-card-collection.js | 3 ++- libs/utils/helpers.js | 18 ++++++++++++++++++ test/utils/helpers.test.js | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 test/utils/helpers.test.js diff --git a/libs/blocks/merch-card-collection/merch-card-collection.js b/libs/blocks/merch-card-collection/merch-card-collection.js index 695f9da51e..dad54cb600 100644 --- a/libs/blocks/merch-card-collection/merch-card-collection.js +++ b/libs/blocks/merch-card-collection/merch-card-collection.js @@ -1,3 +1,4 @@ +import { overrideUrlOrigin } from '../../utils/helpers.js'; import { createTag, decorateLinks, getConfig, loadBlock, loadStyle, localizeLink, } from '../../utils/utils.js'; @@ -57,7 +58,7 @@ async function getCardsRoot(config, html) { } const fetchOverrideCard = (action, config) => new Promise((resolve, reject) => { - fetch(`${localizeLink(action?.target, config)}.plain.html`).then((res) => { + fetch(`${localizeLink(overrideUrlOrigin(action?.target))}.plain.html`).then((res) => { if (res.ok) { res.text().then((cardContent) => { const response = { path: action.target, cardContent: /^
(.*)<\/div>$/.exec(cardContent.replaceAll('\n', ''))[1] }; diff --git a/libs/utils/helpers.js b/libs/utils/helpers.js index 73274a4988..f461139f3e 100644 --- a/libs/utils/helpers.js +++ b/libs/utils/helpers.js @@ -19,3 +19,21 @@ export function updateLinkWithLangRoot(link) { return link; } } + +/** + * Replaces the origin of the provided link with location.origin. + * + * @param link + * @returns {string|*} + */ +export function overrideUrlOrigin(link) { + try { + const url = new URL(link); + if (url.hostname !== window.location.hostname) { + return link.replace(url.origin, window.location.origin); + } + } catch (e) { + // ignore + } + return link; +} diff --git a/test/utils/helpers.test.js b/test/utils/helpers.test.js new file mode 100644 index 0000000000..d49a0ec4b3 --- /dev/null +++ b/test/utils/helpers.test.js @@ -0,0 +1,14 @@ +import { expect } from '@esm-bundle/chai'; +import { overrideUrlOrigin } from '../../libs/utils/helpers.js'; + +describe('overrideUrlOrigin', () => { + it('Change origin to http://localhost:2000', async () => { + const link = overrideUrlOrigin('http://www.qa.adobe.com/some/page.html?a=b#hash'); + expect(link).to.equal('http://localhost:2000/some/page.html?a=b#hash'); + }); + + it('Ignore relative URLs', async () => { + const link = overrideUrlOrigin('/some/page.html?a=b#hash'); + expect(link).to.equal('/some/page.html?a=b#hash'); + }); +}); From e03455463a5e69f612422863d9174f5116a30f55 Mon Sep 17 00:00:00 2001 From: Rares Munteanu Date: Wed, 11 Sep 2024 11:16:21 +0200 Subject: [PATCH 03/22] [MWPW-157740] Table pricing gutter (#2851) --- libs/blocks/table/table.css | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/blocks/table/table.css b/libs/blocks/table/table.css index 391c70fb1a..fd95c0714b 100644 --- a/libs/blocks/table/table.css +++ b/libs/blocks/table/table.css @@ -264,6 +264,7 @@ .table .section-row .col { padding: 16px 24px; + column-gap: 0.5ch; } .table .section-row .col:has(> p:nth-child(2)) { From 4a4a12e70929d9464e409796abccbc42d7836c85 Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Wed, 11 Sep 2024 03:16:28 -0600 Subject: [PATCH 04/22] MWPW-158148 [MEP] Can't use ul, ol or li in MEP selectorsf (#2864) * add ol, ul and li * add unit test --- libs/features/personalization/personalization.js | 1 + .../personalization/modifyNonFragmentSelector.test.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 71857e6c3b..091e6ad027 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -366,6 +366,7 @@ function modifySelectorTerm(termParam) { const htmlEls = [ 'html', 'body', 'header', 'footer', 'main', 'div', 'a', 'p', 'strong', 'em', 'picture', 'source', 'img', 'h', + 'ul', 'ol', 'li', ]; const startTextMatch = term.match(/^[a-zA-Z/./-]*/); const startText = startTextMatch ? startTextMatch[0].toLowerCase() : ''; diff --git a/test/features/personalization/modifyNonFragmentSelector.test.js b/test/features/personalization/modifyNonFragmentSelector.test.js index f9345c3eb1..fd3cdbf893 100644 --- a/test/features/personalization/modifyNonFragmentSelector.test.js +++ b/test/features/personalization/modifyNonFragmentSelector.test.js @@ -135,6 +135,14 @@ const values = [ b: 'custom-block3', a: '.custom-block:nth-child(3 of .custom-block)', }, + { + b: 'any-marquee ol li:nth-child(2)', + a: '[class*="marquee"] ol li:nth-child(2)', + }, + { + b: 'any-marquee ul li:nth-child(2)', + a: '[class*="marquee"] ul li:nth-child(2)', + }, ]; describe('test different values', () => { values.forEach((value) => { From 9c32237bdedf6904fffe8abe83edd4ba4d7d2cf2 Mon Sep 17 00:00:00 2001 From: Robert Bogos <146744221+robert-bogos@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:18:36 +0300 Subject: [PATCH 05/22] [MWPW-157347] Environment relative URLs (#2834) * environment aware urls * added domains map entry for testing * fixed unit tests * hotfix * moved the convert urls logic to a separate function * added early returns * hotfix * relative urls unit tests * removed fork domain --------- Co-authored-by: milo-pr-merge[bot] <169241390+milo-pr-merge[bot]@users.noreply.github.com> --- libs/scripts/scripts.js | 18 +++++--- libs/utils/utils.js | 21 +++++++-- test/utils/utils.test.js | 97 +++++++++++++++++++++++++++++++++++----- 3 files changed, 116 insertions(+), 20 deletions(-) diff --git a/libs/scripts/scripts.js b/libs/scripts/scripts.js index 4388eea325..7c97265400 100644 --- a/libs/scripts/scripts.js +++ b/libs/scripts/scripts.js @@ -22,11 +22,19 @@ import locales from '../utils/locales.js'; const prodDomains = ['milo.adobe.com']; const stageDomainsMap = { - 'www.adobe.com': 'www.stage.adobe.com', - 'blog.adobe.com': 'blog.stage.adobe.com', - 'business.adobe.com': 'business.stage.adobe.com', - 'helpx.adobe.com': 'helpx.stage.adobe.com', - 'news.adobe.com': 'news.stage.adobe.com', + 'www.stage.adobe.com': { + 'www.adobe.com': 'origin', + 'helpx.adobe.com': 'helpx.stage.adobe.com', + }, + '--bacom--adobecom.hlx.live': { + 'business.adobe.com': 'origin', + 'news.adobe.com': 'main--news--adobecom.hlx.live', + }, + '--blog--adobecom.hlx.page': { + 'blog.adobe.com': 'origin', + 'business.adobe.com': 'main--bacom--adobecom.hlx.page', + }, + '.business-graybox.adobe.com': { 'business.adobe.com': 'origin' }, }; const config = { diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 3b57b87e96..07fee41492 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -638,17 +638,32 @@ const decorateCopyLink = (a, evt) => { }); }; +export function convertStageLinks({ anchors, config, hostname }) { + if (config.env?.name === 'prod' || !config.stageDomainsMap) return; + const matchedRules = Object.entries(config.stageDomainsMap) + .find(([domain]) => hostname.includes(domain)); + if (!matchedRules) return; + const [, domainsMap] = matchedRules; + [...anchors].forEach((a) => { + const matchedDomain = Object.keys(domainsMap) + .find((domain) => a.href.includes(domain)); + if (!matchedDomain) return; + a.href = a.href.replace(a.hostname, domainsMap[matchedDomain] === 'origin' + ? hostname + : domainsMap[matchedDomain]); + }); +} + export function decorateLinks(el) { const config = getConfig(); decorateImageLinks(el); const anchors = el.getElementsByTagName('a'); + const { hostname } = window.location; + convertStageLinks({ anchors, config, hostname }); return [...anchors].reduce((rdx, a) => { appendHtmlToLink(a); a.href = localizeLink(a.href); decorateSVG(a); - if (config.env?.name === 'stage' && config.stageDomainsMap?.[a.hostname]) { - a.href = a.href.replace(a.hostname, config.stageDomainsMap[a.hostname]); - } if (a.href.includes('#_blank')) { a.setAttribute('target', '_blank'); a.href = a.href.replace('#_blank', ''); diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js index d3132ea524..669817fe6b 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -10,6 +10,30 @@ const config = { codeRoot: '/libs', locales: { '': { ietf: 'en-US', tk: 'hah7vzn.css' } }, }; +const stageDomainsMap = { + 'www.stage.adobe.com': { + 'www.adobe.com': 'origin', + 'business.adobe.com': 'business.stage.adobe.com', + 'blog.adobe.com': 'blog.stage.adobe.com', + 'helpx.adobe.com': 'helpx.stage.adobe.com', + 'news.adobe.com': 'news.stage.adobe.com', + }, + '--bacom--adobecom.hlx.live': { + 'business.adobe.com': 'origin', + 'blog.adobe.com': 'main--blog--adobecom.hlx.live', + 'helpx.adobe.com': 'main--helpx--adobecom.hlx.live', + 'news.adobe.com': 'main--news--adobecom.hlx.live', + }, + '--blog--adobecom.hlx.page': { + 'blog.adobe.com': 'origin', + 'business.adobe.com': 'main--bacom--adobecom.hlx.page', + 'helpx.adobe.com': 'main--helpx--adobecom.hlx.page', + 'news.adobe.com': 'main--news--adobecom.hlx.page', + }, + '.business-graybox.adobe.com': { 'business.adobe.com': 'origin' }, +}; +const prodDomains = ['www.adobe.com', 'business.adobe.com', 'blog.adobe.com', 'helpx.adobe.com', 'news.adobe.com']; +const externalDomains = ['external1.com', 'external2.com']; const ogFetch = window.fetch; describe('Utils', () => { @@ -114,7 +138,7 @@ describe('Utils', () => { await waitForElement('.login-action'); const login = document.querySelector('.login-action'); utils.decorateLinks(login); - expect(login.href).to.equal('https://www.stage.adobe.com/'); + expect(login.href).to.equal('https://www.adobe.com/'); }); it('Implements a copy link action', async () => { await waitForElement('.copy-action'); @@ -473,22 +497,71 @@ describe('Utils', () => { expect(document.querySelector('.quote.hide-block')).to.be.null; }); - it('should convert prod links to stage links on stage env', async () => { - const stageDomainsMap = { - 'www.adobe.com': 'www.stage.adobe.com', - 'blog.adobe.com': 'blog.stage.adobe.com', - 'business.adobe.com': 'business.stage.adobe.com', - 'helpx.adobe.com': 'helpx.stage.adobe.com', - 'news.adobe.com': 'news.stage.adobe.com', + it('should convert links on stage when stageDomainsMap provided', async () => { + const stageConfig = { + ...config, + env: { name: 'stage' }, + stageDomainsMap, }; - utils.setConfig({ + + Object.entries(stageDomainsMap).forEach(([hostname, domainsMap]) => { + const anchors = Object.keys(domainsMap).map((d) => utils.createTag('a', { href: `https://${d}` })); + const externalAnchors = externalDomains.map((url) => utils.createTag('a', { href: url })); + + utils.convertStageLinks({ + anchors: [...anchors, ...externalAnchors], + config: stageConfig, + hostname, + }); + + anchors.forEach((a, index) => { + const expectedDomain = Object.values(domainsMap)[index]; + expect(a.href).to.contain(expectedDomain === 'origin' ? hostname : expectedDomain); + }); + + externalAnchors.forEach((a) => expect(a.href).to.equal(a.href)); + }); + }); + + it('should not convert links on stage when no stageDomainsMap provided', async () => { + const stageConfig = { ...config, env: { name: 'stage' }, + }; + + Object.entries(stageDomainsMap).forEach(([hostname, domainsMap]) => { + const anchors = Object.keys(domainsMap).map((d) => utils.createTag('a', { href: `https://${d}` })); + const externalAnchors = externalDomains.map((url) => utils.createTag('a', { href: url })); + + utils.convertStageLinks({ + anchors: [...anchors, ...externalAnchors], + config: stageConfig, + hostname, + }); + + [...anchors, ...externalAnchors].forEach((a) => expect(a.href).to.equal(a.href)); + }); + }); + + it('should not convert links on prod', async () => { + const prodConfig = { + ...config, + env: { name: 'prod' }, stageDomainsMap, + }; + + prodDomains.forEach((hostname) => { + const anchors = prodDomains.map((d) => utils.createTag('a', { href: `https://${d}` })); + const externalAnchors = externalDomains.map((url) => utils.createTag('a', { href: url })); + + utils.convertStageLinks({ + anchors: [...anchors, ...externalAnchors], + config: prodConfig, + hostname, + }); + + [...anchors, ...externalAnchors].forEach((a) => expect(a.href).to.equal(a.href)); }); - const links = Object.keys(stageDomainsMap).map((prodDom) => document.body.appendChild(createTag('a', { href: `https://${prodDom}`, 'data-prod-dom': prodDom }))); - await utils.decorateLinks(document.body); - links.forEach((l) => expect(l.hostname === stageDomainsMap[l.dataset.prodDom]).to.be.true); }); }); From c7107532b6a9f8860679aec89695ef4069491cf2 Mon Sep 17 00:00:00 2001 From: Raghav Sharma <118168183+sharmrj@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:48:43 +0530 Subject: [PATCH 06/22] MWPW-151534 [PEP Prompt] Dismissal action on close button of prompt not seen in windows (#2866) * Made the pep close button focus a consistent color * update franklin cache with dummy change * revert previous change --- libs/features/webapp-prompt/webapp-prompt.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libs/features/webapp-prompt/webapp-prompt.css b/libs/features/webapp-prompt/webapp-prompt.css index 39d012916a..dee60577b1 100644 --- a/libs/features/webapp-prompt/webapp-prompt.css +++ b/libs/features/webapp-prompt/webapp-prompt.css @@ -2,6 +2,7 @@ .appPrompt { --pep-background-prompt: #ffffff; --pep-background-progress: #e9e9e9; + --pep-dismiss-button-focus-border-color: #3b63fb; display: none; } @@ -159,10 +160,7 @@ } .appPrompt-close:focus { - /* For Firefox */ - outline: auto; - /* For Chrome, Edge, and Safari */ - outline: 2px solid -webkit-focus-ring-color; + outline: 2px solid var(--pep-dismiss-button-focus-border-color); } .appPrompt-progressWrapper { From 3679fd07498889cb974f46602dace93b3c577445 Mon Sep 17 00:00:00 2001 From: Vivian A Goodrich <101133187+vgoodric@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:39:40 -0600 Subject: [PATCH 07/22] REVERT MWPW-157686 [MEP] Cannot spoof an experience that exists in manifest but not in Target #2844 (#2870) revert --- libs/features/personalization/personalization.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 091e6ad027..b51101f720 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -894,6 +894,8 @@ export function cleanAndSortManifestList(manifests) { freshManifest = manifestObj[manifest.manifestPath]; } freshManifest.name = fullManifest.name; + freshManifest.selectedVariantName = fullManifest.selectedVariantName; + freshManifest.selectedVariant = freshManifest.variants[freshManifest.selectedVariantName]; manifestObj[manifest.manifestPath] = freshManifest; } else { manifestObj[manifest.manifestPath] = manifest; From a7c31b7acb30e0556b32252826e2ddcd56ec21ab Mon Sep 17 00:00:00 2001 From: sharathkannan <138484653+sharath-kannan@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:30:29 +0530 Subject: [PATCH 08/22] fix(MWPW-155964):Lana log error type changed to info. (#2818) * lana log error type changed * modified lana logs * added a few unit tests for lana logs * Updated tag in utils.js * linting error fixed * linting error fix 2 * unit test fix * added unit test for vimeo --- libs/blocks/article-feed/article-helpers.js | 2 +- .../event-rich-results/event-rich-results.js | 2 +- libs/blocks/faas/utils.js | 2 +- libs/blocks/library-config/lists/templates.js | 2 +- .../mobile-app-banner/mobile-app-banner.js | 2 +- libs/blocks/path-finder/path-finder.js | 2 +- libs/blocks/preflight/panels/seo.js | 4 +-- libs/blocks/quiz-entry/mlField.js | 2 +- libs/blocks/quiz-entry/quiz-entry.js | 2 +- libs/blocks/quiz-entry/quizPopover.js | 2 +- libs/blocks/quiz-results/quiz-results.js | 4 +-- libs/blocks/tag-selector/tag-selector.js | 2 +- libs/blocks/video-metadata/video-metadata.js | 8 +++--- libs/blocks/vimeo/vimeo.js | 2 +- libs/features/footer-promo.js | 2 +- libs/martech/martech.js | 4 +-- .../article-header/article-header.test.js | 28 +++++++++++++------ .../global-footer/global-footer.test.js | 6 ++-- test/blocks/quiz-results/mocks/body.html | 10 +++++++ test/blocks/quiz-results/quiz-results.test.js | 7 +++++ test/blocks/vimeo/vimeo.test.html | 12 +++++++- .../webapp-prompt/webapp-prompt.test.js | 4 +-- 22 files changed, 74 insertions(+), 37 deletions(-) diff --git a/libs/blocks/article-feed/article-helpers.js b/libs/blocks/article-feed/article-helpers.js index 779099e8f1..e9cfe1d59b 100644 --- a/libs/blocks/article-feed/article-helpers.js +++ b/libs/blocks/article-feed/article-helpers.js @@ -129,7 +129,7 @@ export async function loadTaxonomy() { a.href = tax.link; } else { // eslint-disable-next-line no-console - window.lana.log(`Trying to get a link for an unknown topic: ${topic} (current page)`, { tags: 'errorType=warn,module=article-feed' }); + window.lana.log(`Trying to get a link for an unknown topic: ${topic} (current page)`, { tags: 'article-feed' }); a.href = '#'; } delete a.dataset.topicLink; diff --git a/libs/blocks/event-rich-results/event-rich-results.js b/libs/blocks/event-rich-results/event-rich-results.js index e0df0551d6..78f69ff15b 100644 --- a/libs/blocks/event-rich-results/event-rich-results.js +++ b/libs/blocks/event-rich-results/event-rich-results.js @@ -16,7 +16,7 @@ function logNullValues(obj) { Object.keys(obj).forEach((key) => { const value = obj[key]; if (!value || value === '') { - window.lana.log(`Event property ${key} is not defined`, { tags: 'errorType=warn,module=event-rich-results' }); + window.lana.log(`Event property ${key} is not defined`, { tags: 'event-rich-results' }); } logNullValues(value); }); diff --git a/libs/blocks/faas/utils.js b/libs/blocks/faas/utils.js index 70c17f093d..9c3249d7e4 100644 --- a/libs/blocks/faas/utils.js +++ b/libs/blocks/faas/utils.js @@ -248,7 +248,7 @@ const beforeSubmitCallback = () => { }), }) .catch((error) => { - window.lana.log(`AA Sandbox Error: ${error.reason || error.error || error.message || error}`, { tags: 'errorType=info,module=faas' }); + window.lana.log(`AA Sandbox Error: ${error.reason || error.error || error.message || error}`, { tags: 'faas', errorType: 'i' }); }); } }; diff --git a/libs/blocks/library-config/lists/templates.js b/libs/blocks/library-config/lists/templates.js index 2c1f780078..736f79c35c 100644 --- a/libs/blocks/library-config/lists/templates.js +++ b/libs/blocks/library-config/lists/templates.js @@ -43,7 +43,7 @@ function formatDom(aemDom, path) { async function formatTemplate(path) { const resp = await fetch(path); - if (!resp.ok) window.lana.log('Could not fetch template path', { tags: 'errorType=info,module=sidekick-templates' }); + if (!resp.ok) window.lana.log('Could not fetch template path', { tags: 'sidekick-templates', errorType: 'i' }); const html = await resp.text(); const dom = new DOMParser().parseFromString(html, 'text/html'); diff --git a/libs/blocks/mobile-app-banner/mobile-app-banner.js b/libs/blocks/mobile-app-banner/mobile-app-banner.js index 9c5988ee44..62f68b7086 100644 --- a/libs/blocks/mobile-app-banner/mobile-app-banner.js +++ b/libs/blocks/mobile-app-banner/mobile-app-banner.js @@ -18,7 +18,7 @@ async function getECID() { if (window.alloy) { await window.alloy('getIdentity').then((data) => { ecid = data?.identity?.ECID; - }).catch((err) => window.lana.log(`Error fetching ECID: ${err}`, { tags: 'errorType=error,module=mobile-app-banner' })); + }).catch((err) => window.lana.log(`Error fetching ECID: ${err}`, { tags: 'mobile-app-banner' })); } return ecid; } diff --git a/libs/blocks/path-finder/path-finder.js b/libs/blocks/path-finder/path-finder.js index 8cad4c1c9d..3ba93e71df 100644 --- a/libs/blocks/path-finder/path-finder.js +++ b/libs/blocks/path-finder/path-finder.js @@ -63,7 +63,7 @@ function buildUi(el, path) { async function setup(el) { await login({ scopes: SCOPES, telemetry: TELEMETRY }); if (!account.value.username) { - window.lana.log('Could not login to MS Graph', { tags: 'errorType=info,module=path-finder' }); + window.lana.log('Could not login to MS Graph', { tags: 'path-finder', errorType: 'i' }); return; } el.innerHTML = ''; diff --git a/libs/blocks/preflight/panels/seo.js b/libs/blocks/preflight/panels/seo.js index e69c561a7d..443b35d7c8 100644 --- a/libs/blocks/preflight/panels/seo.js +++ b/libs/blocks/preflight/panels/seo.js @@ -160,7 +160,7 @@ async function spidyCheck(url) { connectionError(); } catch (e) { connectionError(); - window.lana.log(`There was a problem connecting to the link check API ${url}. ${e}`, { tags: 'errorType=info,module=preflight' }); + window.lana.log(`There was a problem connecting to the link check API ${url}. ${e}`, { tags: 'preflight', errorType: 'i' }); } return false; } @@ -182,7 +182,7 @@ async function getSpidyResults(url, opts) { return acc; }, []); } catch (e) { - window.lana.log(`There was a problem connecting to the link check API ${url}/api/url-http-status. ${e}`, { tags: 'errorType=info,module=preflight' }); + window.lana.log(`There was a problem connecting to the link check API ${url}/api/url-http-status. ${e}`, { tags: 'preflight', errorType: 'i' }); return []; } } diff --git a/libs/blocks/quiz-entry/mlField.js b/libs/blocks/quiz-entry/mlField.js index a04545b80c..17b9f72fc9 100644 --- a/libs/blocks/quiz-entry/mlField.js +++ b/libs/blocks/quiz-entry/mlField.js @@ -26,7 +26,7 @@ export const getMLResults = async (endpoint, apiKey, threshold, input, count, va body: JSON.stringify(params), }) .then((response) => response.json()) - .catch((error) => window.lana.log(`ERROR: Fetching fi codes ${error}`, { tags: 'errorType=info,module=quiz-entry' })); + .catch((error) => window.lana.log(`ERROR: Fetching fi codes ${error}`, { tags: 'quiz-entry', errorType: 'i' })); let value; let highestProb = null; diff --git a/libs/blocks/quiz-entry/quiz-entry.js b/libs/blocks/quiz-entry/quiz-entry.js index 6c9c9394ba..ea83b47c60 100644 --- a/libs/blocks/quiz-entry/quiz-entry.js +++ b/libs/blocks/quiz-entry/quiz-entry.js @@ -212,7 +212,7 @@ const App = ({ } if (fiResults.errors) error = fiResults.errors[0].title; if (fiResults.error_code) error = fiResults.message; - window.lana.log(`ML results error - ${error}`, { tags: 'errorType=info,module=quiz-entry' }); + window.lana.log(`ML results error - ${error}`, { tags: 'quiz-entry', errorType: 'i' }); sendMLFieldAnalytics(fallback, false); } diff --git a/libs/blocks/quiz-entry/quizPopover.js b/libs/blocks/quiz-entry/quizPopover.js index 1c444d0d3d..ed0edbe212 100644 --- a/libs/blocks/quiz-entry/quizPopover.js +++ b/libs/blocks/quiz-entry/quizPopover.js @@ -12,7 +12,7 @@ export const getSuggestions = async (endpoint, clientId, input, scope) => { }); if (!response.ok) { - window.lana.log('Failed to fetch suggestions', { tags: 'errorType=info,module=quiz-entry' }); + window.lana.log('Failed to fetch suggestions', { tags: 'quiz-entry', errorType: 'i' }); return ''; } diff --git a/libs/blocks/quiz-results/quiz-results.js b/libs/blocks/quiz-results/quiz-results.js index 7465782fc7..7c9622366a 100644 --- a/libs/blocks/quiz-results/quiz-results.js +++ b/libs/blocks/quiz-results/quiz-results.js @@ -20,7 +20,7 @@ async function loadFragments(el, experiences) { function redirectPage(quizUrl, debug, message) { const url = quizUrl ? getLocalizedURL(quizUrl.text) : 'https://adobe.com'; - window.lana.log(message, { tags: 'errorType=error,module=quiz-results' }); + window.lana.log(message, { tags: 'quiz-results' }); if (debug === 'quiz-results') { // eslint-disable-next-line no-console @@ -97,7 +97,7 @@ export default async function init(el, debug = null, localStoreKey = null) { loadFragments(el, basic); } else { - window.lana.log(`${LOADING_ERROR} The quiz-results block is misconfigured`, { tags: 'errorType=error,module=quiz-results' }); + window.lana.log(`${LOADING_ERROR} The quiz-results block is misconfigured`, { tags: 'quiz-results' }); return; } diff --git a/libs/blocks/tag-selector/tag-selector.js b/libs/blocks/tag-selector/tag-selector.js index 8607fed08c..ccfc43f9c9 100644 --- a/libs/blocks/tag-selector/tag-selector.js +++ b/libs/blocks/tag-selector/tag-selector.js @@ -86,7 +86,7 @@ const TagSelector = ({ consumerUrls = [] }) => { const fetchCasS = async () => { const { tags, errorMsg } = await loadCaasTags(caasTagUrl); - if (errorMsg) window.lana.log(`Tag Selector. Error fetching caas tags: ${errorMsg}`, { tags: 'errorType=info,module=tag-selector' }); + if (errorMsg) window.lana.log(`Tag Selector. Error fetching caas tags: ${errorMsg}`, { tags: 'tag-selector', errorType: 'i' }); setTagSelectorTags((prevConsumerTags) => ({ CaaS: tags, ...prevConsumerTags })); }; diff --git a/libs/blocks/video-metadata/video-metadata.js b/libs/blocks/video-metadata/video-metadata.js index 388cb62034..d94b9410b4 100644 --- a/libs/blocks/video-metadata/video-metadata.js +++ b/libs/blocks/video-metadata/video-metadata.js @@ -25,7 +25,7 @@ function addBroadcastEventField(videoObj, blockKey, blockValue) { videoObj.publication[i][camelize(key)] = blockValue; break; default: - window.lana.log(`VideoMetadata -- Unknown BroadcastEvent property: ${blockKey}`, { tags: 'errorType=warn,module=video-metadata' }); + window.lana.log(`VideoMetadata -- Unknown BroadcastEvent property: ${blockKey}`, { tags: 'video-metadata' }); break; } } @@ -45,7 +45,7 @@ function addClipField(videoObj, blockKey, blockValue) { videoObj.hasPart[i][camelize(key)] = blockValue; break; default: - window.lana.log(`VideoMetadata -- Unhandled Clip property: ${blockKey}`, { tags: 'errorType=warn,module=video-metadata' }); + window.lana.log(`VideoMetadata -- Unhandled Clip property: ${blockKey}`, { tags: 'video-metadata' }); break; } } @@ -61,7 +61,7 @@ function addSeekToActionField(videoObj, blockKey, blockValue) { videoObj.potentialAction['startOffset-input'] = blockValue; break; default: - window.lana.log(`VideoMetadata -- Unhandled SeekToAction property: ${blockKey}`, { tags: 'errorType=warn,module=video-metadata' }); + window.lana.log(`VideoMetadata -- Unhandled SeekToAction property: ${blockKey}`, { tags: 'video-metadata' }); break; } } @@ -96,7 +96,7 @@ export function createVideoObject(record) { addSeekToActionField(video, blockKey, blockVal); break; default: - window.lana.log(`VideoMetadata -- Unhandled VideoObject property: ${blockKey}`, { tags: 'errorType=warn,module=video-metadata' }); + window.lana.log(`VideoMetadata -- Unhandled VideoObject property: ${blockKey}`, { tags: 'video-metadata' }); break; } }); diff --git a/libs/blocks/vimeo/vimeo.js b/libs/blocks/vimeo/vimeo.js index deabe6bb85..10c55b599d 100644 --- a/libs/blocks/vimeo/vimeo.js +++ b/libs/blocks/vimeo/vimeo.js @@ -36,7 +36,7 @@ class LiteVimeo extends HTMLElement { this.style.backgroundImage = `url("${thumbnailUrl}")`; }) .catch((e) => { - window.lana.log(`Error fetching Vimeo thumbnail: ${e}`, { tags: 'errorType=info,module=vimeo' }); + window.lana.log(`Error fetching Vimeo thumbnail: ${e}`, { tags: 'vimeo', errorType: 'i' }); }); } diff --git a/libs/features/footer-promo.js b/libs/features/footer-promo.js index 78466a0055..2d47501b84 100644 --- a/libs/features/footer-promo.js +++ b/libs/features/footer-promo.js @@ -20,7 +20,7 @@ async function getPromoFromTaxonomy(contentRoot, doc) { if (primaryTag) return primaryTag[FOOTER_PROMO_LINK_KEY]; } catch (error) { /* c8 ignore next 2 */ - window.lana.log(`Footer Promo - Taxonomy error: ${error}`, { tags: 'errorType=info,module=footer-promo' }); + window.lana.log(`Footer Promo - Taxonomy error: ${error}`, { tags: 'footer-promo', errorType: 'i' }); } return undefined; } diff --git a/libs/martech/martech.js b/libs/martech/martech.js index 91906910bc..01492cadfe 100644 --- a/libs/martech/martech.js +++ b/libs/martech/martech.js @@ -118,14 +118,14 @@ export const getTargetPersonalization = async () => { const responseStart = Date.now(); window.addEventListener(ALLOY_SEND_EVENT, () => { const responseTime = calculateResponseTime(responseStart); - window.lana.log(`target response time: ${responseTime}`, { tags: 'errorType=info,module=martech' }); + window.lana.log(`target response time: ${responseTime}`, { tags: 'martech', errorType: 'i' }); }, { once: true }); let manifests = []; let propositions = []; const response = await waitForEventOrTimeout(ALLOY_SEND_EVENT, timeout); if (response.error) { - window.lana.log('target response time: ad blocker', { tags: 'errorType=info,module=martech' }); + window.lana.log('target response time: ad blocker', { tags: 'martech', errorType: 'i' }); return []; } if (response.timeout) { diff --git a/test/blocks/article-header/article-header.test.js b/test/blocks/article-header/article-header.test.js index cbcf096c1f..7c0a20bd17 100644 --- a/test/blocks/article-header/article-header.test.js +++ b/test/blocks/article-header/article-header.test.js @@ -1,7 +1,6 @@ import { readFile } from '@web/test-runner-commands'; import { expect } from '@esm-bundle/chai'; -import sinon from 'sinon'; - +import sinon, { stub } from 'sinon'; import { setConfig, getConfig } from '../../../libs/utils/utils.js'; import { delay, waitForElement } from '../../helpers/waitfor.js'; @@ -9,9 +8,11 @@ const locales = { '': { ietf: 'en-US', tk: 'hah7vzn.css' } }; const conf = { locales, miloLibs: 'http://localhost:2000/libs' }; setConfig(conf); const config = getConfig(); +window.lana = { log: stub() }; document.body.innerHTML = await readFile({ path: './mocks/body.html' }); const { default: init } = await import('../../../libs/blocks/article-header/article-header.js'); +const { loadTaxonomy } = await import('../../../libs/blocks/article-feed/article-helpers.js'); const invalidDoc = await readFile({ path: './mocks/body-invalid.html' }); @@ -20,10 +21,20 @@ describe('article header', () => { const block = document.body.querySelector('.article-header'); config.locale.contentRoot = '/test/blocks/article-header/mocks'; config.taxonomyRoot = undefined; - await init(block); }); + it('should log unknown topic', async () => { + try { + const div = document.createElement('div'); + div.setAttribute('data-topic-link', ['abcd']); + document.body.append(div); + await loadTaxonomy(); + expect(window.lana.log.args[0][0]).to.equal('Trying to get a link for an unknown topic: abcd (current page)'); + } catch (e) { + console.log(e); + } + }); it('creates article header block', () => { expect(document.body.querySelector('.article-category')).to.exist; expect(document.body.querySelector('.article-title')).to.exist; @@ -36,16 +47,16 @@ describe('article header', () => { it('should open link popup when share links are clicked', () => { // first share link is twitter const shareLink = document.querySelector('.article-byline-sharing a'); - const stub = sinon.stub(window, 'open'); + const windowStub = sinon.stub(window, 'open'); shareLink.click(); const url = encodeURIComponent(window.location.href); const title = encodeURIComponent(document.querySelector('h1').textContent); - expect(stub.calledOnce).to.be.true; - expect(stub.firstCall.args[0]).to.equal(`https://www.twitter.com/share?&url=${url}&text=${title}`); - expect(stub.firstCall.args[2]).to.equal('popup,top=233,left=233,width=700,height=467'); + expect(windowStub.calledOnce).to.be.true; + expect(windowStub.firstCall.args[0]).to.equal(`https://www.twitter.com/share?&url=${url}&text=${title}`); + expect(windowStub.firstCall.args[2]).to.equal('popup,top=233,left=233,width=700,height=467'); - stub.restore(); + windowStub.restore(); }); it('updates share text after deferred event', async () => { @@ -111,7 +122,6 @@ describe('test the invalid article header', () => { it('adds invalid-date when invalid date is provided', async () => { await init(document.body.querySelector('.article-header')); - const date = await waitForElement('.article-date-invalid'); expect(date).to.exist; }); diff --git a/test/blocks/global-footer/global-footer.test.js b/test/blocks/global-footer/global-footer.test.js index d99e9868c5..3ab306b9ce 100644 --- a/test/blocks/global-footer/global-footer.test.js +++ b/test/blocks/global-footer/global-footer.test.js @@ -378,7 +378,7 @@ describe('global footer', () => { await createFullGlobalFooter({ waitForDecoration: false }); await clock.runAllAsync(); expect(window.lana.log.getCalls().find((c) => c.args[0].includes('Failed to fetch footer content'))); - expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('errorType=warn,module=global-footer'))); + expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('global-footer'))); }); it('should send log when could not create URL for region picker', async () => { @@ -393,7 +393,7 @@ describe('global footer', () => { // should throw error } expect(window.lana.log.getCalls().find((c) => c.args[0].includes('Could not create URL for region picker'))); - expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('errorType=error,module=global-footer'))); + expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('global-footer'))); }); it('should send log when footer cannot be instantiated ', async () => { @@ -401,7 +401,7 @@ describe('global footer', () => { await createFullGlobalFooter({ waitForDecoration: false }); await clock.runAllAsync(); expect(window.lana.log.getCalls().find((c) => c.args[0].includes('Footer could not be instantiated'))); - expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('errorType=error,module=global-footer'))); + expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('global-footer'))); }); it('should send LANA log when icons.svg has some network issue', async () => { diff --git a/test/blocks/quiz-results/mocks/body.html b/test/blocks/quiz-results/mocks/body.html index 7323de08ac..10823f4ee2 100644 --- a/test/blocks/quiz-results/mocks/body.html +++ b/test/blocks/quiz-results/mocks/body.html @@ -38,5 +38,15 @@
http://this-is-a-fake-redirect-url
+
+
+
nested-fragments
+
marquee-product
+
+
+
style
+
m-spacing
+
+
diff --git a/test/blocks/quiz-results/quiz-results.test.js b/test/blocks/quiz-results/quiz-results.test.js index 37bb6b77b9..7b0b887cb3 100644 --- a/test/blocks/quiz-results/quiz-results.test.js +++ b/test/blocks/quiz-results/quiz-results.test.js @@ -76,4 +76,11 @@ describe('Quiz Results', () => { expect(el.querySelector('.fragment > .section > .content').getAttribute('daa-lh')).to.equal('b1|content'); expect(el.querySelector('a').getAttribute('daa-ll')).to.equal('Fragment link-1--This is a basic frag'); }); + it('should return misconfigured block', async () => { + const el = document.body.querySelector('.nested-three'); + localStorage.setItem('misconf', JSON.stringify(mockData.mockTwo)); + el.classList.remove('nested'); + await init(el, 'quiz-results', 'misconf'); + expect(window.lana.log.args[2][0]).to.equal(`${LOADING_ERROR} The quiz-results block is misconfigured`); + }); }); diff --git a/test/blocks/vimeo/vimeo.test.html b/test/blocks/vimeo/vimeo.test.html index 50504a2436..8eb108eb54 100644 --- a/test/blocks/vimeo/vimeo.test.html +++ b/test/blocks/vimeo/vimeo.test.html @@ -15,9 +15,11 @@ diff --git a/test/features/webapp-prompt/webapp-prompt.test.js b/test/features/webapp-prompt/webapp-prompt.test.js index c065acd726..e7e8308cfa 100644 --- a/test/features/webapp-prompt/webapp-prompt.test.js +++ b/test/features/webapp-prompt/webapp-prompt.test.js @@ -239,7 +239,7 @@ describe('PEP', () => { }), }); 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; + expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('pep'))).to.exist; }); it('should send log when cannot fetch content for prompt', async () => { @@ -256,7 +256,7 @@ describe('PEP', () => { }); await initPep({}); 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; + expect(window.lana.log.getCalls().find((c) => c.args[1].tags.includes('pep'))).to.exist; }); }); }); From 8fd5925aa2bbf6b9bb757c3305727291ecec82b5 Mon Sep 17 00:00:00 2001 From: Sean Archibeque Date: Thu, 12 Sep 2024 11:00:36 -0600 Subject: [PATCH 09/22] MWPW-154980 - Milo advanced page publishing (#2846) * sidekick publish button state * bulk publish publish config errors * dial in functionality add custom messages and denylist * publish permission in bulk pub testing * test permissions and utils coverage * remove extra variable assignment * add no signin message * put back timout for init sidekick pub button decoration * fix edge case where sk event isnt fired on refresh * fix unit tests * put back var assignment * dial in user can publish funtion * move publish utility to tools dir * Update tools/utils/utils.js Co-authored-by: Ryan Parrish --------- Co-authored-by: Ryan Parrish --- .../components/bulk-publisher.js | 17 ++++-- libs/blocks/bulk-publish-v2/services.js | 25 ++++++++ libs/tools/utils/publish.js | 32 ++++++++++ libs/utils/sidekick-decorate.js | 61 ++++++++++++++++--- .../bulk-publish-v2/bulk-publish-v2.test.js | 33 ++++++---- .../bulk-publish-v2/mocks/authentication.js | 2 +- test/blocks/bulk-publish-v2/mocks/fetch.js | 3 +- .../mocks/response/permissions.json | 26 ++++++++ tools/utils/utils.js | 14 ++++- 9 files changed, 183 insertions(+), 30 deletions(-) create mode 100644 libs/tools/utils/publish.js create mode 100644 test/blocks/bulk-publish-v2/mocks/response/permissions.json diff --git a/libs/blocks/bulk-publish-v2/components/bulk-publisher.js b/libs/blocks/bulk-publish-v2/components/bulk-publisher.js index c0c88ae5db..712f663fa0 100644 --- a/libs/blocks/bulk-publish-v2/components/bulk-publisher.js +++ b/libs/blocks/bulk-publish-v2/components/bulk-publisher.js @@ -1,7 +1,7 @@ import './job-process.js'; import { LitElement, html } from '../../../deps/lit-all.min.js'; import { getSheet } from '../../../../tools/utils/utils.js'; -import { authenticate, startJob } from '../services.js'; +import { authenticate, getPublishable, startJob } from '../services.js'; import { getConfig } from '../../../utils/utils.js'; import { delay, @@ -95,7 +95,8 @@ class BulkPublish2 extends LitElement { this.validateUrls(); } - setJobErrors(errors) { + setJobErrors(jobErrors, authErrors) { + const errors = [...jobErrors, ...authErrors]; const urls = []; errors.forEach((error) => { const matched = this.urls.filter((url) => { @@ -323,7 +324,8 @@ class BulkPublish2 extends LitElement { class="panel-title" @click=${handleToggle}> - Job Results + ${this.jobs.length ? html`${this.jobs.length}` : ''} + Job Result${this.jobs.length > 1 ? 's' : ''}
{ + /* c8 ignore next 4 */ const loader = this.renderRoot.querySelector('.load-indicator'); const message = this.renderRoot.querySelector('.message'); loader?.classList.add('hide'); @@ -427,6 +431,7 @@ class BulkPublish2 extends LitElement { const canUse = Object.values(this.user.permissions).filter((perms) => perms.canUse); if (canUse.length) return html``; message = 'Current user is not authorized to use Bulk Publishing Tool'; + /* c8 ignore next 3 */ } else { message = 'Please sign in to AEM sidekick to continue'; } diff --git a/libs/blocks/bulk-publish-v2/services.js b/libs/blocks/bulk-publish-v2/services.js index 2889aa5660..76af892bf2 100644 --- a/libs/blocks/bulk-publish-v2/services.js +++ b/libs/blocks/bulk-publish-v2/services.js @@ -1,3 +1,4 @@ +import userCanPublishPage from '../../tools/utils/publish.js'; import { PROCESS_TYPES, getErrorText, @@ -246,8 +247,32 @@ const updateRetry = async ({ queue, urls, process }) => { return newQueue; }; +// publish authentication service +const getPublishable = async ({ urls, process, user }) => { + let publishable = { authorized: [], unauthorized: [] }; + if (!isLive(process)) { + publishable.authorized = urls; + } else { + const { permissions, profile } = user; + const live = { permissions: ['read'] }; + if (permissions?.publish?.canUse) { + live.permissions.push('write'); + } + publishable = await urls.reduce(async (init, url) => { + const result = await init; + const detail = { webPath: new URL(url).pathname, live, profile }; + const { canPublish, message } = await userCanPublishPage(detail); + if (canPublish) result.authorized.push(url); + else result.unauthorized.push({ href: url, message }); + return result; + }, Promise.resolve(publishable)); + } + return publishable; +}; + export { authenticate, + getPublishable, pollJobStatus, startJob, updateRetry, diff --git a/libs/tools/utils/publish.js b/libs/tools/utils/publish.js new file mode 100644 index 0000000000..49acf0928d --- /dev/null +++ b/libs/tools/utils/publish.js @@ -0,0 +1,32 @@ +import { getCustomConfig } from '../../../tools/utils/utils.js'; + +const userCanPublishPage = async (detail, isBulk = true) => { + if (!detail) return false; + const { live, profile, webPath } = detail; + let canPublish = isBulk ? live?.permissions?.includes('write') : true; + let message = 'Publishing is currently disabled for this page'; + const config = await getCustomConfig('/.milo/publish-permissions-config.json'); + const item = config?.urls?.data?.find(({ url }) => ( + url.endsWith('**') ? webPath.includes(url.slice(0, -2)) : url === webPath + )); + if (item) { + canPublish = false; + if (item.message) message = item.message; + const group = config[item.group]; + if (group && profile?.email) { + let isDeny; + const user = group.data?.find(({ allow, deny }) => { + if (deny) { + /* c8 ignore next 3 */ + isDeny = true; + return deny === profile.email; + } + return allow === profile.email; + }); + canPublish = isDeny ? !user : !!user; + } + } + return { canPublish, message }; +}; + +export default userCanPublishPage; diff --git a/libs/utils/sidekick-decorate.js b/libs/utils/sidekick-decorate.js index ec125ef650..acaa428716 100644 --- a/libs/utils/sidekick-decorate.js +++ b/libs/utils/sidekick-decorate.js @@ -1,4 +1,22 @@ +import userCanPublishPage from '../tools/utils/publish.js'; + +const PUBLISH_BTN = '.publish.plugin button'; +const BACKUP_PROFILE = '.profile-email'; +const CONFIRM_MESSAGE = 'Are you sure? This will publish to production.'; + export default function stylePublish(sk) { + const setupPublishBtn = async (page, btn) => { + const { canPublish, message } = await userCanPublishPage(page, false); + btn.setAttribute('disabled', !canPublish); + const messageText = btn.querySelector('span'); + const text = canPublish ? CONFIRM_MESSAGE : message; + if (messageText) { + messageText.innerText = text; + } else { + btn.insertAdjacentHTML('beforeend', `${text}`); + } + }; + const style = new CSSStyleSheet(); style.replaceSync(` :host { @@ -10,19 +28,21 @@ export default function stylePublish(sk) { order: 100; } .publish.plugin button { + position: relative; + } + .publish.plugin button:not([disabled=true]) { background: var(--bg-color); border-color: #b46157; color: var(--text-color); - position: relative; } - .publish.plugin button:hover { + .publish.plugin button:not([disabled=true]):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); + background: #666; border-radius: 4px; line-height: 1.2rem; padding: 8px 12px; @@ -33,6 +53,9 @@ export default function stylePublish(sk) { width: 150px; white-space: pre-wrap; } + .publish.plugin button:not([disabled=true]) > span { + background: var(--bg-color); + } .publish.plugin button:hover > span { display: block; color: var(--text-color); @@ -41,19 +64,39 @@ export default function stylePublish(sk) { content: ''; border-left: 6px solid transparent; border-right: 6px solid transparent; - border-bottom: 6px solid var(--bg-color); + border-bottom: 6px solid #666; position: absolute; text-align: center; top: -6px; left: 50%; transform: translateX(-50%); } + .publish.plugin button:not([disabled=true]) > span:before { + border-bottom: 6px solid var(--bg-color); + } `); + sk.shadowRoot.adoptedStyleSheets = [style]; - setTimeout(() => { - const btn = sk.shadowRoot.querySelector('.publish.plugin button'); - btn?.insertAdjacentHTML('beforeend', ` - Are you sure? This will publish to production. - `); + + sk.addEventListener('statusfetched', async (event) => { + const page = event?.detail?.data; + const btn = event?.target?.shadowRoot?.querySelector(PUBLISH_BTN); + if (page && btn) { + setupPublishBtn(page, btn); + } + }); + + setTimeout(async () => { + const btn = sk.shadowRoot.querySelector(PUBLISH_BTN); + btn?.setAttribute('disabled', true); + const message = btn?.querySelector('span'); + if (btn && !message) { + const page = { + webPath: window.location.pathname, + // added for edge case where the statusfetched event isnt fired on refresh + profile: { email: sk.shadowRoot.querySelector(BACKUP_PROFILE)?.innerText }, + }; + setupPublishBtn(page, btn); + } }, 500); } diff --git a/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js b/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js index 3b634681d2..7cd56109b4 100644 --- a/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js +++ b/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js @@ -114,6 +114,16 @@ describe('Bulk Publish Tool', () => { await mouseEvent(rootEl.querySelector('.fix-btn')); }); + it('can trigger cannot publish config', async () => { + await clock.runAllAsync(); + await setProcess(rootEl, 'publish'); + await setTextArea(rootEl, 'https://error--milo--adobecom.hlx.page/not/a/valid/path'); + await mouseEvent(rootEl.querySelector('#RunProcess')); + const errors = rootEl.querySelector('.errors'); + expect(errors.querySelector('strong').innerText).to.equal('Publishing disabled until the test is over'); + await mouseEvent(rootEl.querySelector('.fix-btn')); + }); + it('can validate milo urls and enable form', async () => { await clock.runAllAsync(); await setProcess(rootEl, 'publish'); @@ -132,6 +142,17 @@ describe('Bulk Publish Tool', () => { await mouseEvent(rootEl.querySelector('.switch.half')); }); + it('can toggle job timing flyout', async () => { + await clock.runAllAsync(); + const doneJobProcess = rootEl.querySelector('job-process'); + const jobInfo = doneJobProcess?.shadowRoot.querySelector('job-info'); + const timerDetail = jobInfo?.shadowRoot.querySelector('.timer'); + await mouseEvent(timerDetail); + await clock.runAllAsync(); + await mouseEvent(timerDetail); + expect(timerDetail.classList.contains('show-times')).to.be.false; + }); + it('can submit valid bulk preview job', async () => { await clock.runAllAsync(); await setProcess(rootEl, 'preview'); @@ -174,17 +195,6 @@ describe('Bulk Publish Tool', () => { expect(rootEl.querySelectorAll('job-process')).to.have.lengthOf(4); }); - it('can toggle job timing flyout', async () => { - await clock.runAllAsync(); - const doneJobProcess = rootEl.querySelector('job-process'); - const jobInfo = doneJobProcess?.shadowRoot.querySelector('job-info'); - const timerDetail = jobInfo?.shadowRoot.querySelector('.timer'); - await mouseEvent(timerDetail); - await clock.runAllAsync(); - await mouseEvent(timerDetail); - expect(timerDetail.classList.contains('show-times')).to.be.false; - }); - it('can toggle view mode', async () => { await mouseEvent(rootEl.querySelector('.switch.full')); await clock.runAllAsync(); @@ -218,7 +228,6 @@ describe('Bulk Publish Tool', () => { it('can clear bulk jobs', async () => { await clock.runAllAsync(); await mouseEvent(rootEl.querySelector('.clear-jobs')); - await clock.runAllAsync(); expect(rootEl.querySelectorAll('job-process')).to.have.lengthOf(0); }); }); diff --git a/test/blocks/bulk-publish-v2/mocks/authentication.js b/test/blocks/bulk-publish-v2/mocks/authentication.js index 22d01045ad..ca826fa82d 100644 --- a/test/blocks/bulk-publish-v2/mocks/authentication.js +++ b/test/blocks/bulk-publish-v2/mocks/authentication.js @@ -23,7 +23,7 @@ class MockAuth extends HTMLElement { bubbles: true, detail: { data: { - profile: { name: 'Unit Test' }, + profile: { name: 'Unit Test', email: 'tester@adobe.com' }, preview: { permissions }, live: { permissions }, }, diff --git a/test/blocks/bulk-publish-v2/mocks/fetch.js b/test/blocks/bulk-publish-v2/mocks/fetch.js index 8ab62d86d3..48d7637f54 100644 --- a/test/blocks/bulk-publish-v2/mocks/fetch.js +++ b/test/blocks/bulk-publish-v2/mocks/fetch.js @@ -11,6 +11,7 @@ const requests = { delstatus: 'https://admin.hlx.page/job/adobecom/milo/main/preview-remove/job-2024-01-24t23-16-20-377z/details', retry: 'https://admin.hlx.page/preview/adobecom/milo/main/tools/bulk-publish-v2-test', index: 'https://admin.hlx.page/index/adobecom/milo/main/tools/bulk-publish-v2-test', + permissions: '/.milo/publish-permissions-config.json', }; const getMocks = async () => { @@ -25,7 +26,7 @@ const getMocks = async () => { export async function mockFetch() { const mocks = await getMocks(); stub(window, 'fetch').callsFake((...args) => { - const headers = args[1].body ?? null; + const headers = args[1]?.body ?? null; const body = headers ? JSON.parse(headers) : false; const [resource] = args; const response = mocks.find((mock) => (body.delete ? mock.request === 'delete' : mock.url === resource)); diff --git a/test/blocks/bulk-publish-v2/mocks/response/permissions.json b/test/blocks/bulk-publish-v2/mocks/response/permissions.json new file mode 100644 index 0000000000..37fe5eaec9 --- /dev/null +++ b/test/blocks/bulk-publish-v2/mocks/response/permissions.json @@ -0,0 +1,26 @@ +{ + "urls": { + "total": 4, + "offset": 0, + "limit": 4, + "data": [ + { + "url": "/not/a/valid/path", + "group": "gwp-test", + "message": "Publishing disabled until the test is over" + } + ] + }, + "gwp-test": { + "total": 2, + "offset": 0, + "limit": 2, + "data": [ + { "allow": "testuser@adobe.com" }, + { "allow": "testuser1@adobe.com" } + ] + }, + ":version": 3, + ":names": ["urls", "gwp-US", "no-publish", "gwp-FR"], + ":type": "multi-sheet" +} diff --git a/tools/utils/utils.js b/tools/utils/utils.js index 29d68397d8..8193d21fdf 100644 --- a/tools/utils/utils.js +++ b/tools/utils/utils.js @@ -1,6 +1,7 @@ const IMS_CLIENT_ID = 'milo_ims'; const IMS_PROD_URL = 'https://auth.services.adobe.com/imslib/imslib.min.js'; const STYLE_SHEETS = {}; +const CONFIGS = {}; const getImsToken = async (loadScript) => { window.adobeid = { @@ -25,4 +26,15 @@ const getSheet = async (url) => { return sheet; }; -export { getImsToken, getSheet }; +const getCustomConfig = async (path) => { + /* c8 ignore next 3 */ + if (CONFIGS[path] !== undefined) { + return CONFIGS[path]; + } + const resp = await fetch(path); + const config = resp.ok ? await resp.json() : null; + CONFIGS[path] = config; + return config; +}; + +export { getImsToken, getSheet, getCustomConfig }; From 43573462047147675f45e78c9af63d64debe2730 Mon Sep 17 00:00:00 2001 From: sharathkannan <138484653+sharath-kannan@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:30:42 +0530 Subject: [PATCH 10/22] fix(Mwpw-157971):sticky feature works in landscape mode. (#2856) * sticky-header variants added * removed sticky header from all mobile devices * linting error * Added new sticky variant * updated the selector for table buttons * sticky feature adapts to portrait and landscape * removed linting error --- libs/blocks/table/table.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/libs/blocks/table/table.js b/libs/blocks/table/table.js index 57ed330eb4..9d41802ff6 100644 --- a/libs/blocks/table/table.js +++ b/libs/blocks/table/table.js @@ -6,6 +6,7 @@ const DESKTOP_SIZE = 900; const MOBILE_SIZE = 768; const tableHighlightLoadedEvent = new Event('milo:table:highlight:loaded'); let tableIndex = 0; +const isMobileLandscape = () => (window.matchMedia('(orientation: landscape)').matches && window.innerHeight <= MOBILE_SIZE); function defineDeviceByScreenSize() { const screenWidth = window.innerWidth; if (screenWidth >= DESKTOP_SIZE) { @@ -17,6 +18,12 @@ function defineDeviceByScreenSize() { return 'TABLET'; } +function isStickyHeader(el) { + return el.classList.contains('sticky') + || (el.classList.contains('sticky-desktop-up') && defineDeviceByScreenSize() === 'DESKTOP') + || (el.classList.contains('sticky-tablet-up') && defineDeviceByScreenSize() !== 'MOBILE' && !isMobileLandscape()); +} + function handleHeading(table, headingCols) { const isPriceBottom = table.classList.contains('pricing-bottom'); headingCols.forEach((col, i) => { @@ -382,7 +389,7 @@ function applyStylesBasedOnScreenSize(table, originTable) { } if ((!isMerch && !table.querySelector('.col-3')) - || (isMerch && !table.querySelector('.col-2'))) return; + || (isMerch && !table.querySelector('.col-2'))) return; const filterChangeEvent = () => { table.innerHTML = originTable.innerHTML; @@ -501,10 +508,6 @@ export default function init(el) { expandSection = handleSection(sectionParams); }); - const isStickyHeader = el.classList.contains('sticky') - || (el.classList.contains('sticky-desktop-up') && defineDeviceByScreenSize() === 'DESKTOP') - || (el.classList.contains('sticky-tablet-up') && defineDeviceByScreenSize() !== 'MOBILE'); - handleHighlight(el); if (isMerch) formatMerchTable(el); @@ -525,7 +528,7 @@ export default function init(el) { const handleResize = () => { applyStylesBasedOnScreenSize(el, originTable); - if (isStickyHeader) handleScrollEffect(el); + if (isStickyHeader(el)) handleScrollEffect(el); }; handleResize(); From 6fa95584902286108cfea82572288559b3517bb0 Mon Sep 17 00:00:00 2001 From: Nicolas Peltier <1032754+npeltier@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:08:48 +0200 Subject: [PATCH 11/22] MWPW-156612 fragment merch-card variant layouts (#2810) * MWPW-156612 fragment merch-card variant layouts - move from merch-card.js code each layout code related to a given variant under variants/ class - move from global.css.js css related to a given variant under variants/.css.js - move from merch-card.css.js wc css related to a given variant under variants/.variantStyle - uses variants/variants as an index to get related class from related variant, and wc style too, - uses variants/VariantLayout, superclass of all variants, to insert css if card is used, and hold common code * MWPW-156612 changing title selector to something independent of the layout (not initialized when used at first in collections) * MWPW-156612 forking style insertion * MWPW-156612 styles fixes * MWPW-156612 review comments * MWPW-156612 fixing unit tests * MWPW-156612 address review comments * MWPW-156612 catalog & image test cov * MWPW-156612 100% coverage for variants * MWPW-156612 fixing (again) ut * MWPW-156612 fix one inline heading style * MWPW-156612 bg img height --- libs/deps/mas/merch-card.js | 2170 ++++++++--------- libs/deps/mas/plans-modal.js | 747 +----- .../mas/web-components/src/global.css.js | 750 +----- .../mas/web-components/src/merch-card.css.js | 157 +- .../mas/web-components/src/merch-card.js | 454 +--- .../src/variants/catalog.css.js | 89 + .../web-components/src/variants/catalog.js | 97 + .../src/variants/ccd-action.css.js | 40 + .../web-components/src/variants/ccd-action.js | 29 + .../web-components/src/variants/image.css.js | 38 + .../mas/web-components/src/variants/image.js | 36 + .../src/variants/inline-heading.css.js | 39 + .../src/variants/inline-heading.js | 24 + .../src/variants/mini-compare-chart.css.js | 253 ++ .../src/variants/mini-compare-chart.js | 223 ++ .../web-components/src/variants/plans.css.js | 56 + .../mas/web-components/src/variants/plans.js | 53 + .../src/variants/product.css.js | 42 + .../web-components/src/variants/product.js | 26 + .../src/variants/segment.css.js | 48 + .../web-components/src/variants/segment.js | 39 + .../src/variants/special-offer.css.js | 50 + .../src/variants/special-offer.js | 50 + .../web-components/src/variants/twp.css.js | 103 + .../mas/web-components/src/variants/twp.js | 67 + .../src/variants/variant-layout.js | 103 + .../web-components/src/variants/variants.js | 51 + .../test/merch-card.catalog.test.html | 276 +++ .../test/merch-card.catalog.test.html.js | 71 + .../merch-card.mini-compare.mobile.test.html | 634 +++++ ...erch-card.mini-compare.mobile.test.html.js | 83 + .../test/merch-card.mini-compare.test.html | 2 +- .../test/merch-card.mini-compare.test.html.js | 2 +- .../merch-card.special-offer.test.html.js | 53 + .../test/merch-card.special-offers.test.html | 202 ++ .../web-components/test/merch-card.test.html | 380 +-- .../test/merch-card.test.html.js | 39 - .../features/mas/web-components/test/utils.js | 6 + 38 files changed, 4015 insertions(+), 3567 deletions(-) create mode 100644 libs/features/mas/web-components/src/variants/catalog.css.js create mode 100644 libs/features/mas/web-components/src/variants/catalog.js create mode 100644 libs/features/mas/web-components/src/variants/ccd-action.css.js create mode 100644 libs/features/mas/web-components/src/variants/ccd-action.js create mode 100644 libs/features/mas/web-components/src/variants/image.css.js create mode 100644 libs/features/mas/web-components/src/variants/image.js create mode 100644 libs/features/mas/web-components/src/variants/inline-heading.css.js create mode 100644 libs/features/mas/web-components/src/variants/inline-heading.js create mode 100644 libs/features/mas/web-components/src/variants/mini-compare-chart.css.js create mode 100644 libs/features/mas/web-components/src/variants/mini-compare-chart.js create mode 100644 libs/features/mas/web-components/src/variants/plans.css.js create mode 100644 libs/features/mas/web-components/src/variants/plans.js create mode 100644 libs/features/mas/web-components/src/variants/product.css.js create mode 100644 libs/features/mas/web-components/src/variants/product.js create mode 100644 libs/features/mas/web-components/src/variants/segment.css.js create mode 100644 libs/features/mas/web-components/src/variants/segment.js create mode 100644 libs/features/mas/web-components/src/variants/special-offer.css.js create mode 100644 libs/features/mas/web-components/src/variants/special-offer.js create mode 100644 libs/features/mas/web-components/src/variants/twp.css.js create mode 100644 libs/features/mas/web-components/src/variants/twp.js create mode 100644 libs/features/mas/web-components/src/variants/variant-layout.js create mode 100644 libs/features/mas/web-components/src/variants/variants.js create mode 100644 libs/features/mas/web-components/test/merch-card.catalog.test.html create mode 100644 libs/features/mas/web-components/test/merch-card.catalog.test.html.js create mode 100644 libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html create mode 100644 libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html.js create mode 100644 libs/features/mas/web-components/test/merch-card.special-offer.test.html.js create mode 100644 libs/features/mas/web-components/test/merch-card.special-offers.test.html diff --git a/libs/deps/mas/merch-card.js b/libs/deps/mas/merch-card.js index 588638587a..aa77c87567 100644 --- a/libs/deps/mas/merch-card.js +++ b/libs/deps/mas/merch-card.js @@ -1,6 +1,6 @@ -import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L,html as E,css as O}from"../lit-all.min.js";var h=class extends L{static properties={size:{type:String,attribute:!0},src:{type:String,attribute:!0},alt:{type:String,attribute:!0},href:{type:String,attribute:!0}};constructor(){super(),this.size="m",this.alt=""}render(){let{href:e}=this;return e?E` +import{LitElement as we}from"../lit-all.min.js";import{LitElement as me,html as V,css as de}from"../lit-all.min.js";var n=class extends me{static properties={size:{type:String,attribute:!0},src:{type:String,attribute:!0},alt:{type:String,attribute:!0},href:{type:String,attribute:!0}};constructor(){super(),this.size="m",this.alt=""}render(){let{href:e}=this;return e?V` ${this.alt} - `:E` ${this.alt}`}static styles=O` + `:V` ${this.alt}`}static styles=de` :host { --img-width: 32px; --img-height: 32px; @@ -23,7 +23,7 @@ import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L, width: var(--img-width); height: var(--img-height); } - `};customElements.define("merch-icon",h);import{css as b,unsafeCSS as u}from"../lit-all.min.js";var g="(max-width: 767px)",x="(max-width: 1199px)",c="(min-width: 768px)",a="(min-width: 1200px)",i="(min-width: 1600px)";var C=b` + `};customElements.define("merch-icon",n);import{css as M,unsafeCSS as A}from"../lit-all.min.js";var g="(max-width: 767px)",k="(max-width: 1199px)",m="(min-width: 768px)",s="(min-width: 1200px)",l="(min-width: 1600px)";var G=M` :host { position: relative; display: flex; @@ -42,26 +42,6 @@ import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L, visibility: hidden; } - :host([variant='special-offers']) { - min-height: 439px; - } - - :host([variant='catalog']) { - min-height: 330px; - } - - :host([variant='plans']) { - min-height: 348px; - } - - :host([variant='segment']) { - min-height: 214px; - } - - :host([variant='ccd-action']:not([size])) { - width: var(--consonant-merch-card-ccd-action-width); - } - :host([aria-selected]) { outline: none; box-sizing: border-box; @@ -83,10 +63,6 @@ import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L, background-image: var(--ellipsis-icon); } - :host([variant='mini-compare-chart']) > slot:not([name='icons']) { - display: block; - } - .top-section { display: flex; justify-content: flex-start; @@ -151,19 +127,6 @@ import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L, border-radius: 0 5px 5px 0; } - .body .catalog-badge { - display: flex; - height: fit-content; - flex-direction: column; - width: fit-content; - max-width: 140px; - border-radius: 5px; - position: relative; - top: 0; - margin-left: var(--consonant-merch-spacing-xxs); - box-sizing: border-box; - } - .detail-bg-container { right: 0; padding: var(--consonant-merch-spacing-xs); @@ -267,131 +230,13 @@ import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L, margin-top: 2px; } - .twp-badge { - padding: 4px 10px 5px 10px; - } - - :host([aria-selected]) .twp-badge { - margin-inline-end: 2px; - padding-inline-end: 9px; - } - - :host([variant='twp']) { - padding: 4px 10px 5px 10px; - } - slot[name='icons'] { display: flex; gap: 8px; } - - :host([variant='twp']) ::slotted(merch-offer-select) { - display: none; - } - - :host([variant='twp']) .top-section { - flex: 0; - display: flex; - flex-direction: column; - justify-content: flex-start; - height: 100%; - gap: var(--consonant-merch-spacing-xxs); - padding: var(--consonant-merch-spacing-xs) - var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs) - var(--consonant-merch-spacing-xs); - align-items: flex-start; - } - - :host([variant='twp']) .body { - padding: 0 var(--consonant-merch-spacing-xs); - } - - :host([variant='twp']) footer { - gap: var(--consonant-merch-spacing-xxs); - flex-direction: column; - align-self: flex-start; - } - - :host([variant='special-offers'].center) { - text-align: center; - } - - /* plans */ - :host([variant='plans']) { - min-height: 348px; - } - - :host([variant='mini-compare-chart']) footer { - min-height: var(--consonant-merch-card-mini-compare-footer-height); - padding: var(--consonant-merch-spacing-xs); - } - - /* mini-compare card */ - :host([variant='mini-compare-chart']) .top-section { - padding-top: var(--consonant-merch-spacing-s); - padding-inline-start: var(--consonant-merch-spacing-s); - height: var(--consonant-merch-card-mini-compare-top-section-height); - } - - @media screen and ${u(x)} { - [class*'-merch-cards'] :host([variant='mini-compare-chart']) footer { - flex-direction: column; - align-items: stretch; - text-align: center; - } - } - - @media screen and ${u(a)} { - :host([variant='mini-compare-chart']) footer { - padding: var(--consonant-merch-spacing-xs) - var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-s); - } - } - - :host([variant='mini-compare-chart']) slot[name='footer-rows'] { - flex: 1; - display: flex; - flex-direction: column; - justify-content: end; - } - /* mini-compare card heights for the slots: heading-m, body-m, heading-m-price, price-commitment, offers, promo-text, footer */ - :host([variant='mini-compare-chart']) slot[name='heading-m'] { - min-height: var(--consonant-merch-card-mini-compare-heading-m-height); - } - :host([variant='mini-compare-chart']) slot[name='body-m'] { - min-height: var(--consonant-merch-card-mini-compare-body-m-height); - } - :host([variant='mini-compare-chart']) slot[name='heading-m-price'] { - min-height: var( - --consonant-merch-card-mini-compare-heading-m-price-height - ); - } - :host([variant='mini-compare-chart']) slot[name='price-commitment'] { - min-height: var( - --consonant-merch-card-mini-compare-price-commitment-height - ); - } - :host([variant='mini-compare-chart']) slot[name='offers'] { - min-height: var(--consonant-merch-card-mini-compare-offers-height); - } - :host([variant='mini-compare-chart']) slot[name='promo-text'] { - min-height: var(--consonant-merch-card-mini-compare-promo-text-height); - } - :host([variant='mini-compare-chart']) slot[name='callout-content'] { - min-height: var( - --consonant-merch-card-mini-compare-callout-content-height - ); - } - - :host([variant='plans']) ::slotted([slot='heading-xs']), - :host([variant='segment']) ::slotted([slot='heading-xs']) { - max-width: var(--consonant-merch-card-heading-xs-max-width, 100%); - } -`,z=()=>{let l=[b` +`,U=()=>{let r=[M` /* Tablet */ - @media screen and ${u(c)} { + @media screen and ${A(m)} { :host([size='wide']), :host([size='super-wide']) { grid-column: span 3; @@ -402,556 +247,424 @@ import{html as n,LitElement as N}from"../lit-all.min.js";import{LitElement as L, } /* Laptop */ - @media screen and ${u(a)} { + @media screen and ${A(s)} { :host([size='super-wide']) { grid-column: span 3; } - `];return l.push(b` + `];return r.push(M` /* Large desktop */ - @media screen and ${u(i)} { + @media screen and ${A(l)} { :host([size='super-wide']) { grid-column: span 4; } } - `),l};function k(){return window.matchMedia("(max-width: 767px)").matches}function S(){return window.matchMedia("(max-width: 1024px)").matches}var $=document.createElement("style");$.innerHTML=` + `),r};import{html as z}from"../lit-all.min.js";var d=class r{static styleMap={};card;insertVariantStyle(){if(!r.styleMap[this.card.variant]){r.styleMap[this.card.variant]=!0;let e=document.createElement("style");e.innerHTML=this.getGlobalCSS(),document.head.appendChild(e)}}constructor(e){this.card=e,setTimeout(()=>this.insertVariantStyle(),1)}get badge(){let e;if(!(!this.card.badgeBackgroundColor||!this.card.badgeColor||!this.card.badgeText))return this.evergreen&&(e=`border: 1px solid ${this.card.badgeBackgroundColor}; border-right: none;`),z` +
+ ${this.card.badgeText} +
+ `}get cardImage(){return z`
+ + ${this.badge} +
`}getGlobalCSS(){return""}get evergreen(){return this.card.classList.contains("intro-pricing")}get promoBottom(){return this.card.classList.contains("promo-bottom")}get headingSelector(){return'[slot="heading-xs"]'}get secureLabelFooter(){let e=this.card.secureLabel?z`${this.card.secureLabel}`:"";return z`
${e}
`}async adjustTitleWidth(){let e=this.card.getBoundingClientRect().width,t=this.card.badgeElement?.getBoundingClientRect().width||0;e===0||t===0||this.card.style.setProperty("--consonant-merch-card-heading-xs-max-width",`${Math.round(e-t-16)}px`)}postCardUpdateHook(){}connectedCallbackHook(){}disconnectedCallbackHook(){}renderLayout(){}};import{html as P,css as le}from"../lit-all.min.js";function q(){return window.matchMedia("(max-width: 767px)").matches}function j(){return window.matchMedia("(max-width: 1024px)").matches}var I="merch-offer-select:ready",K="merch-card:ready",F="merch-card:action-menu-toggle";var O="merch-storage:change",R="merch-quantity-selector:change";var W=` :root { - --consonant-merch-card-detail-font-size: 12px; - --consonant-merch-card-detail-font-weight: 500; - --consonant-merch-card-detail-letter-spacing: 0.8px; - --consonant-merch-card-background-color: #fff; + --consonant-merch-card-catalog-width: 276px; + --consonant-merch-card-catalog-icon-size: 40px; +} +.one-merch-card.catalog, +.two-merch-cards.catalog, +.three-merch-cards.catalog, +.four-merch-cards.catalog { + grid-template-columns: var(--consonant-merch-card-catalog-width); +} - --consonant-merch-card-heading-font-size: 18px; - --consonant-merch-card-heading-line-height: 22.5px; - --consonant-merch-card-heading-secondary-font-size: 14px; - --consonant-merch-card-body-font-size: 14px; - --consonant-merch-card-body-line-height: 21px; - --consonant-merch-card-promo-text-height: var(--consonant-merch-card-body-font-size); +@media screen and ${m} { + :root { + --consonant-merch-card-catalog-width: 302px; + } - /* responsive width */ - --consonant-merch-card-mobile-width: 300px; - --consonant-merch-card-tablet-wide-width: 700px; + .two-merch-cards.catalog, + .three-merch-cards.catalog, + .four-merch-cards.catalog { + grid-template-columns: repeat(2, var(--consonant-merch-card-catalog-width)); + } +} - /* spacing */ - --consonant-merch-spacing-xxxs: 4px; - --consonant-merch-spacing-xxs: 8px; - --consonant-merch-spacing-xs: 16px; - --consonant-merch-spacing-s: 24px; - --consonant-merch-spacing-m: 32px; +@media screen and ${s} { + :root { + --consonant-merch-card-catalog-width: 276px; + } - /* cta */ - --consonant-merch-card-cta-font-size: 15px; + .three-merch-cards.catalog, + .four-merch-cards.catalog { + grid-template-columns: repeat(3, var(--consonant-merch-card-catalog-width)); + } +} - /* headings */ - --consonant-merch-card-heading-xs-font-size: 18px; - --consonant-merch-card-heading-xs-line-height: 22.5px; - --consonant-merch-card-heading-s-font-size: 20px; - --consonant-merch-card-heading-s-line-height: 25px; - --consonant-merch-card-heading-m-font-size: 24px; - --consonant-merch-card-heading-m-line-height: 30px; - --consonant-merch-card-heading-l-font-size: 20px; - --consonant-merch-card-heading-l-line-height: 30px; - --consonant-merch-card-heading-xl-font-size: 36px; - --consonant-merch-card-heading-xl-line-height: 45px; +@media screen and ${l} { + .four-merch-cards.catalog { + grid-template-columns: repeat(4, var(--consonant-merch-card-catalog-width)); + } +} - /* detail */ - --consonant-merch-card-detail-m-font-size: 12px; - --consonant-merch-card-detail-m-line-height: 15px; - --consonant-merch-card-detail-m-font-weight: 700; - --consonant-merch-card-detail-m-letter-spacing: 1px; +merch-card[variant="catalog"] [slot="action-menu-content"] { + background-color: #000; + color: var(--color-white, #fff); + font-size: var(--consonant-merch-card-body-xs-font-size); + width: fit-content; + padding: var(--consonant-merch-spacing-xs); + border-radius: var(--consonant-merch-spacing-xxxs); + position: absolute; + top: 55px; + right: 15px; + line-height: var(--consonant-merch-card-body-line-height); +} - /* body */ - --consonant-merch-card-body-xxs-font-size: 12px; - --consonant-merch-card-body-xxs-line-height: 18px; - --consonant-merch-card-body-xxs-letter-spacing: 1px; - --consonant-merch-card-body-xs-font-size: 14px; - --consonant-merch-card-body-xs-line-height: 21px; - --consonant-merch-card-body-s-font-size: 16px; - --consonant-merch-card-body-s-line-height: 24px; - --consonant-merch-card-body-m-font-size: 18px; - --consonant-merch-card-body-m-line-height: 27px; - --consonant-merch-card-body-l-font-size: 20px; - --consonant-merch-card-body-l-line-height: 30px; - --consonant-merch-card-body-xl-font-size: 22px; - --consonant-merch-card-body-xl-line-height: 33px; +merch-card[variant="catalog"] [slot="action-menu-content"] ul { + padding-left: 0; + padding-bottom: var(--consonant-merch-spacing-xss); + margin-top: 0; + margin-bottom: 0; + list-style-position: inside; + list-style-type: '\u2022 '; +} +merch-card[variant="catalog"] [slot="action-menu-content"] ul li { + padding-left: 0; + line-height: var(--consonant-merch-card-body-line-height); +} - --consonant-merch-card-heading-padding: 0; - --consonant-merch-card-image-height: 180px; +merch-card[variant="catalog"] [slot="action-menu-content"] ::marker { + margin-right: 0; +} - /* colors */ - --consonant-merch-card-border-color: #eaeaea; - --color-accent: #1473E6; - --merch-color-focus-ring: #1473E6; - --merch-color-grey-80: #2c2c2c; - --merch-color-green-promo: #2D9D78; +merch-card[variant="catalog"] [slot="action-menu-content"] p { + color: var(--color-white, #fff); +} - /* merch card generic */ - --consonant-merch-card-max-width: 300px; - --transition: cmax-height 0.3s linear, opacity 0.3s linear; +merch-card[variant="catalog"] [slot="action-menu-content"] a { + color: var(--consonant-merch-card-background-color); + text-decoration: underline; +} - /* special offers */ - --consonant-merch-card-special-offers-width: 378px; +merch-card[variant="catalog"] .payment-details { + font-size: var(--consonant-merch-card-body-font-size); + font-style: italic; + font-weight: 400; + line-height: var(--consonant-merch-card-body-line-height); +}`;var u=class extends d{constructor(e){super(e)}renderLayout(){return P`
+
+ ${this.badge} +
+
+ ${this.card.actionMenuContent} + + + + ${this.promoBottom?"":P``} + + ${this.promoBottom?P``:""} +
+ ${this.secureLabelFooter}`}getGlobalCSS(){return W}toggleActionMenu=e=>{let t=e?.type==="mouseleave"?!0:void 0,o=this.card.shadowRoot.querySelector('slot[name="action-menu-content"]');o&&(t||this.card.dispatchEvent(new CustomEvent(F,{bubbles:!0,composed:!0,detail:{card:this.card.name,type:"action-menu"}})),o.classList.toggle("hidden",t))};connectedCallbackHook(){this.card.addEventListener("mouseleave",this.toggleActionMenu)}disconnectedCallbackHook(){this.card.removeEventListener("mouseleave",this.toggleActionMenu)}static variantStyle=le` + :host([variant='catalog']) { + min-height: 330px; + } - /* image */ - --consonant-merch-card-image-width: 300px; + .body .catalog-badge { + display: flex; + height: fit-content; + flex-direction: column; + width: fit-content; + max-width: 140px; + border-radius: 5px; + position: relative; + top: 0; + margin-left: var(--consonant-merch-spacing-xxs); + box-sizing: border-box; + } + `};import{html as H,css as pe}from"../lit-all.min.js";var Y=` +:root { + --consonant-merch-card-ccd-action-width: 276px; + --consonant-merch-card-ccd-action-min-height: 320px; +} - /* segment */ - --consonant-merch-card-segment-width: 378px; +.one-merch-card.ccd-action, +.two-merch-cards.ccd-action, +.three-merch-cards.ccd-action, +.four-merch-cards.ccd-action { + grid-template-columns: var(--consonant-merch-card-ccd-action-width); +} - /* inline-heading */ - --consonant-merch-card-inline-heading-width: 300px; +merch-card[variant="ccd-action"] .price-strikethrough { + font-size: 18px; +} - /* product */ - --consonant-merch-card-product-width: 300px; +@media screen and ${m} { + .two-merch-cards.ccd-action, + .three-merch-cards.ccd-action, + .four-merch-cards.ccd-action { + grid-template-columns: repeat(2, var(--consonant-merch-card-ccd-action-width)); + } +} - /* plans */ - --consonant-merch-card-plans-width: 300px; - --consonant-merch-card-plans-icon-size: 40px; - - /* catalog */ - --consonant-merch-card-catalog-width: 276px; - --consonant-merch-card-catalog-icon-size: 40px; - - /* twp */ - --consonant-merch-card-twp-width: 268px; - --consonant-merch-card-twp-mobile-width: 300px; - --consonant-merch-card-twp-mobile-height: 358px; - - /* ccd-action */ - --consonant-merch-card-ccd-action-width: 276px; - --consonant-merch-card-ccd-action-min-height: 320px; - - - /*mini compare chart */ - --consonant-merch-card-mini-compare-chart-icon-size: 32px; - --consonant-merch-card-mini-compare-mobile-cta-font-size: 15px; - --consonant-merch-card-mini-compare-mobile-cta-width: 75px; - --consonant-merch-card-mini-compare-badge-mobile-max-width: 50px; - - /* inline SVGs */ - --checkmark-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath fill='%23fff' d='M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z' class='spectrum-UIIcon--medium'/%3E%3C/svg%3E%0A"); - - --secure-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23757575' viewBox='0 0 12 15'%3E%3Cpath d='M11.5 6H11V5A5 5 0 1 0 1 5v1H.5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5ZM3 5a3 3 0 1 1 6 0v1H3Zm4 6.111V12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1.389a1.5 1.5 0 1 1 2 0Z'/%3E%3C/svg%3E"); - - --info-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36'>'); - - /* callout */ - --consonant-merch-card-callout-line-height: 21px; - --consonant-merch-card-callout-font-size: 14px; - --consonant-merch-card-callout-font-color: #2C2C2C; - --consonant-merch-card-callout-icon-size: 16px; - --consonant-merch-card-callout-icon-top: 6px; - --consonant-merch-card-callout-icon-right: 8px; - --consonant-merch-card-callout-letter-spacing: 0px; - --consonant-merch-card-callout-icon-padding: 34px; - --consonant-merch-card-callout-spacing-xxs: 8px; -} - -merch-card-collection { - display: contents; -} - -merch-card-collection > merch-card:not([style]) { - display: none; -} - -merch-card-collection > p[slot], -merch-card-collection > div[slot] p { - margin: 0; -} - -.one-merch-card, -.two-merch-cards, -.three-merch-cards, -.four-merch-cards { - display: grid; - justify-content: center; - justify-items: stretch; - gap: var(--consonant-merch-spacing-m); - padding: var(--spacing-m); -} - -merch-card.background-opacity-70 { - background-color: rgba(255 255 255 / 70%); -} - -merch-card.has-divider hr { - margin-bottom: var(--consonant-merch-spacing-xs); - height: 1px; - border: none; -} - -merch-card[variant="special-offers"] span[is="inline-price"][data-template="strikethrough"] { - font-size: var(--consonant-merch-card-body-xs-font-size); -} - -merch-card p, merch-card h3, merch-card h4 { - margin: 0; -} - -merch-card span[is=inline-price] { - display: inline-block; -} - -merch-card [slot='heading-xs'] { - color: var(--merch-color-grey-80); - font-size: var(--consonant-merch-card-heading-xs-font-size); - line-height: var(--consonant-merch-card-heading-xs-line-height); - margin: 0; - text-decoration: none; -} - -merch-card.dc-pricing [slot='heading-xs'] { - margin-bottom: var(--consonant-merch-spacing-xxs); -} - -merch-card:not([variant='inline-heading']) [slot='heading-xs'] a { - color: var(--merch-color-grey-80); -} - -merch-card [slot='starting-at'] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - font-weight: 500; -} - -merch-card [slot='heading-xs'] a:not(:hover) { - text-decoration: inherit; -} - -merch-card [slot='heading-s'] { - font-size: var(--consonant-merch-card-heading-s-font-size); - line-height: var(--consonant-merch-card-heading-s-line-height); - margin: 0; - color: var(--merch-color-grey-80); -} - -merch-card [slot='heading-m'] { - font-size: var(--consonant-merch-card-heading-m-font-size); - line-height: var(--consonant-merch-card-heading-m-line-height); - margin: 0; - color: var(--merch-color-grey-80); - font-weight: 700; -} - -merch-card [slot='heading-m-price'] { - font-size: var(--consonant-merch-card-heading-m-font-size); - line-height: var(--consonant-merch-card-heading-m-line-height); - padding: 0 var(--consonant-merch-spacing-s); - font-weight: 700; - margin: 0; - color: var(--merch-color-grey-80); -} - -merch-card [slot='offers'] { - padding: var(--consonant-merch-spacing-xxs) var(--consonant-merch-spacing-s); -} - -merch-card [slot='heading-l'] { - font-size: var(--consonant-merch-card-heading-l-font-size); - line-height: var(--consonant-merch-card-heading-l-line-height); - margin: 0; - color: var(--merch-color-grey-80); -} - -merch-card [slot='heading-xl'] { - font-size: var(--consonant-merch-card-heading-xl-font-size); - line-height: var(--consonant-merch-card-heading-xl-line-height); - margin: 0; - color: var(--merch-color-grey-80); -} - -merch-card [slot='callout-content'] { - display: flex; - flex-direction: column; - margin: var(--consonant-merch-spacing-xxxs) 0px; - gap: var(--consonant-merch-card-callout-spacing-xxs); -} - -merch-card [slot='callout-content'] > div { - display: flex; - flex-direction: column; - margin: var(--consonant-merch-spacing-xxxs) 0px; - gap: var(--consonant-merch-card-callout-spacing-xxs); - align-items: flex-start; -} - -merch-card [slot='callout-content'] > div > div { - display: flex; - background: rgba(203 203 203 / 50%); - border-radius: var(--consonant-merch-spacing-xxxs); - padding: var(--consonant-merch-spacing-xxxs) var(--consonant-merch-spacing-xxxs) var(--consonant-merch-spacing-xxxs) var(--consonant-merch-spacing-xxs); -} - -merch-card [slot='callout-content'] > div > div > div { - display: inline-block; - text-align: left; - font: normal normal normal var(--consonant-merch-card-callout-font-size)/var(--consonant-merch-card-callout-line-height) var(--body-font-family, 'Adobe Clean'); - letter-spacing: var(--consonant-merch-card-callout-letter-spacing); - color: var(--consonant-merch-card-callout-font-color); -} - -merch-card [slot='callout-content'] img { - width: var(--consonant-merch-card-callout-icon-size); - height: var(--consonant-merch-card-callout-icon-size); - margin: 2.5px 0px 0px 9px; -} - -merch-card [slot='detail-m'] { - font-size: var(--consonant-merch-card-detail-m-font-size); - letter-spacing: var(--consonant-merch-card-detail-m-letter-spacing); - font-weight: var(--consonant-merch-card-detail-m-font-weight); - text-transform: uppercase; - margin: 0; - color: var(--merch-color-grey-80); -} - -merch-card [slot="body-xxs"] { - font-size: var(--consonant-merch-card-body-xxs-font-size); - line-height: var(--consonant-merch-card-body-xxs-line-height); - font-weight: normal; - letter-spacing: var(--consonant-merch-card-body-xxs-letter-spacing); - color: var(--merch-color-grey-80); - margin: 0; -} - -merch-card [slot="body-xs"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - color: var(--merch-color-grey-80); -} - -merch-card [slot="body-m"] { - font-size: var(--consonant-merch-card-body-m-font-size); - line-height: var(--consonant-merch-card-body-m-line-height); - color: var(--merch-color-grey-80); -} - -merch-card [slot="body-l"] { - font-size: var(--consonant-merch-card-body-l-font-size); - line-height: var(--consonant-merch-card-body-l-line-height); - color: var(--merch-color-grey-80); -} - -merch-card [slot="body-xl"] { - font-size: var(--consonant-merch-card-body-xl-font-size); - line-height: var(--consonant-merch-card-body-xl-line-height); - color: var(--merch-color-grey-80); -} - -merch-card[variant="plans"] [slot="description"] { - min-height: 84px; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] { - background-color: #000; - color: var(--color-white, #fff); - font-size: var(--consonant-merch-card-body-xs-font-size); - width: fit-content; - padding: var(--consonant-merch-spacing-xs); - border-radius: var(--consonant-merch-spacing-xxxs); - position: absolute; - top: 55px; - right: 15px; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ul { - padding-left: 0; - padding-bottom: var(--consonant-merch-spacing-xss); - margin-top: 0; - margin-bottom: 0; - list-style-position: inside; - list-style-type: '\u2022 '; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ul li { - padding-left: 0; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ::marker { - margin-right: 0; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] p { - color: var(--color-white, #fff); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] a { - color: var(--consonant-merch-card-background-color); - text-decoration: underline; -} - -merch-card[variant="catalog"] .payment-details { - font-size: var(--consonant-merch-card-body-font-size); - font-style: italic; - font-weight: 400; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="ccd-action"] .price-strikethrough { - font-size: 18px; -} - -merch-card[variant="plans"] [slot="quantity-select"] { - display: flex; - justify-content: flex-start; - box-sizing: border-box; - width: 100%; - padding: var(--consonant-merch-spacing-xs); +@media screen and ${s} { + .three-merch-cards.ccd-action, + .four-merch-cards.ccd-action { + grid-template-columns: repeat(3, var(--consonant-merch-card-ccd-action-width)); + } } -merch-card[variant="twp"] div[class$='twp-badge'] { - padding: 4px 10px 5px 10px; +@media screen and ${l} { + .four-merch-cards.ccd-action { + grid-template-columns: repeat(4, var(--consonant-merch-card-ccd-action-width)); + } } - -merch-card[variant="twp"] [slot="body-xs-top"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - color: var(--merch-color-grey-80); +`;var f=class extends d{constructor(e){super(e)}getGlobalCSS(){return Y}renderLayout(){return H`
+ ${this.badge} + + + ${this.promoBottom?H``:H``} +
+ +
`}static variantStyle=pe` + :host([variant='ccd-action']:not([size])) { + width: var(--consonant-merch-card-ccd-action-width); + } + `};import{html as v}from"../lit-all.min.js";var Q=` +:root { + --consonant-merch-card-image-width: 300px; } -merch-card[variant="twp"] [slot="body-xs"] ul { - padding: 0; - margin: 0; +.one-merch-card.image, +.two-merch-cards.image, +.three-merch-cards.image, +.four-merch-cards.image { + grid-template-columns: var(--consonant-merch-card-image-width); } -merch-card[variant="twp"] [slot="body-xs"] ul li { - list-style-type: none; - padding-left: 0; +@media screen and ${m} { + .two-merch-cards.image, + .three-merch-cards.image, + .four-merch-cards.image { + grid-template-columns: repeat(2, var(--consonant-merch-card-image-width)); + } } -merch-card[variant="twp"] [slot="body-xs"] ul li::before { - content: '\xB7'; - font-size: 20px; - padding-right: 5px; - font-weight: bold; +@media screen and ${s} { + :root { + --consonant-merch-card-image-width: 378px; + } + + .three-merch-cards.image, + .four-merch-cards.image { + grid-template-columns: repeat(3, var(--consonant-merch-card-image-width)); + } } -merch-card[variant="twp"] [slot="footer"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - padding: var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs); - color: var(--merch-color-grey-80); - display: flex; - flex-flow: wrap; +@media screen and ${l} { + .four-merch-cards.image { + grid-template-columns: repeat(4, var(--consonant-merch-card-image-width)); + } +} +`;var L=class extends d{constructor(e){super(e)}getGlobalCSS(){return Q}renderLayout(){return v`${this.cardImage} +
+ + + + ${this.promoBottom?v``:v``} +
+ ${this.evergreen?v` +
+ +
+ `:v` +
+ ${this.secureLabelFooter} + `}`}};import{html as J}from"../lit-all.min.js";var Z=` +:root { + --consonant-merch-card-inline-heading-width: 300px; } -merch-card[variant='twp'] merch-quantity-select, -merch-card[variant='twp'] merch-offer-select { - display: none; +.one-merch-card.inline-heading, +.two-merch-cards.inline-heading, +.three-merch-cards.inline-heading, +.four-merch-cards.inline-heading { + grid-template-columns: var(--consonant-merch-card-inline-heading-width); } -[slot="cci-footer"] p, -[slot="cct-footer"] p, -[slot="cce-footer"] p { - margin: 0; +@media screen and ${m} { + .two-merch-cards.inline-heading, + .three-merch-cards.inline-heading, + .four-merch-cards.inline-heading { + grid-template-columns: repeat(2, var(--consonant-merch-card-inline-heading-width)); + } } -/* mini compare chart card styles */ +@media screen and ${s} { + :root { + --consonant-merch-card-inline-heading-width: 378px; + } -merch-card[variant="mini-compare-chart"] [slot="heading-m"] { - padding: 0 var(--consonant-merch-spacing-s) 0; + .three-merch-cards.inline-heading, + .four-merch-cards.inline-heading { + grid-template-columns: repeat(3, var(--consonant-merch-card-inline-heading-width)); + } } -merch-card[variant="mini-compare-chart"] [slot="body-m"] { - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); +@media screen and ${l} { + .four-merch-cards.inline-heading { + grid-template-columns: repeat(4, var(--consonant-merch-card-inline-heading-width)); + } } +`;var T=class extends d{constructor(e){super(e)}getGlobalCSS(){return Z}renderLayout(){return J` ${this.badge} +
+
+ + +
+ +
+ ${this.card.customHr?"":J`
`} ${this.secureLabelFooter}`}};import{html as _,css as ge,unsafeCSS as ee}from"../lit-all.min.js";var X=` + :root { + --consonant-merch-card-mini-compare-chart-icon-size: 32px; + --consonant-merch-card-mini-compare-mobile-cta-font-size: 15px; + --consonant-merch-card-mini-compare-mobile-cta-width: 75px; + --consonant-merch-card-mini-compare-badge-mobile-max-width: 50px; + } + + merch-card[variant="mini-compare-chart"] [slot="heading-m"] { + padding: 0 var(--consonant-merch-spacing-s) 0; + } + + merch-card[variant="mini-compare-chart"] [slot="body-m"] { + padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); + } -merch-card[variant="mini-compare-chart"] [is="inline-price"] { + merch-card[variant="mini-compare-chart"] [is="inline-price"] { display: inline-block; min-height: 30px; min-width: 1px; -} + } -merch-card[variant="mini-compare-chart"] [slot='callout-content'] { + merch-card[variant="mini-compare-chart"] [slot='callout-content'] { padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0px; -} + } -merch-card[variant="mini-compare-chart"] [slot='callout-content'] [is="inline-price"] { + merch-card[variant="mini-compare-chart"] [slot='callout-content'] [is="inline-price"] { min-height: unset; -} + } -merch-card[variant="mini-compare-chart"] [slot="price-commitment"] { + merch-card[variant="mini-compare-chart"] [slot="price-commitment"] { font-size: var(--consonant-merch-card-body-xs-font-size); padding: 0 var(--consonant-merch-spacing-s); -} + } -merch-card[variant="mini-compare-chart"] [slot="price-commitment"] a { + merch-card[variant="mini-compare-chart"] [slot="price-commitment"] a { display: inline-block; height: 27px; -} + } -merch-card[variant="mini-compare-chart"] [slot="offers"] { + merch-card[variant="mini-compare-chart"] [slot="offers"] { font-size: var(--consonant-merch-card-body-xs-font-size); -} - -merch-card [slot="promo-text"] { - color: var(--merch-color-green-promo); - font-size: var(--consonant-merch-card-promo-text-height); - font-weight: 700; - line-height: var(--consonant-merch-card-promo-text-height); - margin: 0; - min-height: var(--consonant-merch-card-promo-text-height); - padding: 0; -} - + } -merch-card[variant="mini-compare-chart"] [slot="body-xxs"] { + merch-card[variant="mini-compare-chart"] [slot="body-xxs"] { font-size: var(--consonant-merch-card-body-xs-font-size); padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; -} + } -merch-card[variant="mini-compare-chart"] [slot="promo-text"] { + merch-card[variant="mini-compare-chart"] [slot="promo-text"] { font-size: var(--consonant-merch-card-body-m-font-size); padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; -} + } -merch-card[variant="mini-compare-chart"] [slot="promo-text"] a { + merch-card[variant="mini-compare-chart"] [slot="promo-text"] a { text-decoration: underline; -} + } -merch-card[variant="mini-compare-chart"] .footer-row-icon { + merch-card[variant="mini-compare-chart"] .footer-row-icon { display: flex; place-items: center; -} + } -merch-card[variant="mini-compare-chart"] .footer-row-icon img { + merch-card[variant="mini-compare-chart"] .footer-row-icon img { max-width: initial; width: var(--consonant-merch-card-mini-compare-chart-icon-size); height: var(--consonant-merch-card-mini-compare-chart-icon-size); -} + } -merch-card[variant="mini-compare-chart"] .footer-row-cell { + merch-card[variant="mini-compare-chart"] .footer-row-cell { border-top: 1px solid var(--consonant-merch-card-border-color); display: flex; gap: var(--consonant-merch-spacing-xs); justify-content: start; place-items: center; padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); -} + } -merch-card[variant="mini-compare-chart"] .footer-row-cell-description { + merch-card[variant="mini-compare-chart"] .footer-row-cell-description { font-size: var(--consonant-merch-card-body-s-font-size); line-height: var(--consonant-merch-card-body-s-line-height); -} + } -merch-card[variant="mini-compare-chart"] .footer-row-cell-description p { + merch-card[variant="mini-compare-chart"] .footer-row-cell-description p { color: var(--merch-color-grey-80); vertical-align: bottom; -} + } -merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { + merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { color: var(--color-accent); text-decoration: solid; + } + +.one-merch-card.mini-compare-chart { + grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); + gap: var(--consonant-merch-spacing-xs); +} + +.two-merch-cards.mini-compare-chart, +.three-merch-cards.mini-compare-chart, +.four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-width)); + gap: var(--consonant-merch-spacing-xs); } /* mini compare mobile */ @media screen and ${g} { + :root { + --consonant-merch-card-mini-compare-chart-width: 302px; + --consonant-merch-card-mini-compare-chart-wide-width: 302px; + } + + .two-merch-cards.mini-compare-chart, + .three-merch-cards.mini-compare-chart, + .four-merch-cards.mini-compare-chart { + grid-template-columns: var(--consonant-merch-card-mini-compare-chart-width); + gap: var(--consonant-merch-spacing-xs); + } + merch-card[variant="mini-compare-chart"] [slot='heading-m'] { font-size: var(--consonant-merch-card-body-s-font-size); line-height: var(--consonant-merch-card-body-s-line-height); @@ -977,8 +690,12 @@ merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { } } -/* mini compare tablet */ -@media screen and ${x} { +@media screen and ${k} { + .three-merch-cards.mini-compare-chart merch-card [slot="footer"] a, + .four-merch-cards.mini-compare-chart merch-card [slot="footer"] a { + flex: 1; + } + merch-card[variant="mini-compare-chart"] [slot='heading-m'] { font-size: var(--consonant-merch-card-body-s-font-size); line-height: var(--consonant-merch-card-body-s-line-height); @@ -1004,73 +721,183 @@ merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { line-height: var(--consonant-merch-card-body-xs-line-height); } } +@media screen and ${m} { + :root { + --consonant-merch-card-mini-compare-chart-width: 302px; + --consonant-merch-card-mini-compare-chart-wide-width: 302px; + } -div[slot="footer"] { - display: contents; + .two-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); + gap: var(--consonant-merch-spacing-m); + } + + .three-merch-cards.mini-compare-chart, + .four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); + } } -[slot="footer"] a { - word-wrap: break-word; - text-align: center; +/* desktop */ +@media screen and ${s} { + :root { + --consonant-merch-card-mini-compare-chart-width: 378px; + --consonant-merch-card-mini-compare-chart-wide-width: 484px; + } + .one-merch-card.mini-compare-chart { + grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); + } + + .two-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-wide-width)); + gap: var(--consonant-merch-spacing-m); + } + + .three-merch-cards.mini-compare-chart, + .four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(3, var(--consonant-merch-card-mini-compare-chart-width)); + gap: var(--consonant-merch-spacing-m); + } } -[slot="footer"] a:not([class]) { - font-weight: 700; - font-size: var(--consonant-merch-card-cta-font-size); +@media screen and ${l} { + .four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(4, var(--consonant-merch-card-mini-compare-chart-width)); + } } -div[slot='bg-image'] img { - position: relative; - width: 100%; - min-height: var(--consonant-merch-card-image-height); - max-height: var(--consonant-merch-card-image-height); - object-fit: cover; - border-top-left-radius: 16px; - border-top-right-radius: 16px; +merch-card .footer-row-cell:nth-child(1) { + min-height: var(--consonant-merch-card-footer-row-1-min-height); } -/* Mobile */ -@media screen and ${g} { - :root { - --consonant-merch-card-mini-compare-chart-width: 302px; - --consonant-merch-card-segment-width: 276px; - --consonant-merch-card-mini-compare-chart-wide-width: 302px; - --consonant-merch-card-special-offers-width: 302px; - --consonant-merch-card-twp-width: 300px; - } +merch-card .footer-row-cell:nth-child(2) { + min-height: var(--consonant-merch-card-footer-row-2-min-height); +} + +merch-card .footer-row-cell:nth-child(3) { + min-height: var(--consonant-merch-card-footer-row-3-min-height); } +merch-card .footer-row-cell:nth-child(4) { + min-height: var(--consonant-merch-card-footer-row-4-min-height); +} -/* Tablet */ -@media screen and ${c} { - :root { - --consonant-merch-card-catalog-width: 302px; - --consonant-merch-card-plans-width: 302px; - --consonant-merch-card-segment-width: 276px; - --consonant-merch-card-mini-compare-chart-width: 302px; - --consonant-merch-card-mini-compare-chart-wide-width: 302px; - --consonant-merch-card-special-offers-width: 302px; - --consonant-merch-card-twp-width: 268px; - } +merch-card .footer-row-cell:nth-child(5) { + min-height: var(--consonant-merch-card-footer-row-5-min-height); } -/* desktop */ -@media screen and ${a} { - :root { - --consonant-merch-card-catalog-width: 276px; - --consonant-merch-card-plans-width: 276px; - --consonant-merch-card-segment-width: 302px; - --consonant-merch-card-inline-heading-width: 378px; - --consonant-merch-card-product-width: 378px; - --consonant-merch-card-image-width: 378px; - --consonant-merch-card-mini-compare-chart-width: 378px; - --consonant-merch-card-mini-compare-chart-wide-width: 484px; - --consonant-merch-card-twp-width: 268px; +merch-card .footer-row-cell:nth-child(6) { + min-height: var(--consonant-merch-card-footer-row-6-min-height); +} + +merch-card .footer-row-cell:nth-child(7) { + min-height: var(--consonant-merch-card-footer-row-7-min-height); +} + +merch-card .footer-row-cell:nth-child(8) { + min-height: var(--consonant-merch-card-footer-row-8-min-height); +} +`;var xe=32,b=class extends d{constructor(e){super(e)}#e;getRowMinHeightPropertyName=e=>`--consonant-merch-card-footer-row-${e}-min-height`;getContainer(){return this.#e=this.#e??this.card.closest('[class*="-merch-cards"]')??this.card.parentElement,this.#e}getGlobalCSS(){return X}getMiniCompareFooter=()=>{let e=this.card.secureLabel?_` + ${this.card.secureLabel}`:_``;return _`
${e}
`};updateMiniCompareElementMinHeight(e,t){let o=`--consonant-merch-card-mini-compare-${t}-height`,h=Math.max(0,parseInt(window.getComputedStyle(e).height)||0),p=parseInt(this.getContainer().style.getPropertyValue(o))||0;h>p&&this.getContainer().style.setProperty(o,`${h}px`)}adjustMiniCompareBodySlots(){if(this.card.getBoundingClientRect().width===0)return;this.updateMiniCompareElementMinHeight(this.card.shadowRoot.querySelector(".top-section"),"top-section"),["heading-m","body-m","heading-m-price","price-commitment","offers","promo-text","callout-content","secure-transaction-label"].forEach(o=>this.updateMiniCompareElementMinHeight(this.card.shadowRoot.querySelector(`slot[name="${o}"]`),o)),this.updateMiniCompareElementMinHeight(this.card.shadowRoot.querySelector("footer"),"footer");let t=this.card.shadowRoot.querySelector(".mini-compare-chart-badge");t&&t.textContent!==""&&this.getContainer().style.setProperty("--consonant-merch-card-mini-compare-top-section-mobile-height","32px")}adjustMiniCompareFooterRows(){if(this.card.getBoundingClientRect().width===0)return;[...this.card.querySelector('[slot="footer-rows"]')?.children].forEach((t,o)=>{let h=Math.max(xe,parseInt(window.getComputedStyle(t).height)||0),p=parseInt(this.getContainer().style.getPropertyValue(this.getRowMinHeightPropertyName(o+1)))||0;h>p&&this.getContainer().style.setProperty(this.getRowMinHeightPropertyName(o+1),`${h}px`)})}removeEmptyRows(){this.card.querySelectorAll(".footer-row-cell").forEach(t=>{let o=t.querySelector(".footer-row-cell-description");o&&!o.textContent.trim()&&t.remove()})}renderLayout(){return _`
+ ${this.badge} +
+ + + + + + + + + ${this.getMiniCompareFooter()} + `}postCardUpdateHook(){q()?this.removeEmptyRows():(this.adjustMiniCompareBodySlots(),this.adjustMiniCompareFooterRows())}static variantStyle=ge` + :host([variant='mini-compare-chart']) > slot:not([name='icons']) { + display: block; + } + :host([variant='mini-compare-chart']) footer { + min-height: var(--consonant-merch-card-mini-compare-footer-height); + padding: var(--consonant-merch-spacing-xs); + } + + /* mini-compare card */ + :host([variant='mini-compare-chart']) .top-section { + padding-top: var(--consonant-merch-spacing-s); + padding-inline-start: var(--consonant-merch-spacing-s); + height: var(--consonant-merch-card-mini-compare-top-section-height); + } + + @media screen and ${ee(k)} { + [class*'-merch-cards'] :host([variant='mini-compare-chart']) footer { + flex-direction: column; + align-items: stretch; + text-align: center; + } + } + + @media screen and ${ee(s)} { + :host([variant='mini-compare-chart']) footer { + padding: var(--consonant-merch-spacing-xs) + var(--consonant-merch-spacing-s) + var(--consonant-merch-spacing-s) + var(--consonant-merch-spacing-s); + } + } + + :host([variant='mini-compare-chart']) slot[name='footer-rows'] { + flex: 1; + display: flex; + flex-direction: column; + justify-content: end; + } + /* mini-compare card heights for the slots: heading-m, body-m, heading-m-price, price-commitment, offers, promo-text, footer */ + :host([variant='mini-compare-chart']) slot[name='heading-m'] { + min-height: var(--consonant-merch-card-mini-compare-heading-m-height); + } + :host([variant='mini-compare-chart']) slot[name='body-m'] { + min-height: var(--consonant-merch-card-mini-compare-body-m-height); + } + :host([variant='mini-compare-chart']) slot[name='heading-m-price'] { + min-height: var( + --consonant-merch-card-mini-compare-heading-m-price-height + ); + } + :host([variant='mini-compare-chart']) slot[name='price-commitment'] { + min-height: var( + --consonant-merch-card-mini-compare-price-commitment-height + ); + } + :host([variant='mini-compare-chart']) slot[name='offers'] { + min-height: var(--consonant-merch-card-mini-compare-offers-height); + } + :host([variant='mini-compare-chart']) slot[name='promo-text'] { + min-height: var(--consonant-merch-card-mini-compare-promo-text-height); + } + :host([variant='mini-compare-chart']) slot[name='callout-content'] { + min-height: var( + --consonant-merch-card-mini-compare-callout-content-height + ); } + `};import{html as $,css as ue}from"../lit-all.min.js";var te=` +:root { + --consonant-merch-card-plans-width: 300px; + --consonant-merch-card-plans-icon-size: 40px; +} + +merch-card[variant="plans"] [slot="description"] { + min-height: 84px; +} + +merch-card[variant="plans"] [slot="quantity-select"] { + display: flex; + justify-content: flex-start; + box-sizing: border-box; + width: 100%; + padding: var(--consonant-merch-spacing-xs); } -/* supported cards */ -/* grid style for plans */ .one-merch-card.plans, .two-merch-cards.plans, .three-merch-cards.plans, @@ -1079,399 +906,716 @@ div[slot='bg-image'] img { } /* Tablet */ -@media screen and ${c} { - .two-merch-cards.plans, - .three-merch-cards.plans, - .four-merch-cards.plans { - grid-template-columns: repeat(2, var(--consonant-merch-card-plans-width)); +@media screen and ${m} { + :root { + --consonant-merch-card-plans-width: 302px; + } + .two-merch-cards.plans, + .three-merch-cards.plans, + .four-merch-cards.plans { + grid-template-columns: repeat(2, var(--consonant-merch-card-plans-width)); + } +} + +/* desktop */ +@media screen and ${s} { + :root { + --consonant-merch-card-plans-width: 276px; + } + .three-merch-cards.plans, + .four-merch-cards.plans { + grid-template-columns: repeat(3, var(--consonant-merch-card-plans-width)); + } +} + +/* Large desktop */ + @media screen and ${l} { + .four-merch-cards.plans { + grid-template-columns: repeat(4, var(--consonant-merch-card-plans-width)); + } +} +`;var y=class extends d{constructor(e){super(e)}getGlobalCSS(){return te}postCardUpdateHook(){this.adjustTitleWidth()}get stockCheckbox(){return this.card.checkboxLabel?$``:""}renderLayout(){return $` ${this.badge} +
+ + + + + ${this.promoBottom?"":$` `} + + ${this.promoBottom?$` `:""} + ${this.stockCheckbox} +
+ + ${this.secureLabelFooter}`}static variantStyle=ue` + :host([variant='plans']) { + min-height: 348px; + } + + :host([variant='plans']) ::slotted([slot='heading-xs']) { + max-width: var(--consonant-merch-card-heading-xs-max-width, 100%); + } + `};import{html as N}from"../lit-all.min.js";var re=` +:root { + --consonant-merch-card-product-width: 300px; +} + +/* grid style for product */ +.one-merch-card.product, +.two-merch-cards.product, +.three-merch-cards.product, +.four-merch-cards.product { + grid-template-columns: var(--consonant-merch-card-product-width); +} + +/* Tablet */ +@media screen and ${m} { + .two-merch-cards.product, + .three-merch-cards.product, + .four-merch-cards.product { + grid-template-columns: repeat(2, var(--consonant-merch-card-product-width)); } } /* desktop */ -@media screen and ${a} { - .three-merch-cards.plans, - .four-merch-cards.plans { - grid-template-columns: repeat(3, var(--consonant-merch-card-plans-width)); - } +@media screen and ${s} { + :root { + --consonant-merch-card-product-width: 378px; + } + + .three-merch-cards.product, + .four-merch-cards.product { + grid-template-columns: repeat(3, var(--consonant-merch-card-product-width)); + } } /* Large desktop */ - @media screen and ${i} { - .four-merch-cards.plans { - grid-template-columns: repeat(4, var(--consonant-merch-card-plans-width)); - } +@media screen and ${l} { + .four-merch-cards.product { + grid-template-columns: repeat(4, var(--consonant-merch-card-product-width)); + } +} +`;var w=class extends d{constructor(e){super(e)}getGlobalCSS(){return re}renderLayout(){return N` ${this.badge} +
+ + + + ${this.promoBottom?"":N``} + + ${this.promoBottom?N``:""} +
+ ${this.secureLabelFooter}`}};import{html as B,css as fe}from"../lit-all.min.js";var oe=` +:root { + --consonant-merch-card-segment-width: 378px; } +/* grid style for segment */ +.one-merch-card.segment, +.two-merch-cards.segment, +.three-merch-cards.segment, +.four-merch-cards.segment { + grid-template-columns: minmax(276px, var(--consonant-merch-card-segment-width)); +} -/* grid style for catalog */ -.one-merch-card.catalog, -.two-merch-cards.catalog, -.three-merch-cards.catalog, -.four-merch-cards.catalog { - grid-template-columns: var(--consonant-merch-card-catalog-width); +/* Mobile */ +@media screen and ${g} { + :root { + --consonant-merch-card-segment-width: 276px; + } } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.catalog, - .three-merch-cards.catalog, - .four-merch-cards.catalog { - grid-template-columns: repeat(2, var(--consonant-merch-card-catalog-width)); - } +@media screen and ${m} { + :root { + --consonant-merch-card-segment-width: 276px; + } + + .two-merch-cards.segment, + .three-merch-cards.segment, + .four-merch-cards.segment { + grid-template-columns: repeat(2, minmax(276px, var(--consonant-merch-card-segment-width))); + } } /* desktop */ -@media screen and ${a} { - .three-merch-cards.catalog, - .four-merch-cards.catalog { - grid-template-columns: repeat(3, var(--consonant-merch-card-catalog-width)); - } -} +@media screen and ${s} { + :root { + --consonant-merch-card-segment-width: 302px; + } + + .three-merch-cards.segment { + grid-template-columns: repeat(3, minmax(276px, var(--consonant-merch-card-segment-width))); + } -/* Large desktop */ - @media screen and ${i} { - .four-merch-cards.catalog { - grid-template-columns: repeat(4, var(--consonant-merch-card-catalog-width)); + .four-merch-cards.segment { + grid-template-columns: repeat(4, minmax(276px, var(--consonant-merch-card-segment-width))); + } +} +`;var E=class extends d{constructor(e){super(e)}getGlobalCSS(){return oe}postCardUpdateHook(){this.adjustTitleWidth()}renderLayout(){return B` ${this.badge} +
+ + + ${this.promoBottom?"":B``} + + ${this.promoBottom?B``:""} +
+
+ ${this.secureLabelFooter}`}static variantStyle=fe` + :host([variant='segment']) { + min-height: 214px; + } + :host([variant='segment']) ::slotted([slot='heading-xs']) { + max-width: var(--consonant-merch-card-heading-xs-max-width, 100%); } + `};import{html as D,css as ve}from"../lit-all.min.js";var ne=` +:root { + --consonant-merch-card-special-offers-width: 378px; } +merch-card[variant="special-offers"] span[is="inline-price"][data-template="strikethrough"] { + font-size: var(--consonant-merch-card-body-xs-font-size); +} /* grid style for special-offers */ .one-merch-card.special-offers, .two-merch-cards.special-offers, .three-merch-cards.special-offers, .four-merch-cards.special-offers { - grid-template-columns: minmax(300px, var(--consonant-merch-card-special-offers-width)); + grid-template-columns: minmax(300px, var(--consonant-merch-card-special-offers-width)); } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.special-offers, - .three-merch-cards.special-offers, - .four-merch-cards.special-offers { - grid-template-columns: repeat(2, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } +@media screen and ${g} { + :root { + --consonant-merch-card-special-offers-width: 302px; + } +} + +@media screen and ${m} { + :root { + --consonant-merch-card-special-offers-width: 302px; + } + + .two-merch-cards.special-offers, + .three-merch-cards.special-offers, + .four-merch-cards.special-offers { + grid-template-columns: repeat(2, minmax(300px, var(--consonant-merch-card-special-offers-width))); + } } /* desktop */ -@media screen and ${a} { - .three-merch-cards.special-offers, - .four-merch-cards.special-offers { - grid-template-columns: repeat(3, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } +@media screen and ${s} { + .three-merch-cards.special-offers, + .four-merch-cards.special-offers { + grid-template-columns: repeat(3, minmax(300px, var(--consonant-merch-card-special-offers-width))); + } } -@media screen and ${i} { - .four-merch-cards.special-offers { - grid-template-columns: repeat(4, minmax(300px, var(--consonant-merch-card-special-offers-width))); +@media screen and ${l} { + .four-merch-cards.special-offers { + grid-template-columns: repeat(4, minmax(300px, var(--consonant-merch-card-special-offers-width))); + } +} +`;var S=class extends d{constructor(e){super(e)}getGlobalCSS(){return ne}get headingSelector(){return'[slot="detail-m"]'}renderLayout(){return D`${this.cardImage} +
+ + + +
+ ${this.evergreen?D` +
+ +
+ `:D` +
+ ${this.secureLabelFooter} + `} + `}static variantStyle=ve` + :host([variant='special-offers']) { + min-height: 439px; } + + :host([variant='special-offers'].center) { + text-align: center; + } + `};import{html as be,css as ye}from"../lit-all.min.js";var ae=` +:root { + --consonant-merch-card-twp-width: 268px; + --consonant-merch-card-twp-mobile-width: 300px; + --consonant-merch-card-twp-mobile-height: 358px; +} + +merch-card[variant="twp"] div[class$='twp-badge'] { + padding: 4px 10px 5px 10px; } +merch-card[variant="twp"] [slot="body-xs-top"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + color: var(--merch-color-grey-80); +} -/* grid style for image */ -.one-merch-card.image, -.two-merch-cards.image, -.three-merch-cards.image, -.four-merch-cards.image { - grid-template-columns: var(--consonant-merch-card-image-width); +merch-card[variant="twp"] [slot="body-xs"] ul { + padding: 0; + margin: 0; } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.image, - .three-merch-cards.image, - .four-merch-cards.image { - grid-template-columns: repeat(2, var(--consonant-merch-card-image-width)); - } +merch-card[variant="twp"] [slot="body-xs"] ul li { + list-style-type: none; + padding-left: 0; } -/* desktop */ -@media screen and ${a} { - .three-merch-cards.image, - .four-merch-cards.image { - grid-template-columns: repeat(3, var(--consonant-merch-card-image-width)); - } +merch-card[variant="twp"] [slot="body-xs"] ul li::before { + content: '\xB7'; + font-size: 20px; + padding-right: 5px; + font-weight: bold; } -/* Large desktop */ - @media screen and ${i} { - .four-merch-cards.image { - grid-template-columns: repeat(4, var(--consonant-merch-card-image-width)); - } +merch-card[variant="twp"] [slot="footer"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + padding: var(--consonant-merch-spacing-s); + var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs); + color: var(--merch-color-grey-80); + display: flex; + flex-flow: wrap; } +merch-card[variant='twp'] merch-quantity-select, +merch-card[variant='twp'] merch-offer-select { + display: none; +} -/* grid style for segment */ -.one-merch-card.segment, -.two-merch-cards.segment, -.three-merch-cards.segment, -.four-merch-cards.segment { - grid-template-columns: minmax(276px, var(--consonant-merch-card-segment-width)); +.one-merch-card.twp, +.two-merch-cards.twp, +.three-merch-cards.twp { + grid-template-columns: var(--consonant-merch-card-image-width); } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.segment, - .three-merch-cards.segment, - .four-merch-cards.segment { - grid-template-columns: repeat(2, minmax(276px, var(--consonant-merch-card-segment-width))); +@media screen and ${g} { + :root { + --consonant-merch-card-twp-width: 300px; + } + .one-merch-card.twp, + .two-merch-cards.twp, + .three-merch-cards.twp { + grid-template-columns: repeat(1, var(--consonant-merch-card-twp-mobile-width)); + } +} + +@media screen and ${m} { + :root { + --consonant-merch-card-twp-width: 268px; + } + .one-merch-card.twp, + .two-merch-cards.twp { + grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); + } + .three-merch-cards.twp { + grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); + } +} + +@media screen and ${s} { + :root { + --consonant-merch-card-twp-width: 268px; + } + .one-merch-card.twp + .two-merch-cards.twp { + grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); + } + .three-merch-cards.twp { + grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); + } +} + +@media screen and ${l} { + .one-merch-card.twp + .two-merch-cards.twp { + grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); } + .three-merch-cards.twp { + grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); + } } +`;var C=class extends d{constructor(e){super(e)}getGlobalCSS(){return ae}renderLayout(){return be`${this.badge} +
+ + + +
+
+ +
+
`}static variantStyle=ye` + :host([variant='twp']) { + padding: 4px 10px 5px 10px; + } + .twp-badge { + padding: 4px 10px 5px 10px; + } -/* desktop */ -@media screen and ${a} { - .three-merch-cards.segment { - grid-template-columns: repeat(3, minmax(276px, var(--consonant-merch-card-segment-width))); + :host([variant='twp']) ::slotted(merch-offer-select) { + display: none; + } + + :host([variant='twp']) .top-section { + flex: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + height: 100%; + gap: var(--consonant-merch-spacing-xxs); + padding: var(--consonant-merch-spacing-xs) + var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs) + var(--consonant-merch-spacing-xs); + align-items: flex-start; + } + + :host([variant='twp']) .body { + padding: 0 var(--consonant-merch-spacing-xs); + } + + :host([aria-selected]) .twp-badge { + margin-inline-end: 2px; + padding-inline-end: 9px; } - .four-merch-cards.segment { - grid-template-columns: repeat(4, minmax(276px, var(--consonant-merch-card-segment-width))); + :host([variant='twp']) footer { + gap: var(--consonant-merch-spacing-xxs); + flex-direction: column; + align-self: flex-start; } + `};var ce=r=>{switch(r.variant){case"catalog":return new u(r);case"ccd-action":return new f(r);case"image":return new L(r);case"inline-heading":return new T(r);case"mini-compare-chart":return new b(r);case"plans":return new y(r);case"product":return new w(r);case"segment":return new E(r);case"special-offers":return new S(r);case"twp":return new C(r);default:return new w(r)}},ie=()=>{let r=[];return r.push(u.variantStyle),r.push(f.variantStyle),r.push(b.variantStyle),r.push(y.variantStyle),r.push(E.variantStyle),r.push(S.variantStyle),r.push(C.variantStyle),r};var se=document.createElement("style");se.innerHTML=` +:root { + --consonant-merch-card-detail-font-size: 12px; + --consonant-merch-card-detail-font-weight: 500; + --consonant-merch-card-detail-letter-spacing: 0.8px; + --consonant-merch-card-background-color: #fff; + + --consonant-merch-card-heading-font-size: 18px; + --consonant-merch-card-heading-line-height: 22.5px; + --consonant-merch-card-heading-secondary-font-size: 14px; + --consonant-merch-card-body-font-size: 14px; + --consonant-merch-card-body-line-height: 21px; + --consonant-merch-card-promo-text-height: var(--consonant-merch-card-body-font-size); + + /* responsive width */ + --consonant-merch-card-mobile-width: 300px; + --consonant-merch-card-tablet-wide-width: 700px; + + /* spacing */ + --consonant-merch-spacing-xxxs: 4px; + --consonant-merch-spacing-xxs: 8px; + --consonant-merch-spacing-xs: 16px; + --consonant-merch-spacing-s: 24px; + --consonant-merch-spacing-m: 32px; + + /* cta */ + --consonant-merch-card-cta-font-size: 15px; + + /* headings */ + --consonant-merch-card-heading-xs-font-size: 18px; + --consonant-merch-card-heading-xs-line-height: 22.5px; + --consonant-merch-card-heading-s-font-size: 20px; + --consonant-merch-card-heading-s-line-height: 25px; + --consonant-merch-card-heading-m-font-size: 24px; + --consonant-merch-card-heading-m-line-height: 30px; + --consonant-merch-card-heading-l-font-size: 20px; + --consonant-merch-card-heading-l-line-height: 30px; + --consonant-merch-card-heading-xl-font-size: 36px; + --consonant-merch-card-heading-xl-line-height: 45px; + + /* detail */ + --consonant-merch-card-detail-m-font-size: 12px; + --consonant-merch-card-detail-m-line-height: 15px; + --consonant-merch-card-detail-m-font-weight: 700; + --consonant-merch-card-detail-m-letter-spacing: 1px; + + /* body */ + --consonant-merch-card-body-xxs-font-size: 12px; + --consonant-merch-card-body-xxs-line-height: 18px; + --consonant-merch-card-body-xxs-letter-spacing: 1px; + --consonant-merch-card-body-xs-font-size: 14px; + --consonant-merch-card-body-xs-line-height: 21px; + --consonant-merch-card-body-s-font-size: 16px; + --consonant-merch-card-body-s-line-height: 24px; + --consonant-merch-card-body-m-font-size: 18px; + --consonant-merch-card-body-m-line-height: 27px; + --consonant-merch-card-body-l-font-size: 20px; + --consonant-merch-card-body-l-line-height: 30px; + --consonant-merch-card-body-xl-font-size: 22px; + --consonant-merch-card-body-xl-line-height: 33px; + + + --consonant-merch-card-heading-padding: 0; + + /* colors */ + --consonant-merch-card-border-color: #eaeaea; + --color-accent: #1473E6; + --merch-color-focus-ring: #1473E6; + --merch-color-grey-80: #2c2c2c; + --merch-color-green-promo: #2D9D78; + + /* merch card generic */ + --consonant-merch-card-max-width: 300px; + --transition: cmax-height 0.3s linear, opacity 0.3s linear; + + /* background image */ + --consonant-merch-card-bg-img-height: 180px; + + /* inline SVGs */ + --checkmark-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath fill='%23fff' d='M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z' class='spectrum-UIIcon--medium'/%3E%3C/svg%3E%0A"); + + --secure-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23757575' viewBox='0 0 12 15'%3E%3Cpath d='M11.5 6H11V5A5 5 0 1 0 1 5v1H.5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5ZM3 5a3 3 0 1 1 6 0v1H3Zm4 6.111V12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1.389a1.5 1.5 0 1 1 2 0Z'/%3E%3C/svg%3E"); + + --info-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36'>'); + + /* callout */ + --consonant-merch-card-callout-line-height: 21px; + --consonant-merch-card-callout-font-size: 14px; + --consonant-merch-card-callout-font-color: #2C2C2C; + --consonant-merch-card-callout-icon-size: 16px; + --consonant-merch-card-callout-icon-top: 6px; + --consonant-merch-card-callout-icon-right: 8px; + --consonant-merch-card-callout-letter-spacing: 0px; + --consonant-merch-card-callout-icon-padding: 34px; + --consonant-merch-card-callout-spacing-xxs: 8px; +} + +merch-card-collection { + display: contents; +} + +merch-card-collection > merch-card:not([style]) { + display: none; } - -/* grid style for product */ -.one-merch-card.product, -.two-merch-cards.product, -.three-merch-cards.product, -.four-merch-cards.product { - grid-template-columns: var(--consonant-merch-card-product-width); +merch-card-collection > p[slot], +merch-card-collection > div[slot] p { + margin: 0; } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.product, - .three-merch-cards.product, - .four-merch-cards.product { - grid-template-columns: repeat(2, var(--consonant-merch-card-product-width)); - } +.one-merch-card, +.two-merch-cards, +.three-merch-cards, +.four-merch-cards { + display: grid; + justify-content: center; + justify-items: stretch; + gap: var(--consonant-merch-spacing-m); + padding: var(--spacing-m); } -/* desktop */ -@media screen and ${a} { - .three-merch-cards.product, - .four-merch-cards.product { - grid-template-columns: repeat(3, var(--consonant-merch-card-product-width)); - } +merch-card.background-opacity-70 { + background-color: rgba(255 255 255 / 70%); } -/* Large desktop */ - @media screen and ${i} { - .four-merch-cards.product { - grid-template-columns: repeat(4, var(--consonant-merch-card-product-width)); - } +merch-card.has-divider hr { + margin-bottom: var(--consonant-merch-spacing-xs); + height: 1px; + border: none; } -/* grid style for twp */ -.one-merch-card.twp, -.two-merch-cards.twp, -.three-merch-cards.twp { - grid-template-columns: var(--consonant-merch-card-image-width); +merch-card p, merch-card h3, merch-card h4 { + margin: 0; } -/* Tablet */ -@media screen and ${c} { - .one-merch-card.twp, - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } +merch-card span[is=inline-price] { + display: inline-block; } -/* desktop */ -@media screen and ${a} { - .one-merch-card.twp - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } +merch-card [slot='heading-xs'] { + color: var(--merch-color-grey-80); + font-size: var(--consonant-merch-card-heading-xs-font-size); + line-height: var(--consonant-merch-card-heading-xs-line-height); + margin: 0; + text-decoration: none; } -/* Large desktop */ - @media screen and ${i} { - .one-merch-card.twp - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } +merch-card.dc-pricing [slot='heading-xs'] { + margin-bottom: var(--consonant-merch-spacing-xxs); } -/* Mobile */ -@media screen and ${g} { - .one-merch-card.twp, - .two-merch-cards.twp, - .three-merch-cards.twp { - grid-template-columns: repeat(1, var(--consonant-merch-card-twp-mobile-width)); - } +merch-card:not([variant='inline-heading']) [slot='heading-xs'] a { + color: var(--merch-color-grey-80); } -/* grid style for inline-heading */ -.one-merch-card.inline-heading, -.two-merch-cards.inline-heading, -.three-merch-cards.inline-heading, -.four-merch-cards.inline-heading { - grid-template-columns: var(--consonant-merch-card-inline-heading-width); +merch-card [slot='starting-at'] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + font-weight: 500; } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.inline-heading, - .three-merch-cards.inline-heading, - .four-merch-cards.inline-heading { - grid-template-columns: repeat(2, var(--consonant-merch-card-inline-heading-width)); - } +merch-card [slot='heading-xs'] a:not(:hover) { + text-decoration: inherit; } -/* desktop */ -@media screen and ${a} { - .three-merch-cards.inline-heading, - .four-merch-cards.inline-heading { - grid-template-columns: repeat(3, var(--consonant-merch-card-inline-heading-width)); - } +merch-card [slot='heading-s'] { + font-size: var(--consonant-merch-card-heading-s-font-size); + line-height: var(--consonant-merch-card-heading-s-line-height); + margin: 0; + color: var(--merch-color-grey-80); } -/* Large desktop */ - @media screen and ${i} { - .four-merch-cards.inline-heading { - grid-template-columns: repeat(4, var(--consonant-merch-card-inline-heading-width)); - } +merch-card [slot='heading-m'] { + font-size: var(--consonant-merch-card-heading-m-font-size); + line-height: var(--consonant-merch-card-heading-m-line-height); + margin: 0; + color: var(--merch-color-grey-80); + font-weight: 700; } -/* grid style for ccd-action */ -.one-merch-card.ccd-action, -.two-merch-cards.ccd-action, -.three-merch-cards.ccd-action, -.four-merch-cards.ccd-action { - grid-template-columns: var(--consonant-merch-card-ccd-action-width); +merch-card [slot='heading-m-price'] { + font-size: var(--consonant-merch-card-heading-m-font-size); + line-height: var(--consonant-merch-card-heading-m-line-height); + padding: 0 var(--consonant-merch-spacing-s); + font-weight: 700; + margin: 0; + color: var(--merch-color-grey-80); } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.ccd-action, - .three-merch-cards.ccd-action, - .four-merch-cards.ccd-action { - grid-template-columns: repeat(2, var(--consonant-merch-card-ccd-action-width)); - } +merch-card [slot='offers'] { + padding: var(--consonant-merch-spacing-xxs) var(--consonant-merch-spacing-s); } -/* desktop */ -@media screen and ${a} { - .three-merch-cards.ccd-action, - .four-merch-cards.ccd-action { - grid-template-columns: repeat(3, var(--consonant-merch-card-ccd-action-width)); - } +merch-card [slot='heading-l'] { + font-size: var(--consonant-merch-card-heading-l-font-size); + line-height: var(--consonant-merch-card-heading-l-line-height); + margin: 0; + color: var(--merch-color-grey-80); } -/* Large desktop */ - @media screen and ${i} { - .four-merch-cards.ccd-action { - grid-template-columns: repeat(4, var(--consonant-merch-card-ccd-action-width)); - } +merch-card [slot='heading-xl'] { + font-size: var(--consonant-merch-card-heading-xl-font-size); + line-height: var(--consonant-merch-card-heading-xl-line-height); + margin: 0; + color: var(--merch-color-grey-80); } -/* grid style for mini-compare-chart */ -.one-merch-card.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); - gap: var(--consonant-merch-spacing-xs); +merch-card [slot='callout-content'] { + display: flex; + flex-direction: column; + margin: var(--consonant-merch-spacing-xxxs) 0px; + gap: var(--consonant-merch-card-callout-spacing-xxs); } -.two-merch-cards.mini-compare-chart, -.three-merch-cards.mini-compare-chart, -.four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-width)); - gap: var(--consonant-merch-spacing-xs); +merch-card [slot='callout-content'] > div { + display: flex; + flex-direction: column; + margin: var(--consonant-merch-spacing-xxxs) 0px; + gap: var(--consonant-merch-card-callout-spacing-xxs); + align-items: flex-start; } -@media screen and ${g} { - .two-merch-cards.mini-compare-chart, - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-width); - gap: var(--consonant-merch-spacing-xs); - } +merch-card [slot='callout-content'] > div > div { + display: flex; + background: rgba(203 203 203 / 50%); + border-radius: var(--consonant-merch-spacing-xxxs); + padding: var(--consonant-merch-spacing-xxxs) var(--consonant-merch-spacing-xxxs) var(--consonant-merch-spacing-xxxs) var(--consonant-merch-spacing-xxs); } -@media screen and ${x} { - .three-merch-cards.mini-compare-chart merch-card [slot="footer"] a, - .four-merch-cards.mini-compare-chart merch-card [slot="footer"] a { - flex: 1; - } +merch-card [slot='callout-content'] > div > div > div { + display: inline-block; + text-align: left; + font: normal normal normal var(--consonant-merch-card-callout-font-size)/var(--consonant-merch-card-callout-line-height) var(--body-font-family, 'Adobe Clean'); + letter-spacing: var(--consonant-merch-card-callout-letter-spacing); + color: var(--consonant-merch-card-callout-font-color); } -/* Tablet */ -@media screen and ${c} { - .two-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); - gap: var(--consonant-merch-spacing-m); - } - - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); - } +merch-card [slot='callout-content'] img { + width: var(--consonant-merch-card-callout-icon-size); + height: var(--consonant-merch-card-callout-icon-size); + margin: 2.5px 0px 0px 9px; } -/* desktop */ -@media screen and ${a} { - .one-merch-card.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); - } +merch-card [slot='detail-m'] { + font-size: var(--consonant-merch-card-detail-m-font-size); + letter-spacing: var(--consonant-merch-card-detail-m-letter-spacing); + font-weight: var(--consonant-merch-card-detail-m-font-weight); + text-transform: uppercase; + margin: 0; + color: var(--merch-color-grey-80); +} - .two-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-wide-width)); - gap: var(--consonant-merch-spacing-m); - } +merch-card [slot="body-xxs"] { + font-size: var(--consonant-merch-card-body-xxs-font-size); + line-height: var(--consonant-merch-card-body-xxs-line-height); + font-weight: normal; + letter-spacing: var(--consonant-merch-card-body-xxs-letter-spacing); + color: var(--merch-color-grey-80); + margin: 0; +} - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(3, var(--consonant-merch-card-mini-compare-chart-width)); - gap: var(--consonant-merch-spacing-m); - } +merch-card [slot="body-xs"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + color: var(--merch-color-grey-80); } -@media screen and ${i} { - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(4, var(--consonant-merch-card-mini-compare-chart-width)); - } +merch-card [slot="body-m"] { + font-size: var(--consonant-merch-card-body-m-font-size); + line-height: var(--consonant-merch-card-body-m-line-height); + color: var(--merch-color-grey-80); } -/* mini-compare card footer rows */ -merch-card .footer-row-cell:nth-child(1) { - min-height: var(--consonant-merch-card-footer-row-1-min-height); +merch-card [slot="body-l"] { + font-size: var(--consonant-merch-card-body-l-font-size); + line-height: var(--consonant-merch-card-body-l-line-height); + color: var(--merch-color-grey-80); } -merch-card .footer-row-cell:nth-child(2) { - min-height: var(--consonant-merch-card-footer-row-2-min-height); +merch-card [slot="body-xl"] { + font-size: var(--consonant-merch-card-body-xl-font-size); + line-height: var(--consonant-merch-card-body-xl-line-height); + color: var(--merch-color-grey-80); } -merch-card .footer-row-cell:nth-child(3) { - min-height: var(--consonant-merch-card-footer-row-3-min-height); +[slot="cci-footer"] p, +[slot="cct-footer"] p, +[slot="cce-footer"] p { + margin: 0; } -merch-card .footer-row-cell:nth-child(4) { - min-height: var(--consonant-merch-card-footer-row-4-min-height); +merch-card [slot="promo-text"] { + color: var(--merch-color-green-promo); + font-size: var(--consonant-merch-card-promo-text-height); + font-weight: 700; + line-height: var(--consonant-merch-card-promo-text-height); + margin: 0; + min-height: var(--consonant-merch-card-promo-text-height); + padding: 0; } -merch-card .footer-row-cell:nth-child(5) { - min-height: var(--consonant-merch-card-footer-row-5-min-height); +div[slot="footer"] { + display: contents; } -merch-card .footer-row-cell:nth-child(6) { - min-height: var(--consonant-merch-card-footer-row-6-min-height); +[slot="footer"] a { + word-wrap: break-word; + text-align: center; } -merch-card .footer-row-cell:nth-child(7) { - min-height: var(--consonant-merch-card-footer-row-7-min-height); +[slot="footer"] a:not([class]) { + font-weight: 700; + font-size: var(--consonant-merch-card-cta-font-size); } -merch-card .footer-row-cell:nth-child(8) { - min-height: var(--consonant-merch-card-footer-row-8-min-height); +div[slot='bg-image'] img { + position: relative; + width: 100%; + min-height: var(--consonant-merch-card-bg-img-height); + max-height: var(--consonant-merch-card-bg-img-height); + object-fit: cover; + border-top-left-radius: 16px; + border-top-right-radius: 16px; } span[is="inline-price"][data-template='strikethrough'] { @@ -1510,152 +1654,4 @@ body.merch-modal { scrollbar-gutter: stable; height: 100vh; } -`;document.head.appendChild($);var T="merch-offer-select:ready",_="merch-card:ready",M="merch-card:action-menu-toggle";var y="merch-storage:change",w="merch-quantity-selector:change";var m="merch-card",H=32,v="mini-compare-chart",R=l=>`--consonant-merch-card-footer-row-${l}-min-height`,d=class extends N{static properties={name:{type:String,attribute:"name",reflect:!0},variant:{type:String,reflect:!0},size:{type:String,attribute:"size",reflect:!0},badgeColor:{type:String,attribute:"badge-color"},borderColor:{type:String,attribute:"border-color"},badgeBackgroundColor:{type:String,attribute:"badge-background-color"},badgeText:{type:String,attribute:"badge-text"},actionMenu:{type:Boolean,attribute:"action-menu"},actionMenuContent:{type:String,attribute:"action-menu-content"},customHr:{type:Boolean,attribute:"custom-hr"},detailBg:{type:String,attribute:"detail-bg"},secureLabel:{type:String,attribute:"secure-label"},checkboxLabel:{type:String,attribute:"checkbox-label"},selected:{type:Boolean,attribute:"aria-selected",reflect:!0},storageOption:{type:String,attribute:"storage",reflect:!0},stockOfferOsis:{type:Object,attribute:"stock-offer-osis",converter:{fromAttribute:e=>{let[t,r,o]=e.split(",");return{PUF:t,ABM:r,M2M:o}}}},filters:{type:String,reflect:!0,converter:{fromAttribute:e=>Object.fromEntries(e.split(",").map(t=>{let[r,o,s]=t.split(":"),p=Number(o);return[r,{order:isNaN(p)?void 0:p,size:s}]})),toAttribute:e=>Object.entries(e).map(([t,{order:r,size:o}])=>[t,r,o].filter(s=>s!=null).join(":")).join(",")}},types:{type:String,attribute:"types",reflect:!0},merchOffer:{type:Object}};static styles=[C,...z()];customerSegment;marketSegment;constructor(){super(),this.filters={},this.types="",this.selected=!1}#e;updated(e){(e.has("badgeBackgroundColor")||e.has("borderColor"))&&(this.style.border=this.computedBorderStyle),this.updateComplete.then(async()=>{let r=Array.from(this.querySelectorAll('span[is="inline-price"][data-wcs-osi]')).filter(o=>!o.closest('[slot="callout-content"]'));await Promise.all(r.map(o=>o.onceSettled())),this.adjustTitleWidth(),k()?this.removeEmptyRows():(this.adjustMiniCompareBodySlots(),this.adjustMiniCompareFooterRows())})}get computedBorderStyle(){return this.variant!=="twp"?`1px solid ${this.borderColor?this.borderColor:this.badgeBackgroundColor}`:""}get evergreen(){return this.classList.contains("intro-pricing")}get stockCheckbox(){return this.checkboxLabel?n``:""}get cardImage(){return n`
- - ${this.badge} -
`}get secureLabelFooter(){let e=this.secureLabel?n`${this.secureLabel}`:"";return n`
${e}
`}get miniCompareFooter(){let e=this.secureLabel?n` - ${this.secureLabel}`:n``;return n`
${e}
`}get badge(){let e;if(!(!this.badgeBackgroundColor||!this.badgeColor||!this.badgeText))return this.evergreen&&(e=`border: 1px solid ${this.badgeBackgroundColor}; border-right: none;`),n` -
- ${this.badgeText} -
- `}get badgeElement(){return this.shadowRoot.getElementById("badge")}getContainer(){return this.closest('[class*="-merch-cards"]')??this.parentElement}get headingmMSlot(){return this.shadowRoot.querySelector('slot[name="heading-m"]').assignedElements()[0]}get footerSlot(){return this.shadowRoot.querySelector('slot[name="footer"]')?.assignedElements()[0]}get price(){return this.headingmMSlot?.querySelector('span[is="inline-price"]')}get checkoutLinks(){return[...this.footerSlot?.querySelectorAll('a[is="checkout-link"]')??[]]}async toggleStockOffer({target:e}){if(!this.stockOfferOsis)return;let t=this.checkoutLinks;if(t.length!==0)for(let r of t){await r.onceSettled();let o=r.value?.[0]?.planType;if(!o)return;let s=this.stockOfferOsis[o];if(!s)return;let p=r.dataset.wcsOsi.split(",").filter(A=>A!==s);e.checked&&p.push(s),r.dataset.wcsOsi=p.join(",")}}toggleActionMenu(e){let t=e?.type==="mouseleave"?!0:void 0,r=this.shadowRoot.querySelector('slot[name="action-menu-content"]');r&&(t||this.dispatchEvent(new CustomEvent(M,{bubbles:!0,composed:!0,detail:{card:this.name,type:"action-menu"}})),r.classList.toggle("hidden",t))}handleQuantitySelection(e){let t=this.checkoutLinks;for(let r of t)r.dataset.quantity=e.detail.option}get titleElement(){return this.variant==="special-offers"?this.querySelector('[slot="detail-m"]'):this.querySelector('[slot="heading-xs"]')}get title(){return this.titleElement?.textContent?.trim()}get description(){return this.querySelector('[slot="body-xs"]')?.textContent?.trim()}updateFilters(e){let t={...this.filters};Object.keys(t).forEach(r=>{if(e){t[r].order=Math.min(t[r].order||2,2);return}let o=t[r].order;o===1||isNaN(o)||(t[r].order=Number(o)+1)}),this.filters=t}includes(e){return this.textContent.match(new RegExp(e,"i"))!==null}render(){if(!(!this.isConnected||this.style.display==="none"))switch(this.variant){case"special-offers":return this.renderSpecialOffer();case"segment":return this.renderSegment();case"plans":return this.renderPlans();case"catalog":return this.renderCatalog();case"image":return this.renderImage();case"product":return this.renderProduct();case"inline-heading":return this.renderInlineHeading();case v:return this.renderMiniCompareChart();case"ccd-action":return this.renderCcdAction();case"twp":return this.renderTwp();default:return this.renderProduct()}}renderSpecialOffer(){return n`${this.cardImage} -
- - - -
- ${this.evergreen?n` -
- -
- `:n` -
- ${this.secureLabelFooter} - `} - `}get promoBottom(){return this.classList.contains("promo-bottom")}get startingAt(){return this.classList.contains("starting-at")}renderSegment(){return n` ${this.badge} -
- - - ${this.promoBottom?"":n``} - - ${this.promoBottom?n``:""} -
-
- ${this.secureLabelFooter}`}renderPlans(){return n` ${this.badge} -
- - - - - ${this.promoBottom?"":n` `} - - ${this.promoBottom?n` `:""} - ${this.stockCheckbox} -
- - ${this.secureLabelFooter}`}renderCatalog(){return n`
-
- ${this.badge} -
-
- ${this.actionMenuContent} - - - - ${this.promoBottom?"":n``} - - ${this.promoBottom?n``:""} -
- ${this.secureLabelFooter}`}renderImage(){return n`${this.cardImage} -
- - - - ${this.promoBottom?n``:n``} -
- ${this.evergreen?n` -
- -
- `:n` -
- ${this.secureLabelFooter} - `}`}renderInlineHeading(){return n` ${this.badge} -
-
- - -
- -
- ${this.customHr?"":n`
`} ${this.secureLabelFooter}`}renderProduct(){return n` ${this.badge} -
- - - - ${this.promoBottom?"":n``} - - ${this.promoBottom?n``:""} -
- ${this.secureLabelFooter}`}renderMiniCompareChart(){let{badge:e}=this;return n`
- ${e} -
- - - - - - - - - ${this.miniCompareFooter} - `}renderTwp(){return n`${this.badge} -
- - - -
-
- -
-
`}renderCcdAction(){return n`
- ${this.badge} - - - ${this.promoBottom?n``:n``} -
- -
`}connectedCallback(){super.connectedCallback(),this.#e=this.getContainer(),this.setAttribute("tabindex",this.getAttribute("tabindex")??"0"),this.addEventListener("mouseleave",this.toggleActionMenu),this.addEventListener(w,this.handleQuantitySelection),this.addEventListener(T,this.merchCardReady,{once:!0}),this.updateComplete.then(()=>{this.merchCardReady()}),this.storageOptions?.addEventListener("change",this.handleStorageChange)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(w,this.handleQuantitySelection),this.storageOptions?.removeEventListener(y,this.handleStorageChange)}updateMiniCompareElementMinHeight(e,t){let r=`--consonant-merch-card-mini-compare-${t}-height`,o=Math.max(0,parseInt(window.getComputedStyle(e).height)||0),s=parseInt(this.#e.style.getPropertyValue(r))||0;o>s&&this.#e.style.setProperty(r,`${o}px`)}async adjustTitleWidth(){if(!["segment","plans"].includes(this.variant))return;let e=this.getBoundingClientRect().width,t=this.badgeElement?.getBoundingClientRect().width||0;e===0||t===0||this.style.setProperty("--consonant-merch-card-heading-xs-max-width",`${Math.round(e-t-16)}px`)}async adjustMiniCompareBodySlots(){if(this.variant!==v||this.getBoundingClientRect().width===0)return;this.updateMiniCompareElementMinHeight(this.shadowRoot.querySelector(".top-section"),"top-section"),["heading-m","body-m","heading-m-price","price-commitment","offers","promo-text","callout-content","secure-transaction-label"].forEach(r=>this.updateMiniCompareElementMinHeight(this.shadowRoot.querySelector(`slot[name="${r}"]`),r)),this.updateMiniCompareElementMinHeight(this.shadowRoot.querySelector("footer"),"footer");let t=this.shadowRoot.querySelector(".mini-compare-chart-badge");t&&t.textContent!==""&&this.#e.style.setProperty("--consonant-merch-card-mini-compare-top-section-mobile-height","32px")}adjustMiniCompareFooterRows(){if(this.variant!==v||this.getBoundingClientRect().width===0)return;[...this.querySelector('[slot="footer-rows"]').children].forEach((t,r)=>{let o=Math.max(H,parseInt(window.getComputedStyle(t).height)||0),s=parseInt(this.#e.style.getPropertyValue(R(r+1)))||0;o>s&&this.#e.style.setProperty(R(r+1),`${o}px`)})}removeEmptyRows(){if(this.variant!==v)return;this.querySelectorAll(".footer-row-cell").forEach(t=>{let r=t.querySelector(".footer-row-cell-description");r&&!r.textContent.trim()&&t.remove()})}get storageOptions(){return this.querySelector("sp-radio-group#storage")}get storageSpecificOfferSelect(){let e=this.storageOptions?.selected;if(e){let t=this.querySelector(`merch-offer-select[storage="${e}"]`);if(t)return t}return this.querySelector("merch-offer-select")}get offerSelect(){return this.storageOptions?this.storageSpecificOfferSelect:this.querySelector("merch-offer-select")}get quantitySelect(){return this.querySelector("merch-quantity-select")}merchCardReady(){this.offerSelect&&!this.offerSelect.planType||this.dispatchEvent(new CustomEvent(_,{bubbles:!0}))}handleStorageChange(){let e=this.closest("merch-card")?.offerSelect.cloneNode(!0);e&&this.dispatchEvent(new CustomEvent(y,{detail:{offerSelect:e},bubbles:!0}))}get dynamicPrice(){return this.querySelector('[slot="price"]')}selectMerchOffer(e){if(e===this.merchOffer)return;this.merchOffer=e;let t=this.dynamicPrice;if(e.price&&t){let r=e.price.cloneNode(!0);t.onceSettled?t.onceSettled().then(()=>{t.replaceWith(r)}):t.replaceWith(r)}}};customElements.define(m,d); +`;document.head.appendChild(se);var c="merch-card",a=class extends we{static properties={name:{type:String,attribute:"name",reflect:!0},variant:{type:String,reflect:!0},size:{type:String,attribute:"size",reflect:!0},badgeColor:{type:String,attribute:"badge-color"},borderColor:{type:String,attribute:"border-color"},badgeBackgroundColor:{type:String,attribute:"badge-background-color"},badgeText:{type:String,attribute:"badge-text"},actionMenu:{type:Boolean,attribute:"action-menu"},actionMenuContent:{type:String,attribute:"action-menu-content"},customHr:{type:Boolean,attribute:"custom-hr"},detailBg:{type:String,attribute:"detail-bg"},secureLabel:{type:String,attribute:"secure-label"},checkboxLabel:{type:String,attribute:"checkbox-label"},selected:{type:Boolean,attribute:"aria-selected",reflect:!0},storageOption:{type:String,attribute:"storage",reflect:!0},stockOfferOsis:{type:Object,attribute:"stock-offer-osis",converter:{fromAttribute:e=>{let[t,o,h]=e.split(",");return{PUF:t,ABM:o,M2M:h}}}},filters:{type:String,reflect:!0,converter:{fromAttribute:e=>Object.fromEntries(e.split(",").map(t=>{let[o,h,p]=t.split(":"),x=Number(h);return[o,{order:isNaN(x)?void 0:x,size:p}]})),toAttribute:e=>Object.entries(e).map(([t,{order:o,size:h}])=>[t,o,h].filter(p=>p!=null).join(":")).join(",")}},types:{type:String,attribute:"types",reflect:!0},merchOffer:{type:Object}};static styles=[G,ie(),...U()];customerSegment;marketSegment;variantLayout;constructor(){super(),this.filters={},this.types="",this.selected=!1}updated(e){(e.has("badgeBackgroundColor")||e.has("borderColor"))&&(this.style.border=this.computedBorderStyle),this.updateComplete.then(async()=>{let o=Array.from(this.querySelectorAll('span[is="inline-price"][data-wcs-osi]')).filter(h=>!h.closest('[slot="callout-content"]'));await Promise.all(o.map(h=>h.onceSettled())),this.variantLayout.postCardUpdateHook(this)})}render(){if(!(!this.isConnected||this.style.display==="none"))return this.variantLayout.renderLayout()}get computedBorderStyle(){return this.variant!=="twp"?`1px solid ${this.borderColor?this.borderColor:this.badgeBackgroundColor}`:""}get badgeElement(){return this.shadowRoot.getElementById("badge")}get headingmMSlot(){return this.shadowRoot.querySelector('slot[name="heading-m"]').assignedElements()[0]}get footerSlot(){return this.shadowRoot.querySelector('slot[name="footer"]')?.assignedElements()[0]}get price(){return this.headingmMSlot?.querySelector('span[is="inline-price"]')}get checkoutLinks(){return[...this.footerSlot?.querySelectorAll('a[is="checkout-link"]')??[]]}async toggleStockOffer({target:e}){if(!this.stockOfferOsis)return;let t=this.checkoutLinks;if(t.length!==0)for(let o of t){await o.onceSettled();let h=o.value?.[0]?.planType;if(!h)return;let p=this.stockOfferOsis[h];if(!p)return;let x=o.dataset.wcsOsi.split(",").filter(he=>he!==p);e.checked&&x.push(p),o.dataset.wcsOsi=x.join(",")}}handleQuantitySelection(e){let t=this.checkoutLinks;for(let o of t)o.dataset.quantity=e.detail.option}get titleElement(){return this.querySelector(this.variantLayout?.headingSelector||".card-heading")}get title(){return this.titleElement?.textContent?.trim()}get description(){return this.querySelector('[slot="body-xs"]')?.textContent?.trim()}updateFilters(e){let t={...this.filters};Object.keys(t).forEach(o=>{if(e){t[o].order=Math.min(t[o].order||2,2);return}let h=t[o].order;h===1||isNaN(h)||(t[o].order=Number(h)+1)}),this.filters=t}includes(e){return this.textContent.match(new RegExp(e,"i"))!==null}get startingAt(){return this.classList.contains("starting-at")}connectedCallback(){super.connectedCallback(),this.variantLayout=ce(this),this.variantLayout.connectedCallbackHook(),this.setAttribute("tabindex",this.getAttribute("tabindex")??"0"),this.addEventListener(R,this.handleQuantitySelection),this.addEventListener(I,this.merchCardReady,{once:!0}),this.updateComplete.then(()=>{this.merchCardReady()}),this.storageOptions?.addEventListener("change",this.handleStorageChange)}disconnectedCallback(){super.disconnectedCallback(),this.variantLayout.disconnectedCallbackHook(),this.removeEventListener(R,this.handleQuantitySelection),this.storageOptions?.removeEventListener(O,this.handleStorageChange)}get storageOptions(){return this.querySelector("sp-radio-group#storage")}get storageSpecificOfferSelect(){let e=this.storageOptions?.selected;if(e){let t=this.querySelector(`merch-offer-select[storage="${e}"]`);if(t)return t}return this.querySelector("merch-offer-select")}get offerSelect(){return this.storageOptions?this.storageSpecificOfferSelect:this.querySelector("merch-offer-select")}get quantitySelect(){return this.querySelector("merch-quantity-select")}merchCardReady(){this.offerSelect&&!this.offerSelect.planType||this.dispatchEvent(new CustomEvent(K,{bubbles:!0}))}handleStorageChange(){let e=this.closest("merch-card")?.offerSelect.cloneNode(!0);e&&this.dispatchEvent(new CustomEvent(O,{detail:{offerSelect:e},bubbles:!0}))}get dynamicPrice(){return this.querySelector('[slot="price"]')}selectMerchOffer(e){if(e===this.merchOffer)return;this.merchOffer=e;let t=this.dynamicPrice;if(e.price&&t){let o=e.price.cloneNode(!0);t.onceSettled?t.onceSettled().then(()=>{t.replaceWith(o)}):t.replaceWith(o)}}};customElements.define(c,a); diff --git a/libs/deps/mas/plans-modal.js b/libs/deps/mas/plans-modal.js index 986dd50015..93c8e38a63 100644 --- a/libs/deps/mas/plans-modal.js +++ b/libs/deps/mas/plans-modal.js @@ -4999,13 +4999,6 @@ var MatchMediaController = class { } }; -// src/media.js -var MOBILE_LANDSCAPE = "(max-width: 767px)"; -var TABLET_DOWN = "(max-width: 1199px)"; -var TABLET_UP = "(min-width: 768px)"; -var DESKTOP_UP = "(min-width: 1200px)"; -var LARGE_DESKTOP = "(min-width: 1600px)"; - // src/global.css.js var styles = document.createElement("style"); styles.innerHTML = ` @@ -5071,7 +5064,6 @@ styles.innerHTML = ` --consonant-merch-card-heading-padding: 0; - --consonant-merch-card-image-height: 180px; /* colors */ --consonant-merch-card-border-color: #eaeaea; @@ -5084,45 +5076,6 @@ styles.innerHTML = ` --consonant-merch-card-max-width: 300px; --transition: cmax-height 0.3s linear, opacity 0.3s linear; - /* special offers */ - --consonant-merch-card-special-offers-width: 378px; - - /* image */ - --consonant-merch-card-image-width: 300px; - - /* segment */ - --consonant-merch-card-segment-width: 378px; - - /* inline-heading */ - --consonant-merch-card-inline-heading-width: 300px; - - /* product */ - --consonant-merch-card-product-width: 300px; - - /* plans */ - --consonant-merch-card-plans-width: 300px; - --consonant-merch-card-plans-icon-size: 40px; - - /* catalog */ - --consonant-merch-card-catalog-width: 276px; - --consonant-merch-card-catalog-icon-size: 40px; - - /* twp */ - --consonant-merch-card-twp-width: 268px; - --consonant-merch-card-twp-mobile-width: 300px; - --consonant-merch-card-twp-mobile-height: 358px; - - /* ccd-action */ - --consonant-merch-card-ccd-action-width: 276px; - --consonant-merch-card-ccd-action-min-height: 320px; - - - /*mini compare chart */ - --consonant-merch-card-mini-compare-chart-icon-size: 32px; - --consonant-merch-card-mini-compare-mobile-cta-font-size: 15px; - --consonant-merch-card-mini-compare-mobile-cta-width: 75px; - --consonant-merch-card-mini-compare-badge-mobile-max-width: 50px; - /* inline SVGs */ --checkmark-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath fill='%23fff' d='M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z' class='spectrum-UIIcon--medium'/%3E%3C/svg%3E%0A"); @@ -5178,10 +5131,6 @@ merch-card.has-divider hr { border: none; } -merch-card[variant="special-offers"] span[is="inline-price"][data-template="strikethrough"] { - font-size: var(--consonant-merch-card-body-xs-font-size); -} - merch-card p, merch-card h3, merch-card h4 { margin: 0; } @@ -5336,155 +5285,12 @@ merch-card [slot="body-xl"] { color: var(--merch-color-grey-80); } -merch-card[variant="plans"] [slot="description"] { - min-height: 84px; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] { - background-color: #000; - color: var(--color-white, #fff); - font-size: var(--consonant-merch-card-body-xs-font-size); - width: fit-content; - padding: var(--consonant-merch-spacing-xs); - border-radius: var(--consonant-merch-spacing-xxxs); - position: absolute; - top: 55px; - right: 15px; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ul { - padding-left: 0; - padding-bottom: var(--consonant-merch-spacing-xss); - margin-top: 0; - margin-bottom: 0; - list-style-position: inside; - list-style-type: '\u2022 '; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ul li { - padding-left: 0; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ::marker { - margin-right: 0; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] p { - color: var(--color-white, #fff); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] a { - color: var(--consonant-merch-card-background-color); - text-decoration: underline; -} - -merch-card[variant="catalog"] [slot="payment-details"] { - font-size: var(--consonant-merch-card-body-font-size); - font-style: italic; - font-weight: 400; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="ccd-action"] .price-strikethrough { - font-size: 18px; -} - -merch-card[variant="plans"] [slot="quantity-select"] { - display: flex; - justify-content: flex-start; - box-sizing: border-box; - width: 100%; - padding: var(--consonant-merch-spacing-xs); -} - -merch-card[variant="twp"] div[class$='twp-badge'] { - padding: 4px 10px 5px 10px; -} - -merch-card[variant="twp"] [slot="body-xs-top"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - color: var(--merch-color-grey-80); -} - -merch-card[variant="twp"] [slot="body-xs"] ul { - padding: 0; - margin: 0; -} - -merch-card[variant="twp"] [slot="body-xs"] ul li { - list-style-type: none; - padding-left: 0; -} - -merch-card[variant="twp"] [slot="body-xs"] ul li::before { - content: '\xB7'; - font-size: 20px; - padding-right: 5px; - font-weight: bold; -} - -merch-card[variant="twp"] [slot="footer"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - padding: var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs); - color: var(--merch-color-grey-80); - display: flex; - flex-flow: wrap; -} - -merch-card[variant='twp'] merch-quantity-select, -merch-card[variant='twp'] merch-offer-select { - display: none; -} - [slot="cci-footer"] p, [slot="cct-footer"] p, [slot="cce-footer"] p { margin: 0; } -/* mini compare chart card styles */ - -merch-card[variant="mini-compare-chart"] [slot="heading-m"] { - padding: 0 var(--consonant-merch-spacing-s) 0; -} - -merch-card[variant="mini-compare-chart"] [slot="body-m"] { - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); -} - -merch-card[variant="mini-compare-chart"] [is="inline-price"] { - display: inline-block; - min-height: 30px; - min-width: 1px; -} - -merch-card[variant="mini-compare-chart"] [slot='callout-content'] { - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0px; -} - -merch-card[variant="mini-compare-chart"] [slot='callout-content'] [is="inline-price"] { - min-height: unset; -} - -merch-card[variant="mini-compare-chart"] [slot="price-commitment"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - padding: 0 var(--consonant-merch-spacing-s); -} - -merch-card[variant="mini-compare-chart"] [slot="price-commitment"] a { - display: inline-block; - height: 27px; -} - -merch-card[variant="mini-compare-chart"] [slot="offers"] { - font-size: var(--consonant-merch-card-body-xs-font-size); -} - merch-card [slot="promo-text"] { color: var(--merch-color-green-promo); font-size: var(--consonant-merch-card-promo-text-height); @@ -5495,111 +5301,6 @@ merch-card [slot="promo-text"] { padding: 0; } - -merch-card[variant="mini-compare-chart"] [slot="body-xxs"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; -} - -merch-card[variant="mini-compare-chart"] [slot="promo-text"] { - font-size: var(--consonant-merch-card-body-m-font-size); - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; -} - -merch-card[variant="mini-compare-chart"] [slot="promo-text"] a { - text-decoration: underline; -} - -merch-card[variant="mini-compare-chart"] .footer-row-icon { - display: flex; - place-items: center; -} - -merch-card[variant="mini-compare-chart"] .footer-row-icon img { - max-width: initial; - width: var(--consonant-merch-card-mini-compare-chart-icon-size); - height: var(--consonant-merch-card-mini-compare-chart-icon-size); -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell { - border-top: 1px solid var(--consonant-merch-card-border-color); - display: flex; - gap: var(--consonant-merch-spacing-xs); - justify-content: start; - place-items: center; - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell-description { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell-description p { - color: var(--merch-color-grey-80); - vertical-align: bottom; -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { - color: var(--color-accent); - text-decoration: solid; -} - -/* mini compare mobile */ -@media screen and ${MOBILE_LANDSCAPE} { - merch-card[variant="mini-compare-chart"] [slot='heading-m'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='heading-m-price'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='body-m'] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot="promo-text"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - merch-card[variant="mini-compare-chart"] .footer-row-cell-description { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } -} - -/* mini compare tablet */ -@media screen and ${TABLET_DOWN} { - merch-card[variant="mini-compare-chart"] [slot='heading-m'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='heading-m-price'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='body-m'] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot="promo-text"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - - merch-card[variant="mini-compare-chart"] .footer-row-cell-description { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } -} - div[slot="footer"] { display: contents; } @@ -5624,451 +5325,6 @@ div[slot='bg-image'] img { border-top-right-radius: 16px; } -/* Mobile */ -@media screen and ${MOBILE_LANDSCAPE} { - :root { - --consonant-merch-card-mini-compare-chart-width: 302px; - --consonant-merch-card-segment-width: 276px; - --consonant-merch-card-mini-compare-chart-wide-width: 302px; - --consonant-merch-card-special-offers-width: 302px; - --consonant-merch-card-twp-width: 300px; - } -} - - -/* Tablet */ -@media screen and ${TABLET_UP} { - :root { - --consonant-merch-card-catalog-width: 302px; - --consonant-merch-card-plans-width: 302px; - --consonant-merch-card-segment-width: 276px; - --consonant-merch-card-mini-compare-chart-width: 302px; - --consonant-merch-card-mini-compare-chart-wide-width: 302px; - --consonant-merch-card-special-offers-width: 302px; - --consonant-merch-card-twp-width: 268px; - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - :root { - --consonant-merch-card-catalog-width: 276px; - --consonant-merch-card-plans-width: 276px; - --consonant-merch-card-segment-width: 302px; - --consonant-merch-card-inline-heading-width: 378px; - --consonant-merch-card-product-width: 378px; - --consonant-merch-card-image-width: 378px; - --consonant-merch-card-mini-compare-chart-width: 378px; - --consonant-merch-card-mini-compare-chart-wide-width: 484px; - --consonant-merch-card-twp-width: 268px; - } -} - -/* supported cards */ -/* grid style for plans */ -.one-merch-card.plans, -.two-merch-cards.plans, -.three-merch-cards.plans, -.four-merch-cards.plans { - grid-template-columns: var(--consonant-merch-card-plans-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.plans, - .three-merch-cards.plans, - .four-merch-cards.plans { - grid-template-columns: repeat(2, var(--consonant-merch-card-plans-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.plans, - .four-merch-cards.plans { - grid-template-columns: repeat(3, var(--consonant-merch-card-plans-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.plans { - grid-template-columns: repeat(4, var(--consonant-merch-card-plans-width)); - } -} - - -/* grid style for catalog */ -.one-merch-card.catalog, -.two-merch-cards.catalog, -.three-merch-cards.catalog, -.four-merch-cards.catalog { - grid-template-columns: var(--consonant-merch-card-catalog-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.catalog, - .three-merch-cards.catalog, - .four-merch-cards.catalog { - grid-template-columns: repeat(2, var(--consonant-merch-card-catalog-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.catalog, - .four-merch-cards.catalog { - grid-template-columns: repeat(3, var(--consonant-merch-card-catalog-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.catalog { - grid-template-columns: repeat(4, var(--consonant-merch-card-catalog-width)); - } -} - - -/* grid style for special-offers */ -.one-merch-card.special-offers, -.two-merch-cards.special-offers, -.three-merch-cards.special-offers, -.four-merch-cards.special-offers { - grid-template-columns: minmax(300px, var(--consonant-merch-card-special-offers-width)); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.special-offers, - .three-merch-cards.special-offers, - .four-merch-cards.special-offers { - grid-template-columns: repeat(2, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.special-offers, - .four-merch-cards.special-offers { - grid-template-columns: repeat(3, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } -} - -@media screen and ${LARGE_DESKTOP} { - .four-merch-cards.special-offers { - grid-template-columns: repeat(4, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } -} - - -/* grid style for image */ -.one-merch-card.image, -.two-merch-cards.image, -.three-merch-cards.image, -.four-merch-cards.image { - grid-template-columns: var(--consonant-merch-card-image-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.image, - .three-merch-cards.image, - .four-merch-cards.image { - grid-template-columns: repeat(2, var(--consonant-merch-card-image-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.image, - .four-merch-cards.image { - grid-template-columns: repeat(3, var(--consonant-merch-card-image-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.image { - grid-template-columns: repeat(4, var(--consonant-merch-card-image-width)); - } -} - - -/* grid style for segment */ -.one-merch-card.segment, -.two-merch-cards.segment, -.three-merch-cards.segment, -.four-merch-cards.segment { - grid-template-columns: minmax(276px, var(--consonant-merch-card-segment-width)); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.segment, - .three-merch-cards.segment, - .four-merch-cards.segment { - grid-template-columns: repeat(2, minmax(276px, var(--consonant-merch-card-segment-width))); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.segment { - grid-template-columns: repeat(3, minmax(276px, var(--consonant-merch-card-segment-width))); - } - - .four-merch-cards.segment { - grid-template-columns: repeat(4, minmax(276px, var(--consonant-merch-card-segment-width))); - } -} - - -/* grid style for product */ -.one-merch-card.product, -.two-merch-cards.product, -.three-merch-cards.product, -.four-merch-cards.product { - grid-template-columns: var(--consonant-merch-card-product-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.product, - .three-merch-cards.product, - .four-merch-cards.product { - grid-template-columns: repeat(2, var(--consonant-merch-card-product-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.product, - .four-merch-cards.product { - grid-template-columns: repeat(3, var(--consonant-merch-card-product-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.product { - grid-template-columns: repeat(4, var(--consonant-merch-card-product-width)); - } -} - -/* grid style for twp */ -.one-merch-card.twp, -.two-merch-cards.twp, -.three-merch-cards.twp { - grid-template-columns: var(--consonant-merch-card-image-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .one-merch-card.twp, - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .one-merch-card.twp - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .one-merch-card.twp - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } -} - -/* Mobile */ -@media screen and ${MOBILE_LANDSCAPE} { - .one-merch-card.twp, - .two-merch-cards.twp, - .three-merch-cards.twp { - grid-template-columns: repeat(1, var(--consonant-merch-card-twp-mobile-width)); - } -} - -/* grid style for inline-heading */ -.one-merch-card.inline-heading, -.two-merch-cards.inline-heading, -.three-merch-cards.inline-heading, -.four-merch-cards.inline-heading { - grid-template-columns: var(--consonant-merch-card-inline-heading-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.inline-heading, - .three-merch-cards.inline-heading, - .four-merch-cards.inline-heading { - grid-template-columns: repeat(2, var(--consonant-merch-card-inline-heading-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.inline-heading, - .four-merch-cards.inline-heading { - grid-template-columns: repeat(3, var(--consonant-merch-card-inline-heading-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.inline-heading { - grid-template-columns: repeat(4, var(--consonant-merch-card-inline-heading-width)); - } -} - -/* grid style for ccd-action */ -.one-merch-card.ccd-action, -.two-merch-cards.ccd-action, -.three-merch-cards.ccd-action, -.four-merch-cards.ccd-action { - grid-template-columns: var(--consonant-merch-card-ccd-action-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.ccd-action, - .three-merch-cards.ccd-action, - .four-merch-cards.ccd-action { - grid-template-columns: repeat(2, var(--consonant-merch-card-ccd-action-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.ccd-action, - .four-merch-cards.ccd-action { - grid-template-columns: repeat(3, var(--consonant-merch-card-ccd-action-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.ccd-action { - grid-template-columns: repeat(4, var(--consonant-merch-card-ccd-action-width)); - } -} - -/* grid style for mini-compare-chart */ -.one-merch-card.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); - gap: var(--consonant-merch-spacing-xs); -} - -.two-merch-cards.mini-compare-chart, -.three-merch-cards.mini-compare-chart, -.four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-width)); - gap: var(--consonant-merch-spacing-xs); -} - -@media screen and ${MOBILE_LANDSCAPE} { - .two-merch-cards.mini-compare-chart, - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-width); - gap: var(--consonant-merch-spacing-xs); - } -} - -@media screen and ${TABLET_DOWN} { - .three-merch-cards.mini-compare-chart merch-card [slot="footer"] a, - .four-merch-cards.mini-compare-chart merch-card [slot="footer"] a { - flex: 1; - } -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); - gap: var(--consonant-merch-spacing-m); - } - - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .one-merch-card.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); - } - - .two-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-wide-width)); - gap: var(--consonant-merch-spacing-m); - } - - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(3, var(--consonant-merch-card-mini-compare-chart-width)); - gap: var(--consonant-merch-spacing-m); - } -} - -@media screen and ${LARGE_DESKTOP} { - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(4, var(--consonant-merch-card-mini-compare-chart-width)); - } -} - -/* mini-compare card footer rows */ -merch-card .footer-row-cell:nth-child(1) { - min-height: var(--consonant-merch-card-footer-row-1-min-height); -} - -merch-card .footer-row-cell:nth-child(2) { - min-height: var(--consonant-merch-card-footer-row-2-min-height); -} - -merch-card .footer-row-cell:nth-child(3) { - min-height: var(--consonant-merch-card-footer-row-3-min-height); -} - -merch-card .footer-row-cell:nth-child(4) { - min-height: var(--consonant-merch-card-footer-row-4-min-height); -} - -merch-card .footer-row-cell:nth-child(5) { - min-height: var(--consonant-merch-card-footer-row-5-min-height); -} - -merch-card .footer-row-cell:nth-child(6) { - min-height: var(--consonant-merch-card-footer-row-6-min-height); -} - -merch-card .footer-row-cell:nth-child(7) { - min-height: var(--consonant-merch-card-footer-row-7-min-height); -} - -merch-card .footer-row-cell:nth-child(8) { - min-height: var(--consonant-merch-card-footer-row-8-min-height); -} - span[is="inline-price"][data-template='strikethrough'] { text-decoration: line-through; } @@ -6231,6 +5487,9 @@ var styles2 = css` `; var plans_modal_css_default = styles2; +// src/media.js +var MOBILE_LANDSCAPE = "(max-width: 767px)"; + // src/plans-modal.js var PlansModal = class extends LitElement { static properties = { diff --git a/libs/features/mas/web-components/src/global.css.js b/libs/features/mas/web-components/src/global.css.js index a143726907..0d4d5bfe9e 100644 --- a/libs/features/mas/web-components/src/global.css.js +++ b/libs/features/mas/web-components/src/global.css.js @@ -1,11 +1,3 @@ -import { - TABLET_UP, - DESKTOP_UP, - LARGE_DESKTOP, - MOBILE_LANDSCAPE, - TABLET_DOWN, -} from './media.js'; - const styles = document.createElement('style'); styles.innerHTML = ` @@ -71,7 +63,6 @@ styles.innerHTML = ` --consonant-merch-card-heading-padding: 0; - --consonant-merch-card-image-height: 180px; /* colors */ --consonant-merch-card-border-color: #eaeaea; @@ -84,44 +75,8 @@ styles.innerHTML = ` --consonant-merch-card-max-width: 300px; --transition: cmax-height 0.3s linear, opacity 0.3s linear; - /* special offers */ - --consonant-merch-card-special-offers-width: 378px; - - /* image */ - --consonant-merch-card-image-width: 300px; - - /* segment */ - --consonant-merch-card-segment-width: 378px; - - /* inline-heading */ - --consonant-merch-card-inline-heading-width: 300px; - - /* product */ - --consonant-merch-card-product-width: 300px; - - /* plans */ - --consonant-merch-card-plans-width: 300px; - --consonant-merch-card-plans-icon-size: 40px; - - /* catalog */ - --consonant-merch-card-catalog-width: 276px; - --consonant-merch-card-catalog-icon-size: 40px; - - /* twp */ - --consonant-merch-card-twp-width: 268px; - --consonant-merch-card-twp-mobile-width: 300px; - --consonant-merch-card-twp-mobile-height: 358px; - - /* ccd-action */ - --consonant-merch-card-ccd-action-width: 276px; - --consonant-merch-card-ccd-action-min-height: 320px; - - - /*mini compare chart */ - --consonant-merch-card-mini-compare-chart-icon-size: 32px; - --consonant-merch-card-mini-compare-mobile-cta-font-size: 15px; - --consonant-merch-card-mini-compare-mobile-cta-width: 75px; - --consonant-merch-card-mini-compare-badge-mobile-max-width: 50px; + /* background image */ + --consonant-merch-card-bg-img-height: 180px; /* inline SVGs */ --checkmark-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath fill='%23fff' d='M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z' class='spectrum-UIIcon--medium'/%3E%3C/svg%3E%0A"); @@ -178,10 +133,6 @@ merch-card.has-divider hr { border: none; } -merch-card[variant="special-offers"] span[is="inline-price"][data-template="strikethrough"] { - font-size: var(--consonant-merch-card-body-xs-font-size); -} - merch-card p, merch-card h3, merch-card h4 { margin: 0; } @@ -336,155 +287,12 @@ merch-card [slot="body-xl"] { color: var(--merch-color-grey-80); } -merch-card[variant="plans"] [slot="description"] { - min-height: 84px; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] { - background-color: #000; - color: var(--color-white, #fff); - font-size: var(--consonant-merch-card-body-xs-font-size); - width: fit-content; - padding: var(--consonant-merch-spacing-xs); - border-radius: var(--consonant-merch-spacing-xxxs); - position: absolute; - top: 55px; - right: 15px; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ul { - padding-left: 0; - padding-bottom: var(--consonant-merch-spacing-xss); - margin-top: 0; - margin-bottom: 0; - list-style-position: inside; - list-style-type: '• '; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ul li { - padding-left: 0; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] ::marker { - margin-right: 0; -} - -merch-card[variant="catalog"] [slot="action-menu-content"] p { - color: var(--color-white, #fff); -} - -merch-card[variant="catalog"] [slot="action-menu-content"] a { - color: var(--consonant-merch-card-background-color); - text-decoration: underline; -} - -merch-card[variant="catalog"] .payment-details { - font-size: var(--consonant-merch-card-body-font-size); - font-style: italic; - font-weight: 400; - line-height: var(--consonant-merch-card-body-line-height); -} - -merch-card[variant="ccd-action"] .price-strikethrough { - font-size: 18px; -} - -merch-card[variant="plans"] [slot="quantity-select"] { - display: flex; - justify-content: flex-start; - box-sizing: border-box; - width: 100%; - padding: var(--consonant-merch-spacing-xs); -} - -merch-card[variant="twp"] div[class$='twp-badge'] { - padding: 4px 10px 5px 10px; -} - -merch-card[variant="twp"] [slot="body-xs-top"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - color: var(--merch-color-grey-80); -} - -merch-card[variant="twp"] [slot="body-xs"] ul { - padding: 0; - margin: 0; -} - -merch-card[variant="twp"] [slot="body-xs"] ul li { - list-style-type: none; - padding-left: 0; -} - -merch-card[variant="twp"] [slot="body-xs"] ul li::before { - content: '·'; - font-size: 20px; - padding-right: 5px; - font-weight: bold; -} - -merch-card[variant="twp"] [slot="footer"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - padding: var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs); - color: var(--merch-color-grey-80); - display: flex; - flex-flow: wrap; -} - -merch-card[variant='twp'] merch-quantity-select, -merch-card[variant='twp'] merch-offer-select { - display: none; -} - [slot="cci-footer"] p, [slot="cct-footer"] p, [slot="cce-footer"] p { margin: 0; } -/* mini compare chart card styles */ - -merch-card[variant="mini-compare-chart"] [slot="heading-m"] { - padding: 0 var(--consonant-merch-spacing-s) 0; -} - -merch-card[variant="mini-compare-chart"] [slot="body-m"] { - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); -} - -merch-card[variant="mini-compare-chart"] [is="inline-price"] { - display: inline-block; - min-height: 30px; - min-width: 1px; -} - -merch-card[variant="mini-compare-chart"] [slot='callout-content'] { - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0px; -} - -merch-card[variant="mini-compare-chart"] [slot='callout-content'] [is="inline-price"] { - min-height: unset; -} - -merch-card[variant="mini-compare-chart"] [slot="price-commitment"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - padding: 0 var(--consonant-merch-spacing-s); -} - -merch-card[variant="mini-compare-chart"] [slot="price-commitment"] a { - display: inline-block; - height: 27px; -} - -merch-card[variant="mini-compare-chart"] [slot="offers"] { - font-size: var(--consonant-merch-card-body-xs-font-size); -} - merch-card [slot="promo-text"] { color: var(--merch-color-green-promo); font-size: var(--consonant-merch-card-promo-text-height); @@ -495,111 +303,6 @@ merch-card [slot="promo-text"] { padding: 0; } - -merch-card[variant="mini-compare-chart"] [slot="body-xxs"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; -} - -merch-card[variant="mini-compare-chart"] [slot="promo-text"] { - font-size: var(--consonant-merch-card-body-m-font-size); - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; -} - -merch-card[variant="mini-compare-chart"] [slot="promo-text"] a { - text-decoration: underline; -} - -merch-card[variant="mini-compare-chart"] .footer-row-icon { - display: flex; - place-items: center; -} - -merch-card[variant="mini-compare-chart"] .footer-row-icon img { - max-width: initial; - width: var(--consonant-merch-card-mini-compare-chart-icon-size); - height: var(--consonant-merch-card-mini-compare-chart-icon-size); -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell { - border-top: 1px solid var(--consonant-merch-card-border-color); - display: flex; - gap: var(--consonant-merch-spacing-xs); - justify-content: start; - place-items: center; - padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell-description { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell-description p { - color: var(--merch-color-grey-80); - vertical-align: bottom; -} - -merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { - color: var(--color-accent); - text-decoration: solid; -} - -/* mini compare mobile */ -@media screen and ${MOBILE_LANDSCAPE} { - merch-card[variant="mini-compare-chart"] [slot='heading-m'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='heading-m-price'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='body-m'] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot="promo-text"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - merch-card[variant="mini-compare-chart"] .footer-row-cell-description { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } -} - -/* mini compare tablet */ -@media screen and ${TABLET_DOWN} { - merch-card[variant="mini-compare-chart"] [slot='heading-m'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='heading-m-price'] { - font-size: var(--consonant-merch-card-body-s-font-size); - line-height: var(--consonant-merch-card-body-s-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot='body-m'] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - - merch-card[variant="mini-compare-chart"] [slot="promo-text"] { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } - - merch-card[variant="mini-compare-chart"] .footer-row-cell-description { - font-size: var(--consonant-merch-card-body-xs-font-size); - line-height: var(--consonant-merch-card-body-xs-line-height); - } -} - div[slot="footer"] { display: contents; } @@ -617,458 +320,13 @@ div[slot="footer"] { div[slot='bg-image'] img { position: relative; width: 100%; - min-height: var(--consonant-merch-card-image-height); - max-height: var(--consonant-merch-card-image-height); + min-height: var(--consonant-merch-card-bg-img-height); + max-height: var(--consonant-merch-card-bg-img-height); object-fit: cover; border-top-left-radius: 16px; border-top-right-radius: 16px; } -/* Mobile */ -@media screen and ${MOBILE_LANDSCAPE} { - :root { - --consonant-merch-card-mini-compare-chart-width: 302px; - --consonant-merch-card-segment-width: 276px; - --consonant-merch-card-mini-compare-chart-wide-width: 302px; - --consonant-merch-card-special-offers-width: 302px; - --consonant-merch-card-twp-width: 300px; - } -} - - -/* Tablet */ -@media screen and ${TABLET_UP} { - :root { - --consonant-merch-card-catalog-width: 302px; - --consonant-merch-card-plans-width: 302px; - --consonant-merch-card-segment-width: 276px; - --consonant-merch-card-mini-compare-chart-width: 302px; - --consonant-merch-card-mini-compare-chart-wide-width: 302px; - --consonant-merch-card-special-offers-width: 302px; - --consonant-merch-card-twp-width: 268px; - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - :root { - --consonant-merch-card-catalog-width: 276px; - --consonant-merch-card-plans-width: 276px; - --consonant-merch-card-segment-width: 302px; - --consonant-merch-card-inline-heading-width: 378px; - --consonant-merch-card-product-width: 378px; - --consonant-merch-card-image-width: 378px; - --consonant-merch-card-mini-compare-chart-width: 378px; - --consonant-merch-card-mini-compare-chart-wide-width: 484px; - --consonant-merch-card-twp-width: 268px; - } -} - -/* supported cards */ -/* grid style for plans */ -.one-merch-card.plans, -.two-merch-cards.plans, -.three-merch-cards.plans, -.four-merch-cards.plans { - grid-template-columns: var(--consonant-merch-card-plans-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.plans, - .three-merch-cards.plans, - .four-merch-cards.plans { - grid-template-columns: repeat(2, var(--consonant-merch-card-plans-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.plans, - .four-merch-cards.plans { - grid-template-columns: repeat(3, var(--consonant-merch-card-plans-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.plans { - grid-template-columns: repeat(4, var(--consonant-merch-card-plans-width)); - } -} - - -/* grid style for catalog */ -.one-merch-card.catalog, -.two-merch-cards.catalog, -.three-merch-cards.catalog, -.four-merch-cards.catalog { - grid-template-columns: var(--consonant-merch-card-catalog-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.catalog, - .three-merch-cards.catalog, - .four-merch-cards.catalog { - grid-template-columns: repeat(2, var(--consonant-merch-card-catalog-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.catalog, - .four-merch-cards.catalog { - grid-template-columns: repeat(3, var(--consonant-merch-card-catalog-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.catalog { - grid-template-columns: repeat(4, var(--consonant-merch-card-catalog-width)); - } -} - - -/* grid style for special-offers */ -.one-merch-card.special-offers, -.two-merch-cards.special-offers, -.three-merch-cards.special-offers, -.four-merch-cards.special-offers { - grid-template-columns: minmax(300px, var(--consonant-merch-card-special-offers-width)); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.special-offers, - .three-merch-cards.special-offers, - .four-merch-cards.special-offers { - grid-template-columns: repeat(2, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.special-offers, - .four-merch-cards.special-offers { - grid-template-columns: repeat(3, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } -} - -@media screen and ${LARGE_DESKTOP} { - .four-merch-cards.special-offers { - grid-template-columns: repeat(4, minmax(300px, var(--consonant-merch-card-special-offers-width))); - } -} - - -/* grid style for image */ -.one-merch-card.image, -.two-merch-cards.image, -.three-merch-cards.image, -.four-merch-cards.image { - grid-template-columns: var(--consonant-merch-card-image-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.image, - .three-merch-cards.image, - .four-merch-cards.image { - grid-template-columns: repeat(2, var(--consonant-merch-card-image-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.image, - .four-merch-cards.image { - grid-template-columns: repeat(3, var(--consonant-merch-card-image-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.image { - grid-template-columns: repeat(4, var(--consonant-merch-card-image-width)); - } -} - - -/* grid style for segment */ -.one-merch-card.segment, -.two-merch-cards.segment, -.three-merch-cards.segment, -.four-merch-cards.segment { - grid-template-columns: minmax(276px, var(--consonant-merch-card-segment-width)); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.segment, - .three-merch-cards.segment, - .four-merch-cards.segment { - grid-template-columns: repeat(2, minmax(276px, var(--consonant-merch-card-segment-width))); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.segment { - grid-template-columns: repeat(3, minmax(276px, var(--consonant-merch-card-segment-width))); - } - - .four-merch-cards.segment { - grid-template-columns: repeat(4, minmax(276px, var(--consonant-merch-card-segment-width))); - } -} - - -/* grid style for product */ -.one-merch-card.product, -.two-merch-cards.product, -.three-merch-cards.product, -.four-merch-cards.product { - grid-template-columns: var(--consonant-merch-card-product-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.product, - .three-merch-cards.product, - .four-merch-cards.product { - grid-template-columns: repeat(2, var(--consonant-merch-card-product-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.product, - .four-merch-cards.product { - grid-template-columns: repeat(3, var(--consonant-merch-card-product-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.product { - grid-template-columns: repeat(4, var(--consonant-merch-card-product-width)); - } -} - -/* grid style for twp */ -.one-merch-card.twp, -.two-merch-cards.twp, -.three-merch-cards.twp { - grid-template-columns: var(--consonant-merch-card-image-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .one-merch-card.twp, - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .one-merch-card.twp - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .one-merch-card.twp - .two-merch-cards.twp { - grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); - } - .three-merch-cards.twp { - grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); - } -} - -/* Mobile */ -@media screen and ${MOBILE_LANDSCAPE} { - .one-merch-card.twp, - .two-merch-cards.twp, - .three-merch-cards.twp { - grid-template-columns: repeat(1, var(--consonant-merch-card-twp-mobile-width)); - } -} - -/* grid style for inline-heading */ -.one-merch-card.inline-heading, -.two-merch-cards.inline-heading, -.three-merch-cards.inline-heading, -.four-merch-cards.inline-heading { - grid-template-columns: var(--consonant-merch-card-inline-heading-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.inline-heading, - .three-merch-cards.inline-heading, - .four-merch-cards.inline-heading { - grid-template-columns: repeat(2, var(--consonant-merch-card-inline-heading-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.inline-heading, - .four-merch-cards.inline-heading { - grid-template-columns: repeat(3, var(--consonant-merch-card-inline-heading-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.inline-heading { - grid-template-columns: repeat(4, var(--consonant-merch-card-inline-heading-width)); - } -} - -/* grid style for ccd-action */ -.one-merch-card.ccd-action, -.two-merch-cards.ccd-action, -.three-merch-cards.ccd-action, -.four-merch-cards.ccd-action { - grid-template-columns: var(--consonant-merch-card-ccd-action-width); -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.ccd-action, - .three-merch-cards.ccd-action, - .four-merch-cards.ccd-action { - grid-template-columns: repeat(2, var(--consonant-merch-card-ccd-action-width)); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .three-merch-cards.ccd-action, - .four-merch-cards.ccd-action { - grid-template-columns: repeat(3, var(--consonant-merch-card-ccd-action-width)); - } -} - -/* Large desktop */ - @media screen and ${LARGE_DESKTOP} { - .four-merch-cards.ccd-action { - grid-template-columns: repeat(4, var(--consonant-merch-card-ccd-action-width)); - } -} - -/* grid style for mini-compare-chart */ -.one-merch-card.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); - gap: var(--consonant-merch-spacing-xs); -} - -.two-merch-cards.mini-compare-chart, -.three-merch-cards.mini-compare-chart, -.four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-width)); - gap: var(--consonant-merch-spacing-xs); -} - -@media screen and ${MOBILE_LANDSCAPE} { - .two-merch-cards.mini-compare-chart, - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-width); - gap: var(--consonant-merch-spacing-xs); - } -} - -@media screen and ${TABLET_DOWN} { - .three-merch-cards.mini-compare-chart merch-card [slot="footer"] a, - .four-merch-cards.mini-compare-chart merch-card [slot="footer"] a { - flex: 1; - } -} - -/* Tablet */ -@media screen and ${TABLET_UP} { - .two-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); - gap: var(--consonant-merch-spacing-m); - } - - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); - } -} - -/* desktop */ -@media screen and ${DESKTOP_UP} { - .one-merch-card.mini-compare-chart { - grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); - } - - .two-merch-cards.mini-compare-chart { - grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-wide-width)); - gap: var(--consonant-merch-spacing-m); - } - - .three-merch-cards.mini-compare-chart, - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(3, var(--consonant-merch-card-mini-compare-chart-width)); - gap: var(--consonant-merch-spacing-m); - } -} - -@media screen and ${LARGE_DESKTOP} { - .four-merch-cards.mini-compare-chart { - grid-template-columns: repeat(4, var(--consonant-merch-card-mini-compare-chart-width)); - } -} - -/* mini-compare card footer rows */ -merch-card .footer-row-cell:nth-child(1) { - min-height: var(--consonant-merch-card-footer-row-1-min-height); -} - -merch-card .footer-row-cell:nth-child(2) { - min-height: var(--consonant-merch-card-footer-row-2-min-height); -} - -merch-card .footer-row-cell:nth-child(3) { - min-height: var(--consonant-merch-card-footer-row-3-min-height); -} - -merch-card .footer-row-cell:nth-child(4) { - min-height: var(--consonant-merch-card-footer-row-4-min-height); -} - -merch-card .footer-row-cell:nth-child(5) { - min-height: var(--consonant-merch-card-footer-row-5-min-height); -} - -merch-card .footer-row-cell:nth-child(6) { - min-height: var(--consonant-merch-card-footer-row-6-min-height); -} - -merch-card .footer-row-cell:nth-child(7) { - min-height: var(--consonant-merch-card-footer-row-7-min-height); -} - -merch-card .footer-row-cell:nth-child(8) { - min-height: var(--consonant-merch-card-footer-row-8-min-height); -} - span[is="inline-price"][data-template='strikethrough'] { text-decoration: line-through; } diff --git a/libs/features/mas/web-components/src/merch-card.css.js b/libs/features/mas/web-components/src/merch-card.css.js index 6599a6e153..bcc2081651 100644 --- a/libs/features/mas/web-components/src/merch-card.css.js +++ b/libs/features/mas/web-components/src/merch-card.css.js @@ -1,5 +1,5 @@ import { css, unsafeCSS } from 'lit'; -import { DESKTOP_UP, LARGE_DESKTOP, TABLET_UP, TABLET_DOWN } from './media.js'; +import { DESKTOP_UP, LARGE_DESKTOP, TABLET_UP, } from './media.js'; export const styles = css` :host { @@ -20,26 +20,6 @@ export const styles = css` visibility: hidden; } - :host([variant='special-offers']) { - min-height: 439px; - } - - :host([variant='catalog']) { - min-height: 330px; - } - - :host([variant='plans']) { - min-height: 348px; - } - - :host([variant='segment']) { - min-height: 214px; - } - - :host([variant='ccd-action']:not([size])) { - width: var(--consonant-merch-card-ccd-action-width); - } - :host([aria-selected]) { outline: none; box-sizing: border-box; @@ -61,10 +41,6 @@ export const styles = css` background-image: var(--ellipsis-icon); } - :host([variant='mini-compare-chart']) > slot:not([name='icons']) { - display: block; - } - .top-section { display: flex; justify-content: flex-start; @@ -129,19 +105,6 @@ export const styles = css` border-radius: 0 5px 5px 0; } - .body .catalog-badge { - display: flex; - height: fit-content; - flex-direction: column; - width: fit-content; - max-width: 140px; - border-radius: 5px; - position: relative; - top: 0; - margin-left: var(--consonant-merch-spacing-xxs); - box-sizing: border-box; - } - .detail-bg-container { right: 0; padding: var(--consonant-merch-spacing-xs); @@ -245,128 +208,10 @@ export const styles = css` margin-top: 2px; } - .twp-badge { - padding: 4px 10px 5px 10px; - } - - :host([aria-selected]) .twp-badge { - margin-inline-end: 2px; - padding-inline-end: 9px; - } - - :host([variant='twp']) { - padding: 4px 10px 5px 10px; - } - slot[name='icons'] { display: flex; gap: 8px; } - - :host([variant='twp']) ::slotted(merch-offer-select) { - display: none; - } - - :host([variant='twp']) .top-section { - flex: 0; - display: flex; - flex-direction: column; - justify-content: flex-start; - height: 100%; - gap: var(--consonant-merch-spacing-xxs); - padding: var(--consonant-merch-spacing-xs) - var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs) - var(--consonant-merch-spacing-xs); - align-items: flex-start; - } - - :host([variant='twp']) .body { - padding: 0 var(--consonant-merch-spacing-xs); - } - - :host([variant='twp']) footer { - gap: var(--consonant-merch-spacing-xxs); - flex-direction: column; - align-self: flex-start; - } - - :host([variant='special-offers'].center) { - text-align: center; - } - - /* plans */ - :host([variant='plans']) { - min-height: 348px; - } - - :host([variant='mini-compare-chart']) footer { - min-height: var(--consonant-merch-card-mini-compare-footer-height); - padding: var(--consonant-merch-spacing-xs); - } - - /* mini-compare card */ - :host([variant='mini-compare-chart']) .top-section { - padding-top: var(--consonant-merch-spacing-s); - padding-inline-start: var(--consonant-merch-spacing-s); - height: var(--consonant-merch-card-mini-compare-top-section-height); - } - - @media screen and ${unsafeCSS(TABLET_DOWN)} { - [class*'-merch-cards'] :host([variant='mini-compare-chart']) footer { - flex-direction: column; - align-items: stretch; - text-align: center; - } - } - - @media screen and ${unsafeCSS(DESKTOP_UP)} { - :host([variant='mini-compare-chart']) footer { - padding: var(--consonant-merch-spacing-xs) - var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-s) - var(--consonant-merch-spacing-s); - } - } - - :host([variant='mini-compare-chart']) slot[name='footer-rows'] { - flex: 1; - display: flex; - flex-direction: column; - justify-content: end; - } - /* mini-compare card heights for the slots: heading-m, body-m, heading-m-price, price-commitment, offers, promo-text, footer */ - :host([variant='mini-compare-chart']) slot[name='heading-m'] { - min-height: var(--consonant-merch-card-mini-compare-heading-m-height); - } - :host([variant='mini-compare-chart']) slot[name='body-m'] { - min-height: var(--consonant-merch-card-mini-compare-body-m-height); - } - :host([variant='mini-compare-chart']) slot[name='heading-m-price'] { - min-height: var( - --consonant-merch-card-mini-compare-heading-m-price-height - ); - } - :host([variant='mini-compare-chart']) slot[name='price-commitment'] { - min-height: var( - --consonant-merch-card-mini-compare-price-commitment-height - ); - } - :host([variant='mini-compare-chart']) slot[name='offers'] { - min-height: var(--consonant-merch-card-mini-compare-offers-height); - } - :host([variant='mini-compare-chart']) slot[name='promo-text'] { - min-height: var(--consonant-merch-card-mini-compare-promo-text-height); - } - :host([variant='mini-compare-chart']) slot[name='callout-content'] { - min-height: var( - --consonant-merch-card-mini-compare-callout-content-height - ); - } - - :host([variant='plans']) ::slotted([slot='heading-xs']), - :host([variant='segment']) ::slotted([slot='heading-xs']) { - max-width: var(--consonant-merch-card-heading-xs-max-width, 100%); - } `; export const sizeStyles = () => { diff --git a/libs/features/mas/web-components/src/merch-card.js b/libs/features/mas/web-components/src/merch-card.js index 025be0ef1c..92136552a1 100644 --- a/libs/features/mas/web-components/src/merch-card.js +++ b/libs/features/mas/web-components/src/merch-card.js @@ -1,6 +1,7 @@ -import { html, LitElement, nothing } from 'lit'; +import { LitElement } from 'lit'; import { sizeStyles, styles } from './merch-card.css.js'; -import { isMobile, isMobileOrTablet } from './utils.js'; +import { getVariantLayout, getVariantStyles } from './variants/variants.js'; + import './global.css.js'; import { @@ -8,19 +9,11 @@ import { EVENT_MERCH_OFFER_SELECT_READY, EVENT_MERCH_QUANTITY_SELECTOR_CHANGE, EVENT_MERCH_STORAGE_CHANGE, - EVENT_MERCH_CARD_ACTION_MENU_TOGGLE, } from './constants.js'; export const MERCH_CARD_NODE_NAME = 'MERCH-CARD'; export const MERCH_CARD = 'merch-card'; -const FOOTER_ROW_MIN_HEIGHT = 32; // as per the XD. - -const MINI_COMPARE_CHART = 'mini-compare-chart'; - -const getRowMinHeightPropertyName = (index) => - `--consonant-merch-card-footer-row-${index}-min-height`; - export class MerchCard extends LitElement { static properties = { name: { type: String, attribute: 'name', reflect: true }, @@ -89,10 +82,11 @@ export class MerchCard extends LitElement { merchOffer: { type: Object }, }; - static styles = [styles, ...sizeStyles()]; + static styles = [styles, getVariantStyles(), ...sizeStyles()]; customerSegment; marketSegment; + variantLayout; constructor() { super(); @@ -101,8 +95,6 @@ export class MerchCard extends LitElement { this.selected = false; } - #container; - updated(changedProperties) { if ( changedProperties.has('badgeBackgroundColor') || @@ -112,23 +104,22 @@ export class MerchCard extends LitElement { } this.updateComplete.then(async () => { const allPrices = Array.from( - this.querySelectorAll('span[is="inline-price"][data-wcs-osi]'), + this.querySelectorAll('span[is="inline-price"][data-wcs-osi]'), ); // Filter out prices within the callout-content slot const prices = allPrices.filter( (price) => !price.closest('[slot="callout-content"]'), ); await Promise.all(prices.map((price) => price.onceSettled())); - this.adjustTitleWidth(); - if (!isMobile()) { - this.adjustMiniCompareBodySlots(); - this.adjustMiniCompareFooterRows(); - } else { - this.removeEmptyRows(); - } + this.variantLayout.postCardUpdateHook(this); }); } + render() { + if (!this.isConnected || this.style.display === 'none') return; + return this.variantLayout.renderLayout(); + } + get computedBorderStyle() { if (this.variant !== 'twp') { return `1px solid ${ @@ -138,76 +129,10 @@ export class MerchCard extends LitElement { return ''; } - get evergreen() { - return this.classList.contains('intro-pricing'); - } - - get stockCheckbox() { - return this.checkboxLabel - ? html`` - : ''; - } - - get cardImage() { - return html`
- - ${this.badge} -
`; - } - - get secureLabelFooter() { - const secureLabel = this.secureLabel - ? html`${this.secureLabel}` - : ''; - return html`
${secureLabel}
`; - } - - get miniCompareFooter() { - const secureLabel = this.secureLabel - ? html` - ${this.secureLabel}` - : html``; - return html`
${secureLabel}
`; - } - - get badge() { - let additionalStyles; - if (!this.badgeBackgroundColor || !this.badgeColor || !this.badgeText) { - return; - } - if (this.evergreen) { - additionalStyles = `border: 1px solid ${this.badgeBackgroundColor}; border-right: none;`; - } - return html` -
- ${this.badgeText} -
- `; - } - get badgeElement() { return this.shadowRoot.getElementById('badge'); } - getContainer() { - return this.closest('[class*="-merch-cards"]') ?? this.parentElement; - } - get headingmMSlot() { return this.shadowRoot .querySelector('slot[name="heading-m"]') @@ -252,27 +177,6 @@ export class MerchCard extends LitElement { } } - toggleActionMenu(e) { - const retract = e?.type === 'mouseleave' ? true : undefined; - const actionMenuContentSlot = this.shadowRoot.querySelector( - 'slot[name="action-menu-content"]', - ); - if (!actionMenuContentSlot) return; - if (!retract) { - this.dispatchEvent( - new CustomEvent(EVENT_MERCH_CARD_ACTION_MENU_TOGGLE, { - bubbles: true, - composed: true, - detail: { - card: this.name, - type: 'action-menu', - }, - }), - ); - } - actionMenuContentSlot.classList.toggle('hidden', retract); - } - handleQuantitySelection(event) { const elements = this.checkoutLinks; for (const element of elements) { @@ -280,12 +184,8 @@ export class MerchCard extends LitElement { } } - get titleElement() { - const heading = - this.variant === 'special-offers' - ? this.querySelector('[slot="detail-m"]') - : this.querySelector('[slot="heading-xs"]'); - return heading; + get titleElement() { + return this.querySelector(this.variantLayout?.headingSelector || '.card-heading'); } get title() { @@ -321,225 +221,15 @@ export class MerchCard extends LitElement { return this.textContent.match(new RegExp(text, 'i')) !== null; } - render() { - if (!this.isConnected || this.style.display === 'none') return; - switch (this.variant) { - case 'special-offers': - return this.renderSpecialOffer(); - case 'segment': - return this.renderSegment(); - case 'plans': - return this.renderPlans(); - case 'catalog': - return this.renderCatalog(); - case 'image': - return this.renderImage(); - case 'product': - return this.renderProduct(); - case 'inline-heading': - return this.renderInlineHeading(); - case MINI_COMPARE_CHART: - return this.renderMiniCompareChart(); - case 'ccd-action': - return this.renderCcdAction(); - case 'twp': - return this.renderTwp(); - default: - // this line should never hit, to check. - return this.renderProduct(); - } - } - - renderSpecialOffer() { - return html`${this.cardImage} -
- - - -
- ${this.evergreen - ? html` -
- -
- ` - : html` -
- ${this.secureLabelFooter} - `} - `; - } - - get promoBottom() { - return this.classList.contains('promo-bottom'); - } - get startingAt() { return this.classList.contains('starting-at'); } - renderSegment() { - return html` ${this.badge} -
- - - ${!this.promoBottom ? html`` : ''} - - ${this.promoBottom ? html`` : ''} -
-
- ${this.secureLabelFooter}`; - } - - renderPlans() { - return html` ${this.badge} -
- - - - - ${!this.promoBottom ? html` ` : ''} - - ${this.promoBottom ? html` ` : ''} - ${this.stockCheckbox} -
- - ${this.secureLabelFooter}`; - } - - renderCatalog() { - return html`
-
- ${this.badge} -
-
- ${this.actionMenuContent} - - - - ${!this.promoBottom - ? html`` - : ''} - - ${this.promoBottom - ? html`` - : ''} -
- ${this.secureLabelFooter}`; - } - - renderImage() { - return html`${this.cardImage} -
- - - - ${this.promoBottom ? html`` : html``} -
- ${this.evergreen - ? html` -
- -
- ` - : html` -
- ${this.secureLabelFooter} - `}`; - } - - renderInlineHeading() { - return html` ${this.badge} -
-
- - -
- -
- ${!this.customHr ? html`
` : ''} ${this.secureLabelFooter}`; - } - - renderProduct() { - return html` ${this.badge} -
- - - - ${!this.promoBottom ? html`` : ''} - - ${this.promoBottom ? html`` : ''} -
- ${this.secureLabelFooter}`; - } - - renderMiniCompareChart() { - // Define the HTML structure for the 'mini-compare-chart' variant here - const { badge } = this; - return html`
- ${badge} -
- - - - - - - - - ${this.miniCompareFooter} - `; - } - - renderTwp() { - return html`${this.badge} -
- - - -
-
- -
-
`; - } - - renderCcdAction() { - return html`
- ${this.badge} - - - ${this.promoBottom ? html`` : html``} -
- -
`; - } - connectedCallback() { super.connectedCallback(); - this.#container = this.getContainer(); + this.variantLayout = getVariantLayout(this); + this.variantLayout.connectedCallbackHook(); this.setAttribute('tabindex', this.getAttribute('tabindex') ?? '0'); - this.addEventListener('mouseleave', this.toggleActionMenu); this.addEventListener( EVENT_MERCH_QUANTITY_SELECTOR_CHANGE, this.handleQuantitySelection, @@ -560,6 +250,7 @@ export class MerchCard extends LitElement { disconnectedCallback() { super.disconnectedCallback(); + this.variantLayout.disconnectedCallbackHook(); this.removeEventListener( EVENT_MERCH_QUANTITY_SELECTOR_CHANGE, @@ -570,119 +261,8 @@ export class MerchCard extends LitElement { this.handleStorageChange, ); } - // custom methods - updateMiniCompareElementMinHeight(el, name) { - const elMinHeightPropertyName = `--consonant-merch-card-mini-compare-${name}-height`; - const height = Math.max( - 0, - parseInt(window.getComputedStyle(el).height) || 0, - ); - const maxMinHeight = - parseInt( - this.#container.style.getPropertyValue(elMinHeightPropertyName), - ) || 0; - if (height > maxMinHeight) { - this.#container.style.setProperty( - elMinHeightPropertyName, - `${height}px`, - ); - } - } - - async adjustTitleWidth() { - if (!['segment', 'plans'].includes(this.variant)) return; - const cardWidth = this.getBoundingClientRect().width; - const badgeWidth = - this.badgeElement?.getBoundingClientRect().width || 0; - if (cardWidth === 0 || badgeWidth === 0) return; - this.style.setProperty( - '--consonant-merch-card-heading-xs-max-width', - `${Math.round(cardWidth - badgeWidth - 16)}px`, // consonant-merch-spacing-xs - ); - } - - async adjustMiniCompareBodySlots() { - if (this.variant !== MINI_COMPARE_CHART) return; - if (this.getBoundingClientRect().width === 0) return; - - this.updateMiniCompareElementMinHeight( - this.shadowRoot.querySelector('.top-section'), - 'top-section', - ); - - const slots = [ - 'heading-m', - 'body-m', - 'heading-m-price', - 'price-commitment', - 'offers', - 'promo-text', - 'callout-content', - 'secure-transaction-label', - ]; - - slots.forEach((slot) => - this.updateMiniCompareElementMinHeight( - this.shadowRoot.querySelector(`slot[name="${slot}"]`), - slot, - ), - ); - this.updateMiniCompareElementMinHeight( - this.shadowRoot.querySelector('footer'), - 'footer', - ); - - const badge = this.shadowRoot.querySelector( - '.mini-compare-chart-badge', - ); - if (badge && badge.textContent !== '') { - this.#container.style.setProperty( - '--consonant-merch-card-mini-compare-top-section-mobile-height', - '32px', - ); - } - } - - adjustMiniCompareFooterRows() { - if (this.variant !== MINI_COMPARE_CHART) return; - if (this.getBoundingClientRect().width === 0) return; - const footerRows = this.querySelector('[slot="footer-rows"]'); - [...footerRows.children].forEach((el, index) => { - const height = Math.max( - FOOTER_ROW_MIN_HEIGHT, - parseInt(window.getComputedStyle(el).height) || 0, - ); - const maxMinHeight = - parseInt( - this.#container.style.getPropertyValue( - getRowMinHeightPropertyName(index + 1), - ), - ) || 0; - if (height > maxMinHeight) { - this.#container.style.setProperty( - getRowMinHeightPropertyName(index + 1), - `${height}px`, - ); - } - }); - } - - removeEmptyRows() { - if (this.variant !== MINI_COMPARE_CHART) return; - const footerRows = this.querySelectorAll('.footer-row-cell'); - footerRows.forEach((row) => { - const rowDescription = row.querySelector('.footer-row-cell-description'); - if (rowDescription) { - const isEmpty = !rowDescription.textContent.trim(); - if (isEmpty) { - row.remove(); - } - } - }); - } - get storageOptions() { return this.querySelector('sp-radio-group#storage'); } diff --git a/libs/features/mas/web-components/src/variants/catalog.css.js b/libs/features/mas/web-components/src/variants/catalog.css.js new file mode 100644 index 0000000000..db23735641 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/catalog.css.js @@ -0,0 +1,89 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP,} from '../media.js'; + +export const CSS = ` +:root { + --consonant-merch-card-catalog-width: 276px; + --consonant-merch-card-catalog-icon-size: 40px; +} +.one-merch-card.catalog, +.two-merch-cards.catalog, +.three-merch-cards.catalog, +.four-merch-cards.catalog { + grid-template-columns: var(--consonant-merch-card-catalog-width); +} + +@media screen and ${TABLET_UP} { + :root { + --consonant-merch-card-catalog-width: 302px; + } + + .two-merch-cards.catalog, + .three-merch-cards.catalog, + .four-merch-cards.catalog { + grid-template-columns: repeat(2, var(--consonant-merch-card-catalog-width)); + } +} + +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-catalog-width: 276px; + } + + .three-merch-cards.catalog, + .four-merch-cards.catalog { + grid-template-columns: repeat(3, var(--consonant-merch-card-catalog-width)); + } +} + +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.catalog { + grid-template-columns: repeat(4, var(--consonant-merch-card-catalog-width)); + } +} + +merch-card[variant="catalog"] [slot="action-menu-content"] { + background-color: #000; + color: var(--color-white, #fff); + font-size: var(--consonant-merch-card-body-xs-font-size); + width: fit-content; + padding: var(--consonant-merch-spacing-xs); + border-radius: var(--consonant-merch-spacing-xxxs); + position: absolute; + top: 55px; + right: 15px; + line-height: var(--consonant-merch-card-body-line-height); +} + +merch-card[variant="catalog"] [slot="action-menu-content"] ul { + padding-left: 0; + padding-bottom: var(--consonant-merch-spacing-xss); + margin-top: 0; + margin-bottom: 0; + list-style-position: inside; + list-style-type: '• '; +} + +merch-card[variant="catalog"] [slot="action-menu-content"] ul li { + padding-left: 0; + line-height: var(--consonant-merch-card-body-line-height); +} + +merch-card[variant="catalog"] [slot="action-menu-content"] ::marker { + margin-right: 0; +} + +merch-card[variant="catalog"] [slot="action-menu-content"] p { + color: var(--color-white, #fff); +} + +merch-card[variant="catalog"] [slot="action-menu-content"] a { + color: var(--consonant-merch-card-background-color); + text-decoration: underline; +} + +merch-card[variant="catalog"] .payment-details { + font-size: var(--consonant-merch-card-body-font-size); + font-style: italic; + font-weight: 400; + line-height: var(--consonant-merch-card-body-line-height); +}`; diff --git a/libs/features/mas/web-components/src/variants/catalog.js b/libs/features/mas/web-components/src/variants/catalog.js new file mode 100644 index 0000000000..8a9f80162c --- /dev/null +++ b/libs/features/mas/web-components/src/variants/catalog.js @@ -0,0 +1,97 @@ +import { VariantLayout } from './variant-layout.js'; +import { html, css } from 'lit'; +import { isMobileOrTablet } from '../utils.js'; +import { EVENT_MERCH_CARD_ACTION_MENU_TOGGLE } from '../constants.js'; +import { CSS } from './catalog.css.js'; + +export class Catalog extends VariantLayout { + constructor(card) { + super(card); + } + + renderLayout() { + return html`
+
+ ${this.badge} +
+
+ ${this.card.actionMenuContent} + + + + ${!this.promoBottom + ? html`` + : ''} + + ${this.promoBottom + ? html`` + : ''} +
+ ${this.secureLabelFooter}`; + } + + getGlobalCSS() { + return CSS; + } + + toggleActionMenu = (e) => { + //beware this is an event on card, so this points to the card, not the layout + const retract = e?.type === 'mouseleave' ? true : undefined; + const actionMenuContentSlot = this.card.shadowRoot.querySelector( + 'slot[name="action-menu-content"]', + ); + if (!actionMenuContentSlot) return; + if (!retract) { + this.card.dispatchEvent( + new CustomEvent(EVENT_MERCH_CARD_ACTION_MENU_TOGGLE, { + bubbles: true, + composed: true, + detail: { + card: this.card.name, + type: 'action-menu', + }, + }), + ); + } + actionMenuContentSlot.classList.toggle('hidden', retract); + } + + connectedCallbackHook() { + this.card.addEventListener('mouseleave', this.toggleActionMenu); + } + disconnectedCallbackHook() { + this.card.removeEventListener('mouseleave', this.toggleActionMenu); + } + static variantStyle = css` + :host([variant='catalog']) { + min-height: 330px; + } + + .body .catalog-badge { + display: flex; + height: fit-content; + flex-direction: column; + width: fit-content; + max-width: 140px; + border-radius: 5px; + position: relative; + top: 0; + margin-left: var(--consonant-merch-spacing-xxs); + box-sizing: border-box; + } + `; +} diff --git a/libs/features/mas/web-components/src/variants/ccd-action.css.js b/libs/features/mas/web-components/src/variants/ccd-action.css.js new file mode 100644 index 0000000000..1bae9d8f3c --- /dev/null +++ b/libs/features/mas/web-components/src/variants/ccd-action.css.js @@ -0,0 +1,40 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP } from "../media.js"; + +export const CSS = ` +:root { + --consonant-merch-card-ccd-action-width: 276px; + --consonant-merch-card-ccd-action-min-height: 320px; +} + +.one-merch-card.ccd-action, +.two-merch-cards.ccd-action, +.three-merch-cards.ccd-action, +.four-merch-cards.ccd-action { + grid-template-columns: var(--consonant-merch-card-ccd-action-width); +} + +merch-card[variant="ccd-action"] .price-strikethrough { + font-size: 18px; +} + +@media screen and ${TABLET_UP} { + .two-merch-cards.ccd-action, + .three-merch-cards.ccd-action, + .four-merch-cards.ccd-action { + grid-template-columns: repeat(2, var(--consonant-merch-card-ccd-action-width)); + } +} + +@media screen and ${DESKTOP_UP} { + .three-merch-cards.ccd-action, + .four-merch-cards.ccd-action { + grid-template-columns: repeat(3, var(--consonant-merch-card-ccd-action-width)); + } +} + +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.ccd-action { + grid-template-columns: repeat(4, var(--consonant-merch-card-ccd-action-width)); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/ccd-action.js b/libs/features/mas/web-components/src/variants/ccd-action.js new file mode 100644 index 0000000000..5fb02cc9a5 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/ccd-action.js @@ -0,0 +1,29 @@ +import { VariantLayout } from './variant-layout.js'; +import { html, css } from 'lit'; +import { CSS } from './ccd-action.css.js'; + +export class CCDAction extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + renderLayout() { + return html`
+ ${this.badge} + + + ${this.promoBottom ? html`` : html``} +
+ +
`; + } + static variantStyle = css` + :host([variant='ccd-action']:not([size])) { + width: var(--consonant-merch-card-ccd-action-width); + } + `; +} diff --git a/libs/features/mas/web-components/src/variants/image.css.js b/libs/features/mas/web-components/src/variants/image.css.js new file mode 100644 index 0000000000..30551c0a84 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/image.css.js @@ -0,0 +1,38 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP,} from '../media.js'; +export const CSS = ` +:root { + --consonant-merch-card-image-width: 300px; +} + +.one-merch-card.image, +.two-merch-cards.image, +.three-merch-cards.image, +.four-merch-cards.image { + grid-template-columns: var(--consonant-merch-card-image-width); +} + +@media screen and ${TABLET_UP} { + .two-merch-cards.image, + .three-merch-cards.image, + .four-merch-cards.image { + grid-template-columns: repeat(2, var(--consonant-merch-card-image-width)); + } +} + +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-image-width: 378px; + } + + .three-merch-cards.image, + .four-merch-cards.image { + grid-template-columns: repeat(3, var(--consonant-merch-card-image-width)); + } +} + +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.image { + grid-template-columns: repeat(4, var(--consonant-merch-card-image-width)); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/image.js b/libs/features/mas/web-components/src/variants/image.js new file mode 100644 index 0000000000..dcc5599bd3 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/image.js @@ -0,0 +1,36 @@ +import { VariantLayout } from "./variant-layout.js"; +import { html } from 'lit'; +import { CSS } from './image.css.js'; + +export class Image extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + renderLayout() { + return html`${this.cardImage} +
+ + + + ${this.promoBottom ? html`` : html``} +
+ ${this.evergreen + ? html` +
+ +
+ ` + : html` +
+ ${this.secureLabelFooter} + `}`; + } +} diff --git a/libs/features/mas/web-components/src/variants/inline-heading.css.js b/libs/features/mas/web-components/src/variants/inline-heading.css.js new file mode 100644 index 0000000000..7bb9c25a1e --- /dev/null +++ b/libs/features/mas/web-components/src/variants/inline-heading.css.js @@ -0,0 +1,39 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP,} from '../media.js'; + +export const CSS = ` +:root { + --consonant-merch-card-inline-heading-width: 300px; +} + +.one-merch-card.inline-heading, +.two-merch-cards.inline-heading, +.three-merch-cards.inline-heading, +.four-merch-cards.inline-heading { + grid-template-columns: var(--consonant-merch-card-inline-heading-width); +} + +@media screen and ${TABLET_UP} { + .two-merch-cards.inline-heading, + .three-merch-cards.inline-heading, + .four-merch-cards.inline-heading { + grid-template-columns: repeat(2, var(--consonant-merch-card-inline-heading-width)); + } +} + +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-inline-heading-width: 378px; + } + + .three-merch-cards.inline-heading, + .four-merch-cards.inline-heading { + grid-template-columns: repeat(3, var(--consonant-merch-card-inline-heading-width)); + } +} + +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.inline-heading { + grid-template-columns: repeat(4, var(--consonant-merch-card-inline-heading-width)); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/inline-heading.js b/libs/features/mas/web-components/src/variants/inline-heading.js new file mode 100644 index 0000000000..a038819f3c --- /dev/null +++ b/libs/features/mas/web-components/src/variants/inline-heading.js @@ -0,0 +1,24 @@ +import { VariantLayout } from './variant-layout'; +import { html } from 'lit'; +import { CSS } from './inline-heading.css.js' +export class InlineHeading extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + renderLayout() { + return html` ${this.badge} +
+
+ + +
+ +
+ ${!this.card.customHr ? html`
` : ''} ${this.secureLabelFooter}`; + } +} diff --git a/libs/features/mas/web-components/src/variants/mini-compare-chart.css.js b/libs/features/mas/web-components/src/variants/mini-compare-chart.css.js new file mode 100644 index 0000000000..167102700d --- /dev/null +++ b/libs/features/mas/web-components/src/variants/mini-compare-chart.css.js @@ -0,0 +1,253 @@ +import { MOBILE_LANDSCAPE, TABLET_DOWN, TABLET_UP, DESKTOP_UP, LARGE_DESKTOP } from "../media.js"; +export const CSS = ` + :root { + --consonant-merch-card-mini-compare-chart-icon-size: 32px; + --consonant-merch-card-mini-compare-mobile-cta-font-size: 15px; + --consonant-merch-card-mini-compare-mobile-cta-width: 75px; + --consonant-merch-card-mini-compare-badge-mobile-max-width: 50px; + } + + merch-card[variant="mini-compare-chart"] [slot="heading-m"] { + padding: 0 var(--consonant-merch-spacing-s) 0; + } + + merch-card[variant="mini-compare-chart"] [slot="body-m"] { + padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); + } + + merch-card[variant="mini-compare-chart"] [is="inline-price"] { + display: inline-block; + min-height: 30px; + min-width: 1px; + } + + merch-card[variant="mini-compare-chart"] [slot='callout-content'] { + padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0px; + } + + merch-card[variant="mini-compare-chart"] [slot='callout-content'] [is="inline-price"] { + min-height: unset; + } + + merch-card[variant="mini-compare-chart"] [slot="price-commitment"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + padding: 0 var(--consonant-merch-spacing-s); + } + + merch-card[variant="mini-compare-chart"] [slot="price-commitment"] a { + display: inline-block; + height: 27px; + } + + merch-card[variant="mini-compare-chart"] [slot="offers"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + } + + merch-card[variant="mini-compare-chart"] [slot="body-xxs"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; + } + + merch-card[variant="mini-compare-chart"] [slot="promo-text"] { + font-size: var(--consonant-merch-card-body-m-font-size); + padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s) 0; + } + + merch-card[variant="mini-compare-chart"] [slot="promo-text"] a { + text-decoration: underline; + } + + merch-card[variant="mini-compare-chart"] .footer-row-icon { + display: flex; + place-items: center; + } + + merch-card[variant="mini-compare-chart"] .footer-row-icon img { + max-width: initial; + width: var(--consonant-merch-card-mini-compare-chart-icon-size); + height: var(--consonant-merch-card-mini-compare-chart-icon-size); + } + + merch-card[variant="mini-compare-chart"] .footer-row-cell { + border-top: 1px solid var(--consonant-merch-card-border-color); + display: flex; + gap: var(--consonant-merch-spacing-xs); + justify-content: start; + place-items: center; + padding: var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-s); + } + + merch-card[variant="mini-compare-chart"] .footer-row-cell-description { + font-size: var(--consonant-merch-card-body-s-font-size); + line-height: var(--consonant-merch-card-body-s-line-height); + } + + merch-card[variant="mini-compare-chart"] .footer-row-cell-description p { + color: var(--merch-color-grey-80); + vertical-align: bottom; + } + + merch-card[variant="mini-compare-chart"] .footer-row-cell-description a { + color: var(--color-accent); + text-decoration: solid; + } + +.one-merch-card.mini-compare-chart { + grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); + gap: var(--consonant-merch-spacing-xs); +} + +.two-merch-cards.mini-compare-chart, +.three-merch-cards.mini-compare-chart, +.four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-width)); + gap: var(--consonant-merch-spacing-xs); +} + +/* mini compare mobile */ +@media screen and ${MOBILE_LANDSCAPE} { + :root { + --consonant-merch-card-mini-compare-chart-width: 302px; + --consonant-merch-card-mini-compare-chart-wide-width: 302px; + } + + .two-merch-cards.mini-compare-chart, + .three-merch-cards.mini-compare-chart, + .four-merch-cards.mini-compare-chart { + grid-template-columns: var(--consonant-merch-card-mini-compare-chart-width); + gap: var(--consonant-merch-spacing-xs); + } + + merch-card[variant="mini-compare-chart"] [slot='heading-m'] { + font-size: var(--consonant-merch-card-body-s-font-size); + line-height: var(--consonant-merch-card-body-s-line-height); + } + + merch-card[variant="mini-compare-chart"] [slot='heading-m-price'] { + font-size: var(--consonant-merch-card-body-s-font-size); + line-height: var(--consonant-merch-card-body-s-line-height); + } + + merch-card[variant="mini-compare-chart"] [slot='body-m'] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + } + + merch-card[variant="mini-compare-chart"] [slot="promo-text"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + } + merch-card[variant="mini-compare-chart"] .footer-row-cell-description { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + } +} + +@media screen and ${TABLET_DOWN} { + .three-merch-cards.mini-compare-chart merch-card [slot="footer"] a, + .four-merch-cards.mini-compare-chart merch-card [slot="footer"] a { + flex: 1; + } + + merch-card[variant="mini-compare-chart"] [slot='heading-m'] { + font-size: var(--consonant-merch-card-body-s-font-size); + line-height: var(--consonant-merch-card-body-s-line-height); + } + + merch-card[variant="mini-compare-chart"] [slot='heading-m-price'] { + font-size: var(--consonant-merch-card-body-s-font-size); + line-height: var(--consonant-merch-card-body-s-line-height); + } + + merch-card[variant="mini-compare-chart"] [slot='body-m'] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + } + + merch-card[variant="mini-compare-chart"] [slot="promo-text"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + } + + merch-card[variant="mini-compare-chart"] .footer-row-cell-description { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + } +} +@media screen and ${TABLET_UP} { + :root { + --consonant-merch-card-mini-compare-chart-width: 302px; + --consonant-merch-card-mini-compare-chart-wide-width: 302px; + } + + .two-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); + gap: var(--consonant-merch-spacing-m); + } + + .three-merch-cards.mini-compare-chart, + .four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, minmax(var(--consonant-merch-card-mini-compare-chart-width), var(--consonant-merch-card-mini-compare-chart-wide-width))); + } +} + +/* desktop */ +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-mini-compare-chart-width: 378px; + --consonant-merch-card-mini-compare-chart-wide-width: 484px; + } + .one-merch-card.mini-compare-chart { + grid-template-columns: var(--consonant-merch-card-mini-compare-chart-wide-width); + } + + .two-merch-cards.mini-compare-chart { + grid-template-columns: repeat(2, var(--consonant-merch-card-mini-compare-chart-wide-width)); + gap: var(--consonant-merch-spacing-m); + } + + .three-merch-cards.mini-compare-chart, + .four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(3, var(--consonant-merch-card-mini-compare-chart-width)); + gap: var(--consonant-merch-spacing-m); + } +} + +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.mini-compare-chart { + grid-template-columns: repeat(4, var(--consonant-merch-card-mini-compare-chart-width)); + } +} + +merch-card .footer-row-cell:nth-child(1) { + min-height: var(--consonant-merch-card-footer-row-1-min-height); +} + +merch-card .footer-row-cell:nth-child(2) { + min-height: var(--consonant-merch-card-footer-row-2-min-height); +} + +merch-card .footer-row-cell:nth-child(3) { + min-height: var(--consonant-merch-card-footer-row-3-min-height); +} + +merch-card .footer-row-cell:nth-child(4) { + min-height: var(--consonant-merch-card-footer-row-4-min-height); +} + +merch-card .footer-row-cell:nth-child(5) { + min-height: var(--consonant-merch-card-footer-row-5-min-height); +} + +merch-card .footer-row-cell:nth-child(6) { + min-height: var(--consonant-merch-card-footer-row-6-min-height); +} + +merch-card .footer-row-cell:nth-child(7) { + min-height: var(--consonant-merch-card-footer-row-7-min-height); +} + +merch-card .footer-row-cell:nth-child(8) { + min-height: var(--consonant-merch-card-footer-row-8-min-height); +} +`; diff --git a/libs/features/mas/web-components/src/variants/mini-compare-chart.js b/libs/features/mas/web-components/src/variants/mini-compare-chart.js new file mode 100644 index 0000000000..b72a092367 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/mini-compare-chart.js @@ -0,0 +1,223 @@ +import { html, css, unsafeCSS } from 'lit'; +import { isMobile } from '../utils.js'; +import { VariantLayout } from './variant-layout.js'; +import { CSS } from './mini-compare-chart.css.js'; +import { DESKTOP_UP, TABLET_DOWN } from '../media.js'; +const FOOTER_ROW_MIN_HEIGHT = 32; // as per the XD. + +export class MiniCompareChart extends VariantLayout { + constructor(card) { + super(card); + } + + #container; + + getRowMinHeightPropertyName = (index) => + `--consonant-merch-card-footer-row-${index}-min-height`; + + getContainer() { + this.#container = this.#container ?? this.card.closest('[class*="-merch-cards"]') ?? this.card.parentElement; + return this.#container; + } + + getGlobalCSS() { + return CSS; + } + + getMiniCompareFooter = () => { + const secureLabel = this.card.secureLabel + ? html` + ${this.card.secureLabel}` + : html``; + return html`
${secureLabel}
`; + } + + updateMiniCompareElementMinHeight (el, name) { + const elMinHeightPropertyName = `--consonant-merch-card-mini-compare-${name}-height`; + const height = Math.max( + 0, + parseInt(window.getComputedStyle(el).height) || 0, + ); + const maxMinHeight = + parseInt( + this.getContainer().style.getPropertyValue(elMinHeightPropertyName), + ) || 0; + if (height > maxMinHeight) { + this.getContainer().style.setProperty( + elMinHeightPropertyName, + `${height}px`, + ); + } + } + + adjustMiniCompareBodySlots () { + if (this.card.getBoundingClientRect().width === 0) return; + + this.updateMiniCompareElementMinHeight( + this.card.shadowRoot.querySelector('.top-section'), + 'top-section', + ); + + const slots = [ + 'heading-m', + 'body-m', + 'heading-m-price', + 'price-commitment', + 'offers', + 'promo-text', + 'callout-content', + 'secure-transaction-label', + ]; + + slots.forEach((slot) => + this.updateMiniCompareElementMinHeight( + this.card.shadowRoot.querySelector(`slot[name="${slot}"]`), + slot, + ), + ); + this.updateMiniCompareElementMinHeight( + this.card.shadowRoot.querySelector('footer'), + 'footer', + ); + + const badge = this.card.shadowRoot.querySelector( + '.mini-compare-chart-badge', + ); + if (badge && badge.textContent !== '') { + this.getContainer().style.setProperty( + '--consonant-merch-card-mini-compare-top-section-mobile-height', + '32px', + ); + } + } + adjustMiniCompareFooterRows () { + if (this.card.getBoundingClientRect().width === 0) return; + const footerRows = this.card.querySelector('[slot="footer-rows"]'); + [...footerRows?.children].forEach((el, index) => { + const height = Math.max( + FOOTER_ROW_MIN_HEIGHT, + parseInt(window.getComputedStyle(el).height) || 0, + ); + const maxMinHeight = + parseInt( + this.getContainer().style.getPropertyValue( + this.getRowMinHeightPropertyName(index + 1), + ), + ) || 0; + if (height > maxMinHeight) { + this.getContainer().style.setProperty( + this.getRowMinHeightPropertyName(index + 1), + `${height}px`, + ); + } + }); + } + + removeEmptyRows() { + const footerRows = this.card.querySelectorAll('.footer-row-cell'); + footerRows.forEach((row) => { + const rowDescription = row.querySelector('.footer-row-cell-description'); + if (rowDescription) { + const isEmpty = !rowDescription.textContent.trim(); + if (isEmpty) { + row.remove(); + } + } + }); + } + + renderLayout () { + return html`
+ ${this.badge} +
+ + + + + + + + + ${this.getMiniCompareFooter()} + `; + } + postCardUpdateHook() { + if (!isMobile()) { + this.adjustMiniCompareBodySlots(); + this.adjustMiniCompareFooterRows(); + } else { + this.removeEmptyRows(); + } + } + static variantStyle = css` + :host([variant='mini-compare-chart']) > slot:not([name='icons']) { + display: block; + } + :host([variant='mini-compare-chart']) footer { + min-height: var(--consonant-merch-card-mini-compare-footer-height); + padding: var(--consonant-merch-spacing-xs); + } + + /* mini-compare card */ + :host([variant='mini-compare-chart']) .top-section { + padding-top: var(--consonant-merch-spacing-s); + padding-inline-start: var(--consonant-merch-spacing-s); + height: var(--consonant-merch-card-mini-compare-top-section-height); + } + + @media screen and ${unsafeCSS(TABLET_DOWN)} { + [class*'-merch-cards'] :host([variant='mini-compare-chart']) footer { + flex-direction: column; + align-items: stretch; + text-align: center; + } + } + + @media screen and ${unsafeCSS(DESKTOP_UP)} { + :host([variant='mini-compare-chart']) footer { + padding: var(--consonant-merch-spacing-xs) + var(--consonant-merch-spacing-s) + var(--consonant-merch-spacing-s) + var(--consonant-merch-spacing-s); + } + } + + :host([variant='mini-compare-chart']) slot[name='footer-rows'] { + flex: 1; + display: flex; + flex-direction: column; + justify-content: end; + } + /* mini-compare card heights for the slots: heading-m, body-m, heading-m-price, price-commitment, offers, promo-text, footer */ + :host([variant='mini-compare-chart']) slot[name='heading-m'] { + min-height: var(--consonant-merch-card-mini-compare-heading-m-height); + } + :host([variant='mini-compare-chart']) slot[name='body-m'] { + min-height: var(--consonant-merch-card-mini-compare-body-m-height); + } + :host([variant='mini-compare-chart']) slot[name='heading-m-price'] { + min-height: var( + --consonant-merch-card-mini-compare-heading-m-price-height + ); + } + :host([variant='mini-compare-chart']) slot[name='price-commitment'] { + min-height: var( + --consonant-merch-card-mini-compare-price-commitment-height + ); + } + :host([variant='mini-compare-chart']) slot[name='offers'] { + min-height: var(--consonant-merch-card-mini-compare-offers-height); + } + :host([variant='mini-compare-chart']) slot[name='promo-text'] { + min-height: var(--consonant-merch-card-mini-compare-promo-text-height); + } + :host([variant='mini-compare-chart']) slot[name='callout-content'] { + min-height: var( + --consonant-merch-card-mini-compare-callout-content-height + ); + } + `; +}; diff --git a/libs/features/mas/web-components/src/variants/plans.css.js b/libs/features/mas/web-components/src/variants/plans.css.js new file mode 100644 index 0000000000..d4f22fc53d --- /dev/null +++ b/libs/features/mas/web-components/src/variants/plans.css.js @@ -0,0 +1,56 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP,} from '../media.js'; +export const CSS = ` +:root { + --consonant-merch-card-plans-width: 300px; + --consonant-merch-card-plans-icon-size: 40px; +} + +merch-card[variant="plans"] [slot="description"] { + min-height: 84px; +} + +merch-card[variant="plans"] [slot="quantity-select"] { + display: flex; + justify-content: flex-start; + box-sizing: border-box; + width: 100%; + padding: var(--consonant-merch-spacing-xs); +} + +.one-merch-card.plans, +.two-merch-cards.plans, +.three-merch-cards.plans, +.four-merch-cards.plans { + grid-template-columns: var(--consonant-merch-card-plans-width); +} + +/* Tablet */ +@media screen and ${TABLET_UP} { + :root { + --consonant-merch-card-plans-width: 302px; + } + .two-merch-cards.plans, + .three-merch-cards.plans, + .four-merch-cards.plans { + grid-template-columns: repeat(2, var(--consonant-merch-card-plans-width)); + } +} + +/* desktop */ +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-plans-width: 276px; + } + .three-merch-cards.plans, + .four-merch-cards.plans { + grid-template-columns: repeat(3, var(--consonant-merch-card-plans-width)); + } +} + +/* Large desktop */ + @media screen and ${LARGE_DESKTOP} { + .four-merch-cards.plans { + grid-template-columns: repeat(4, var(--consonant-merch-card-plans-width)); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/plans.js b/libs/features/mas/web-components/src/variants/plans.js new file mode 100644 index 0000000000..02fcdb7dc1 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/plans.js @@ -0,0 +1,53 @@ +import { VariantLayout } from "./variant-layout"; +import { html, css } from 'lit'; +import { CSS } from './plans.css.js'; + +export class Plans extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + postCardUpdateHook() { + this.adjustTitleWidth(); + } + + get stockCheckbox() { + return this.card.checkboxLabel + ? html`` + : ''; + } + + renderLayout() { + return html` ${this.badge} +
+ + + + + ${!this.promoBottom ? html` ` : ''} + + ${this.promoBottom ? html` ` : ''} + ${this.stockCheckbox} +
+ + ${this.secureLabelFooter}`; + } + + static variantStyle = css` + :host([variant='plans']) { + min-height: 348px; + } + + :host([variant='plans']) ::slotted([slot='heading-xs']) { + max-width: var(--consonant-merch-card-heading-xs-max-width, 100%); + } + `; +} diff --git a/libs/features/mas/web-components/src/variants/product.css.js b/libs/features/mas/web-components/src/variants/product.css.js new file mode 100644 index 0000000000..0bfb799d11 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/product.css.js @@ -0,0 +1,42 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP,} from '../media.js'; +export const CSS = ` +:root { + --consonant-merch-card-product-width: 300px; +} + +/* grid style for product */ +.one-merch-card.product, +.two-merch-cards.product, +.three-merch-cards.product, +.four-merch-cards.product { + grid-template-columns: var(--consonant-merch-card-product-width); +} + +/* Tablet */ +@media screen and ${TABLET_UP} { + .two-merch-cards.product, + .three-merch-cards.product, + .four-merch-cards.product { + grid-template-columns: repeat(2, var(--consonant-merch-card-product-width)); + } +} + +/* desktop */ +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-product-width: 378px; + } + + .three-merch-cards.product, + .four-merch-cards.product { + grid-template-columns: repeat(3, var(--consonant-merch-card-product-width)); + } +} + +/* Large desktop */ +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.product { + grid-template-columns: repeat(4, var(--consonant-merch-card-product-width)); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/product.js b/libs/features/mas/web-components/src/variants/product.js new file mode 100644 index 0000000000..b9aced8379 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/product.js @@ -0,0 +1,26 @@ +import { VariantLayout } from "./variant-layout"; +import { html } from 'lit'; +import { CSS } from './product.css.js'; + +export class Product extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + renderLayout() { + return html` ${this.badge} +
+ + + + ${!this.promoBottom ? html`` : ''} + + ${this.promoBottom ? html`` : ''} +
+ ${this.secureLabelFooter}`; + } +} diff --git a/libs/features/mas/web-components/src/variants/segment.css.js b/libs/features/mas/web-components/src/variants/segment.css.js new file mode 100644 index 0000000000..010b74bdf8 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/segment.css.js @@ -0,0 +1,48 @@ +import { TABLET_UP, DESKTOP_UP, MOBILE_LANDSCAPE,} from '../media.js'; +export const CSS = ` +:root { + --consonant-merch-card-segment-width: 378px; +} + +/* grid style for segment */ +.one-merch-card.segment, +.two-merch-cards.segment, +.three-merch-cards.segment, +.four-merch-cards.segment { + grid-template-columns: minmax(276px, var(--consonant-merch-card-segment-width)); +} + +/* Mobile */ +@media screen and ${MOBILE_LANDSCAPE} { + :root { + --consonant-merch-card-segment-width: 276px; + } +} + +@media screen and ${TABLET_UP} { + :root { + --consonant-merch-card-segment-width: 276px; + } + + .two-merch-cards.segment, + .three-merch-cards.segment, + .four-merch-cards.segment { + grid-template-columns: repeat(2, minmax(276px, var(--consonant-merch-card-segment-width))); + } +} + +/* desktop */ +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-segment-width: 302px; + } + + .three-merch-cards.segment { + grid-template-columns: repeat(3, minmax(276px, var(--consonant-merch-card-segment-width))); + } + + .four-merch-cards.segment { + grid-template-columns: repeat(4, minmax(276px, var(--consonant-merch-card-segment-width))); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/segment.js b/libs/features/mas/web-components/src/variants/segment.js new file mode 100644 index 0000000000..f62a9e00d2 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/segment.js @@ -0,0 +1,39 @@ +import { VariantLayout } from "./variant-layout" +import { html, css } from 'lit'; +import { CSS } from './segment.css.js'; + +export class Segment extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + postCardUpdateHook() { + this.adjustTitleWidth(); + } + + renderLayout() { + return html` ${this.badge} +
+ + + ${!this.promoBottom ? html`` : ''} + + ${this.promoBottom ? html`` : ''} +
+
+ ${this.secureLabelFooter }`; + } + + static variantStyle = css` + :host([variant='segment']) { + min-height: 214px; + } + :host([variant='segment']) ::slotted([slot='heading-xs']) { + max-width: var(--consonant-merch-card-heading-xs-max-width, 100%); + } + `; +} diff --git a/libs/features/mas/web-components/src/variants/special-offer.css.js b/libs/features/mas/web-components/src/variants/special-offer.css.js new file mode 100644 index 0000000000..ed4024d1a0 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/special-offer.css.js @@ -0,0 +1,50 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP, MOBILE_LANDSCAPE,} from '../media.js'; +export const CSS = ` +:root { + --consonant-merch-card-special-offers-width: 378px; +} + +merch-card[variant="special-offers"] span[is="inline-price"][data-template="strikethrough"] { + font-size: var(--consonant-merch-card-body-xs-font-size); +} + +/* grid style for special-offers */ +.one-merch-card.special-offers, +.two-merch-cards.special-offers, +.three-merch-cards.special-offers, +.four-merch-cards.special-offers { + grid-template-columns: minmax(300px, var(--consonant-merch-card-special-offers-width)); +} + +@media screen and ${MOBILE_LANDSCAPE} { + :root { + --consonant-merch-card-special-offers-width: 302px; + } +} + +@media screen and ${TABLET_UP} { + :root { + --consonant-merch-card-special-offers-width: 302px; + } + + .two-merch-cards.special-offers, + .three-merch-cards.special-offers, + .four-merch-cards.special-offers { + grid-template-columns: repeat(2, minmax(300px, var(--consonant-merch-card-special-offers-width))); + } +} + +/* desktop */ +@media screen and ${DESKTOP_UP} { + .three-merch-cards.special-offers, + .four-merch-cards.special-offers { + grid-template-columns: repeat(3, minmax(300px, var(--consonant-merch-card-special-offers-width))); + } +} + +@media screen and ${LARGE_DESKTOP} { + .four-merch-cards.special-offers { + grid-template-columns: repeat(4, minmax(300px, var(--consonant-merch-card-special-offers-width))); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/special-offer.js b/libs/features/mas/web-components/src/variants/special-offer.js new file mode 100644 index 0000000000..b76faec004 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/special-offer.js @@ -0,0 +1,50 @@ +import { html, css } from 'lit'; +import { VariantLayout } from './variant-layout'; +import { CSS } from './special-offer.css.js'; + +export class SpecialOffer extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + get headingSelector() { + return '[slot="detail-m"]'; + } + + renderLayout () { + return html`${this.cardImage} +
+ + + +
+ ${this.evergreen + ? html` +
+ +
+ ` + : html` +
+ ${this.secureLabelFooter} + `} + `; + } + + static variantStyle = css` + :host([variant='special-offers']) { + min-height: 439px; + } + + :host([variant='special-offers'].center) { + text-align: center; + } + `; +}; diff --git a/libs/features/mas/web-components/src/variants/twp.css.js b/libs/features/mas/web-components/src/variants/twp.css.js new file mode 100644 index 0000000000..76b8ff8d92 --- /dev/null +++ b/libs/features/mas/web-components/src/variants/twp.css.js @@ -0,0 +1,103 @@ +import { TABLET_UP, DESKTOP_UP, LARGE_DESKTOP, MOBILE_LANDSCAPE,} from '../media.js'; +export const CSS = ` +:root { + --consonant-merch-card-twp-width: 268px; + --consonant-merch-card-twp-mobile-width: 300px; + --consonant-merch-card-twp-mobile-height: 358px; +} + +merch-card[variant="twp"] div[class$='twp-badge'] { + padding: 4px 10px 5px 10px; +} + +merch-card[variant="twp"] [slot="body-xs-top"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + color: var(--merch-color-grey-80); +} + +merch-card[variant="twp"] [slot="body-xs"] ul { + padding: 0; + margin: 0; +} + +merch-card[variant="twp"] [slot="body-xs"] ul li { + list-style-type: none; + padding-left: 0; +} + +merch-card[variant="twp"] [slot="body-xs"] ul li::before { + content: '·'; + font-size: 20px; + padding-right: 5px; + font-weight: bold; +} + +merch-card[variant="twp"] [slot="footer"] { + font-size: var(--consonant-merch-card-body-xs-font-size); + line-height: var(--consonant-merch-card-body-xs-line-height); + padding: var(--consonant-merch-spacing-s); + var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs); + color: var(--merch-color-grey-80); + display: flex; + flex-flow: wrap; +} + +merch-card[variant='twp'] merch-quantity-select, +merch-card[variant='twp'] merch-offer-select { + display: none; +} + +.one-merch-card.twp, +.two-merch-cards.twp, +.three-merch-cards.twp { + grid-template-columns: var(--consonant-merch-card-image-width); +} + +@media screen and ${MOBILE_LANDSCAPE} { + :root { + --consonant-merch-card-twp-width: 300px; + } + .one-merch-card.twp, + .two-merch-cards.twp, + .three-merch-cards.twp { + grid-template-columns: repeat(1, var(--consonant-merch-card-twp-mobile-width)); + } +} + +@media screen and ${TABLET_UP} { + :root { + --consonant-merch-card-twp-width: 268px; + } + .one-merch-card.twp, + .two-merch-cards.twp { + grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); + } + .three-merch-cards.twp { + grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); + } +} + +@media screen and ${DESKTOP_UP} { + :root { + --consonant-merch-card-twp-width: 268px; + } + .one-merch-card.twp + .two-merch-cards.twp { + grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); + } + .three-merch-cards.twp { + grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); + } +} + +@media screen and ${LARGE_DESKTOP} { + .one-merch-card.twp + .two-merch-cards.twp { + grid-template-columns: repeat(2, var(--consonant-merch-card-twp-width)); + } + .three-merch-cards.twp { + grid-template-columns: repeat(3, var(--consonant-merch-card-twp-width)); + } +} +`; diff --git a/libs/features/mas/web-components/src/variants/twp.js b/libs/features/mas/web-components/src/variants/twp.js new file mode 100644 index 0000000000..0da885c4cb --- /dev/null +++ b/libs/features/mas/web-components/src/variants/twp.js @@ -0,0 +1,67 @@ +import { VariantLayout } from "./variant-layout"; +import { html, css } from 'lit'; +import { CSS } from './twp.css.js'; + +export class TWP extends VariantLayout { + constructor(card) { + super(card); + } + + getGlobalCSS() { + return CSS; + } + + renderLayout() { + return html`${this.badge} +
+ + + +
+
+ +
+
`; + } + + static variantStyle = css` + :host([variant='twp']) { + padding: 4px 10px 5px 10px; + } + .twp-badge { + padding: 4px 10px 5px 10px; + } + + :host([variant='twp']) ::slotted(merch-offer-select) { + display: none; + } + + :host([variant='twp']) .top-section { + flex: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + height: 100%; + gap: var(--consonant-merch-spacing-xxs); + padding: var(--consonant-merch-spacing-xs) + var(--consonant-merch-spacing-xs) var(--consonant-merch-spacing-xs) + var(--consonant-merch-spacing-xs); + align-items: flex-start; + } + + :host([variant='twp']) .body { + padding: 0 var(--consonant-merch-spacing-xs); + } + + :host([aria-selected]) .twp-badge { + margin-inline-end: 2px; + padding-inline-end: 9px; + } + + :host([variant='twp']) footer { + gap: var(--consonant-merch-spacing-xxs); + flex-direction: column; + align-self: flex-start; + } + `; +} diff --git a/libs/features/mas/web-components/src/variants/variant-layout.js b/libs/features/mas/web-components/src/variants/variant-layout.js new file mode 100644 index 0000000000..d75cc465cc --- /dev/null +++ b/libs/features/mas/web-components/src/variants/variant-layout.js @@ -0,0 +1,103 @@ +import { html } from 'lit'; + +export class VariantLayout { + static styleMap = {}; + + card; + + insertVariantStyle() { + if (!VariantLayout.styleMap[this.card.variant]) { + VariantLayout.styleMap[this.card.variant] = true; + const styles = document.createElement('style'); + styles.innerHTML = this.getGlobalCSS(); + document.head.appendChild(styles); + } + } + + constructor(card) { + this.card = card; + setTimeout(() => this.insertVariantStyle(), 1); + } + + get badge() { + let additionalStyles; + if (!this.card.badgeBackgroundColor || !this.card.badgeColor || !this.card.badgeText) { + return; + } + if (this.evergreen) { + additionalStyles = `border: 1px solid ${this.card.badgeBackgroundColor}; border-right: none;`; + } + return html` +
+ ${this.card.badgeText} +
+ `; + } + + get cardImage() { + return html`
+ + ${this.badge} +
`; + } + + /* c8 ignore next 3 */ + getGlobalCSS() { + return ''; + } + + get evergreen() { + return this.card.classList.contains('intro-pricing'); + } + + get promoBottom() { + return this.card.classList.contains('promo-bottom'); + } + + get headingSelector() { + return '[slot="heading-xs"]'; + } + + get secureLabelFooter() { + const secureLabel = this.card.secureLabel + ? html`${this.card.secureLabel}` + : ''; + return html`
${secureLabel}
`; + } + + async adjustTitleWidth() { + const cardWidth = this.card.getBoundingClientRect().width; + const badgeWidth = + this.card.badgeElement?.getBoundingClientRect().width || 0; + if (cardWidth === 0 || badgeWidth === 0) return; + this.card.style.setProperty( + '--consonant-merch-card-heading-xs-max-width', + `${Math.round(cardWidth - badgeWidth - 16)}px`, // consonant-merch-spacing-xs + ); + } + + postCardUpdateHook() { + //nothing to do by default + } + + connectedCallbackHook() { + //nothing to do by default + } + + disconnectedCallbackHook() { + //nothing to do by default + } + + /* c8 ignore next 3 */ + renderLayout () { + //nothing to do by default + } +} diff --git a/libs/features/mas/web-components/src/variants/variants.js b/libs/features/mas/web-components/src/variants/variants.js new file mode 100644 index 0000000000..934cf0998f --- /dev/null +++ b/libs/features/mas/web-components/src/variants/variants.js @@ -0,0 +1,51 @@ +import { Catalog } from './catalog.js'; +import { CCDAction } from './ccd-action.js'; +import { Image } from './image.js'; +import { InlineHeading } from './inline-heading.js'; +import { MiniCompareChart } from './mini-compare-chart.js'; +import { Plans } from './plans.js'; +import { Product } from './product.js'; +import { Segment } from './segment.js'; +import { SpecialOffer } from './special-offer.js'; +import { TWP } from './twp.js'; + +const getVariantLayout = (card) => { + switch (card.variant) { + case 'catalog': + return new Catalog(card); + case 'ccd-action': + return new CCDAction(card); + case 'image': + return new Image(card); + case 'inline-heading': + return new InlineHeading(card); + case 'mini-compare-chart': + return new MiniCompareChart(card); + case 'plans': + return new Plans(card); + case 'product': + return new Product(card); + case 'segment': + return new Segment(card); + case 'special-offers': + return new SpecialOffer(card); + case 'twp': + return new TWP(card); + default: + return new Product(card); + } +}; + +const getVariantStyles = () => { + const styles = []; + styles.push(Catalog.variantStyle); + styles.push(CCDAction.variantStyle); + styles.push(MiniCompareChart.variantStyle); + styles.push(Plans.variantStyle); + styles.push(Segment.variantStyle); + styles.push(SpecialOffer.variantStyle); + styles.push(TWP.variantStyle); + return styles; +} + +export { getVariantLayout, getVariantStyles }; diff --git a/libs/features/mas/web-components/test/merch-card.catalog.test.html b/libs/features/mas/web-components/test/merch-card.catalog.test.html new file mode 100644 index 0000000000..8d7f0c1029 --- /dev/null +++ b/libs/features/mas/web-components/test/merch-card.catalog.test.html @@ -0,0 +1,276 @@ + + + + + + + Merch Card Web Component demo page + + + + + + +
+
+ + + +
+
+ + +
+

Best for:

+
    +
  • Photo editing
  • +
  • Compositing
  • +
  • Drawing and painting
  • +
  • Graphic design
  • +
+

Storage:

+

100 GB of cloud storage

+

See system requirements

+
+

Photography

+

+ +

+
+

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

+

Learn more

+
+ +
+ + +
+

Best for:

+
    +
  • Photo editing
  • +
  • Compositing
  • +
  • Drawing and painting
  • +
  • Graphic design
  • +
+

Storage:

+

100 GB of cloud storage

+

See system requirements

+
+

Photography

+

+ +
Inclusive of VAT
+

+
+

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

+

Learn more

+
+
+ Android + IOS + Save now +
+
+ +

Creative Cloud All Apps

+

+
Desktop
+
+

+

Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. + (Substance 3D apps are not included.)

+

See what’s included | Learn more

+
+

Buy now free trial

+
+
+
+ + +
+

Best for:

+
    +
  • Photo editing
  • +
  • Compositing
  • +
  • Drawing and painting
  • +
  • Graphic design
  • +
+

Storage:

+

100 GB of cloud storage

+

See system requirements

+
+

Photography

+

+ +

+
+

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

+

Learn more

+
+
+
+
+
AI Assistant add-on available
+ +
+
+
+
+
+ For a limited time pay + when you add AI assistant to Acrobat Reader, Standard, or Pro. Ends 6/4.Learn More +
+ +
+
+
+ +
+ + +
+

Best for:

+
    +
  • Photo editing
  • +
  • Compositing
  • +
  • Drawing and painting
  • +
  • Graphic design
  • +
+

Storage:

+

100 GB of cloud storage

+

See system requirements

+
+

Photography

+

+ +

+
+

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

+

Learn more

+
+
+
+
+
AI Assistant add-on available
+ +
+
+
+
+
+ For a limited time pay + when you add AI assistant to Acrobat Reader, Standard, or Pro. Ends 6/4.Learn More +
+ +
+
+
+
+ Android + IOS + Save now +
+
+ +

Creative Cloud All Apps

+

+
Desktop
+
+

+

Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. + (Substance 3D apps are not included.)

+

See what’s included | Learn more

+
+
+
+
+
AI Assistant add-on available
+ +
+
+
+
+
+ For a limited time pay + when you add AI assistant to Acrobat Reader, Standard, or Pro. Ends 6/4.Learn More +
+ +
+
+
+ +

Buy now free trial

+
+
+
+ + diff --git a/libs/features/mas/web-components/test/merch-card.catalog.test.html.js b/libs/features/mas/web-components/test/merch-card.catalog.test.html.js new file mode 100644 index 0000000000..1284a2224f --- /dev/null +++ b/libs/features/mas/web-components/test/merch-card.catalog.test.html.js @@ -0,0 +1,71 @@ +// @ts-nocheck +import { runTests } from '@web/test-runner-mocha'; +import { expect } from '@esm-bundle/chai'; + +import { mockLana } from './mocks/lana.js'; +import { mockFetch } from './mocks/fetch.js'; +import { mockConfig } from './mocks/config.js'; + +import '../src/merch-offer.js'; +import '../src/merch-offer-select.js'; +import '../src/merch-quantity-select.js'; + +import { appendMiloStyles, delay } from './utils.js'; +import { mockIms } from './mocks/ims.js'; +import { withWcs } from './mocks/wcs.js'; +import mas from './mas.js'; + +const skipTests = sessionStorage.getItem('skipTests'); + +runTests(async () => { + mockIms(); + mockLana(); + await mockFetch(withWcs); + await mas(); + if (skipTests !== null) { + appendMiloStyles(); + return; + } + describe('merch-card web component', () => { + it('should exist in the HTML document', async () => { + expect(document.querySelector('merch-card')).to.exist; + }); + + it('should display an action menu on hover for catalog variant', async () => { + const catalogCard = document.querySelector( + 'merch-card[variant="catalog"]', + ); + catalogCard.dispatchEvent( + new MouseEvent('mouseover', { bubbles: true }), + ); + await delay(100); + const shadowRoot = catalogCard.shadowRoot; + const actionMenu = shadowRoot.querySelector('.action-menu'); + const actionMenuContent = shadowRoot.querySelector( + '.action-menu-content', + ); + expect(actionMenu.classList.contains('invisible')).to.be.true; + expect(actionMenuContent.classList.contains('hidden')).to.be.true; + expect(actionMenu).to.exist; + expect(actionMenuContent).to.exist; + }); + + it('should display some content when action is clicked for catalog variant', async () => { + const catalogCard = document.querySelector( + 'merch-card[variant="catalog"]', + ); + catalogCard.dispatchEvent( + new MouseEvent('mouseover', { bubbles: true }), + ); + await delay(100); + const actionMenu = catalogCard.shadowRoot.querySelector('.action-menu'); + const actionMenuContent = catalogCard.shadowRoot.querySelector( + '.action-menu-content', + ); + await actionMenu.click(); + await delay(100); + expect(actionMenuContent.classList.contains('hidden')).to.be.false; + }); + }); + +}); diff --git a/libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html b/libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html new file mode 100644 index 0000000000..3354124554 --- /dev/null +++ b/libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html @@ -0,0 +1,634 @@ + + + + + + + Merch Card Web Component demo page + + + + + + +
+
+ + + +
+
+ + + +

This is promo text with a + link

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Creative Cloud All Apps +

+ +
+
+
+
AI Assistant add-on available
+ +
+
+
+
+

+ free + trial + Buy now +

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

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Illustrator

+

+

+ free trial + Buy nowBuy +

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

Save 19% ($US7.99/mo) + for the first year if you buy now.

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+

See terms.

+
+

+

Illustrator

+

Save 19% ($US7.99/mo) for the + first year if you buy now.

+
+
+

+ Free + trial + Buy now +

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

This is promo text with a + link

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Creative Cloud All Apps +

+ +
+

+ free + trial + Buy now +

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

This is promo text with a + link

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Creative Cloud All Apps +

+ +
+

+ free + trial + Buy now +

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

This is promo text with a + link

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Creative Cloud All Apps +

+ +
+
+
+
AI Assistant add-on available
+ +
+
+
+
+

+ free + trial + Buy now +

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

This is promo text with a + link

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Creative Cloud All Apps +

+ +
+
+
+
AI Assistant add-on available
+ +
+
+
+
+

+ free + trial + Buy now +

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

This is promo text with a + link

+
+

+

Get Illustrator on desktop and iPad as part of Creative Cloud.

+
+

+

Creative Cloud All Apps +

+ +
+

+ free + trial + Buy now +

+
+ +
+
+ + + + + +
+
+
+
+ + + diff --git a/libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html.js b/libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html.js new file mode 100644 index 0000000000..f86ba5895f --- /dev/null +++ b/libs/features/mas/web-components/test/merch-card.mini-compare.mobile.test.html.js @@ -0,0 +1,83 @@ +// @ts-nocheck +import { runTests } from '@web/test-runner-mocha'; +import { expect } from '@esm-bundle/chai'; + +import { mockLana } from './mocks/lana.js'; +import { mockFetch } from './mocks/fetch.js'; +import { mockConfig } from './mocks/config.js'; + +import '../src/merch-offer.js'; +import '../src/merch-offer-select.js'; +import '../src/merch-quantity-select.js'; + +import { appendMiloStyles, toggleMobile } from './utils.js'; +import { mockIms } from './mocks/ims.js'; +import { withWcs } from './mocks/wcs.js'; +import mas from './mas.js'; + +const skipTests = sessionStorage.getItem('skipTests'); + +runTests(async () => { + await toggleMobile(); + mockIms(); + mockLana(); + await mockFetch(withWcs); + await mas(); + if (skipTests !== null) { + appendMiloStyles(); + return; + } + describe('[mobile] merch-card web component with mini-compare variant', () => { + it('[mobile] mini-compare-chart should have same body slot heights', async () => { + const miniCompareCharts = document.querySelectorAll( + 'merch-card[variant="mini-compare-chart"]', + ); + await Promise.all( + [...miniCompareCharts].flatMap((card) => { + return [ + card.updateComplete, + ...[...card.querySelectorAll('[data-wcs-osi]')].map( + (osi) => osi.onceSettled(), + ), + ]; + }), + ); + const [card1Slots, card2Slots, card3Slots] = [ + ...miniCompareCharts, + ].map((miniCompareChart) => { + const heights = [ + 'slot[name="heading-m"]', + 'slot[name="body-m"]', + 'slot[name="heading-m-price"]', + 'slot[name="price-commitment"]', + 'slot[name="offers"]', + 'slot[name="promo-text"]', + 'slot[name="callout-content"]', + 'footer', + ] + .map((selector) => + Math.round( + window.getComputedStyle( + miniCompareChart.shadowRoot.querySelector( + selector, + ), + ).height, + ), + ) + .join(','); + return heights; + }); + expect(card1Slots).to.not.contain('auto'); + expect(card1Slots).to.equal(card2Slots); + expect(card2Slots).to.equal(card3Slots); + }); + + it('[mobile] mini-compare-chart should ', async () => { + const miniCompareChart = document.querySelector( + 'merch-card[variant="mini-compare-chart"]', + ); + miniCompareChart?.variantLayout?.removeEmptyRows(); + expect(true, 'removing empty lines do not fail').to.be.true; // TODO improve the assertion + }); + }); +}); diff --git a/libs/features/mas/web-components/test/merch-card.mini-compare.test.html b/libs/features/mas/web-components/test/merch-card.mini-compare.test.html index 38d0cf0893..a822c47b96 100644 --- a/libs/features/mas/web-components/test/merch-card.mini-compare.test.html +++ b/libs/features/mas/web-components/test/merch-card.mini-compare.test.html @@ -63,7 +63,7 @@

This is promo text { const miniCompareChart = document.querySelector( 'merch-card[variant="mini-compare-chart"]', ); - miniCompareChart.removeEmptyRows(); + miniCompareChart?.variantLayout?.removeEmptyRows(); expect(true, 'removing empty lines do not fail').to.be.true; // TODO improve the assertion }); }); diff --git a/libs/features/mas/web-components/test/merch-card.special-offer.test.html.js b/libs/features/mas/web-components/test/merch-card.special-offer.test.html.js new file mode 100644 index 0000000000..63eb936984 --- /dev/null +++ b/libs/features/mas/web-components/test/merch-card.special-offer.test.html.js @@ -0,0 +1,53 @@ +// @ts-nocheck +import { runTests } from '@web/test-runner-mocha'; +import { expect } from '@esm-bundle/chai'; + +import { mockLana } from './mocks/lana.js'; +import { mockFetch } from './mocks/fetch.js'; +import { mockConfig } from './mocks/config.js'; + +import '../src/merch-offer.js'; +import '../src/merch-offer-select.js'; +import '../src/merch-quantity-select.js'; + +import { appendMiloStyles, delay } from './utils.js'; +import { mockIms } from './mocks/ims.js'; +import { withWcs } from './mocks/wcs.js'; +import mas from './mas.js'; + +const skipTests = sessionStorage.getItem('skipTests'); + +runTests(async () => { + mockIms(); + mockLana(); + await mockFetch(withWcs); + await mas(); + if (skipTests !== null) { + appendMiloStyles(); + return; + } + describe('merch-card web component', () => { + it('should exist in the HTML document', async () => { + expect(document.querySelector('merch-card')).to.exist; + }); + it('should exist special offers card in HTML document', async () => { + expect( + document.querySelector('merch-card[variant="special-offers"]'), + ).to.exist; + }); + it('should display a merch-badge', async () => { + expect( + document + .querySelector('merch-card[variant="special-offers"]') + .shadowRoot.querySelector('.special-offers-badge'), + ).to.exist; + }); + }); + + it('should return title for special offer card', async () => { + const title = document.querySelector( + 'merch-card[variant="special-offers"]', + ).title; + expect(title).to.equal('INDIVIDUALS'); + }); +}); diff --git a/libs/features/mas/web-components/test/merch-card.special-offers.test.html b/libs/features/mas/web-components/test/merch-card.special-offers.test.html new file mode 100644 index 0000000000..b0d613bf94 --- /dev/null +++ b/libs/features/mas/web-components/test/merch-card.special-offers.test.html @@ -0,0 +1,202 @@ + + + + + + + Merch Card Web Component demo page + + + + + + +

+
+ + + +
+
+

Three cards

+ +

INDIVIDUALS

+

Save over 30% on Creative Cloud All Apps.

+

+
+
+ Save now +
+
+ + + + + + +
+ + +

STUDENTS AND TEACHERS

+

Save over 60% on Creative Cloud All Apps.

+
+

Get 20+ apps — including Photoshop, Premiere Pro, and Illustrator — and save big for the first + year. +

+

Get 20+ apps — including Photoshop, Premiere Pro, and Illustrator — and save big for the first + year. +

+

Get 20+ apps — including Photoshop, Premiere Pro, and Illustrator — and save big for the first + year. +

+
+
+ Save now +
+
+ + + + + + +
+
+ +

INDIVIDUALS

+

+

Get 10% off Photoshop.

+

+

+

US$19.99/mo US$54.99/mo

+

+
+

Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar + 20. +

+

See terms.

+
+
+ Save now +
+
+ + + + + + +
+
+
+
+ +

INDIVIDUALS

+

+

Get 10% off Photoshop.

+

+

+

US$19.99/mo US$54.99/mo

+

+
+

Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar + 20. +

+

See terms.

+
+
+ Save now +
+
+ + + + + + +
+
+
+
+ +

INDIVIDUALS

+

+

Get 10% off Photoshop.

+

+

+

US$19.99/mo US$54.99/mo

+

+
+

Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar + 20. +

+

See terms.

+
+
+ Save now +
+
+ + + + + + +
+
+
+
+ + diff --git a/libs/features/mas/web-components/test/merch-card.test.html b/libs/features/mas/web-components/test/merch-card.test.html index 477880ec37..3c6dbc8340 100644 --- a/libs/features/mas/web-components/test/merch-card.test.html +++ b/libs/features/mas/web-components/test/merch-card.test.html @@ -56,147 +56,6 @@
-
-

Three cards

- -

INDIVIDUALS

-

Save over 30% on Creative Cloud All Apps.

-

-
-

Get 20+ creative apps and save big when you choose a yearly plan instead of a monthly plan.

-

See terms.

-
-
- Save now -
-
- - - - - - -
-
- -

STUDENTS AND TEACHERS

-

Save over 60% on Creative Cloud All Apps.

-
-

Get 20+ apps — including Photoshop, Premiere Pro, and Illustrator — and save big for the first - year. -

-

Get 20+ apps — including Photoshop, Premiere Pro, and Illustrator — and save big for the first - year. -

-

Get 20+ apps — including Photoshop, Premiere Pro, and Illustrator — and save big for the first - year. -

-
-
- Save now -
-
- - - - - - -
-
- -

INDIVIDUALS

-

-

Get 10% off Photoshop.

-

-

-

US$19.99/mo US$54.99/mo

-

-
-

Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar - 20. -

-

See terms.

-
-
- Save now -
-
- - - - - - -
-
-
-
- -

INDIVIDUALS

-

-

Get 10% off Photoshop.

-

-

-

US$19.99/mo US$54.99/mo

-

-
-

Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar - 20. -

-

See terms.

-
-
- Save now -
-
- - - - - - -
-
-
-
- -

INDIVIDUALS

-

-

Get 10% off Photoshop.

-

-

-

US$19.99/mo US$54.99/mo

-

-
-

Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar - 20. -

-

See terms.

-
-
- Save now -
-
- - - - - - -
-
-
@@ -339,88 +198,6 @@

-
- - -
-

Best for:

-
    -
  • Photo editing
  • -
  • Compositing
  • -
  • Drawing and painting
  • -
  • Graphic design
  • -
-

Storage:

-

100 GB of cloud storage

-

See system requirements

-
-

Photography

-

- -

-
-

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

-

Learn more

-
- -
- - -
-

Best for:

-
    -
  • Photo editing
  • -
  • Compositing
  • -
  • Drawing and painting
  • -
  • Graphic design
  • -
-

Storage:

-

100 GB of cloud storage

-

See system requirements

-
-

Photography

-

- -
Inclusive of VAT
-

-
-

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

-

Learn more

-
-
- Android - IOS - Save now -
-
- -

Creative Cloud All Apps

-

-
Desktop
-
-

-

Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. - (Substance 3D apps are not included.)

-

See what’s included | Learn more

-
-

Buy now free trial

-
-

Acrobat

@@ -540,6 +317,29 @@

Desktop + Mobile

+ + +

Photoshop

+

Desktop + Mobile

+
+

Our real-time customer data platform collects B2C and B2B data from across systems and unifies it + into real-time profiles ready for activation across any channel.

+
+ +
+ + + + + + +
+
-
- - -
-

Best for:

-
    -
  • Photo editing
  • -
  • Compositing
  • -
  • Drawing and painting
  • -
  • Graphic design
  • -
-

Storage:

-

100 GB of cloud storage

-

See system requirements

-
-

Photography

-

- -

-
-

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

-

Learn more

-
-
-
-
-
AI Assistant add-on available
- -
-
-
-
-
- For a limited time pay - when you add AI assistant to Acrobat Reader, Standard, or Pro. Ends 6/4.Learn More -
- -
-
-
- -
- - -
-

Best for:

-
    -
  • Photo editing
  • -
  • Compositing
  • -
  • Drawing and painting
  • -
  • Graphic design
  • -
-

Storage:

-

100 GB of cloud storage

-

See system requirements

-
-

Photography

-

- -

-
-

Lightroom, Lightroom Classic, Photoshop on desktop and iPad, and 1TB of cloud storage.

-

Learn more

-
-
-
-
-
AI Assistant add-on available
- -
-
-
-
-
- For a limited time pay - when you add AI assistant to Acrobat Reader, Standard, or Pro. Ends 6/4.Learn More -
- -
-
-
-
- Android - IOS - Save now -
-
- -

Creative Cloud All Apps

-

-
Desktop
-
-

-

Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. - (Substance 3D apps are not included.)

-

See what’s included | Learn more

-
-
-
-
-
AI Assistant add-on available
- -
-
-
-
-
- For a limited time pay - when you add AI assistant to Acrobat Reader, Standard, or Pro. Ends 6/4.Learn More -
- -
-
-
- -

Buy now free trial

-
-
Desktop + Mobile secure-label="Secure Transaction" tabindex="0" filters="" + variant="product" types="" > diff --git a/libs/features/mas/web-components/test/merch-card.test.html.js b/libs/features/mas/web-components/test/merch-card.test.html.js index fd98d757a2..0bbd0c477d 100644 --- a/libs/features/mas/web-components/test/merch-card.test.html.js +++ b/libs/features/mas/web-components/test/merch-card.test.html.js @@ -30,18 +30,6 @@ runTests(async () => { it('should exist in the HTML document', async () => { expect(document.querySelector('merch-card')).to.exist; }); - it('should exist special offers card in HTML document', async () => { - expect( - document.querySelector('merch-card[variant="special-offers"]'), - ).to.exist; - }); - it('should display a merch-badge', async () => { - expect( - document - .querySelector('merch-card[variant="special-offers"]') - .shadowRoot.querySelector('.special-offers-badge'), - ).to.exist; - }); it('should exist segment card in HTML document', async () => { expect(document.querySelector('merch-card[variant="segment"]')).to .exist; @@ -81,26 +69,6 @@ runTests(async () => { 'm2m,stock-m2m', ); }); - it('should display an action menu on hover for catalog variant', async () => { - const catalogCard = document.querySelector( - 'merch-card[variant="catalog"]', - ); - catalogCard.dispatchEvent( - new MouseEvent('mouseover', { bubbles: true }), - ); - await delay(100); - const shadowRoot = catalogCard.shadowRoot; - const actionMenu = shadowRoot.querySelector('.action-menu'); - const actionMenuContent = shadowRoot.querySelector( - '.action-menu-content', - ); - expect(actionMenu.classList.contains('invisible')).to.be.true; - expect(actionMenuContent.classList.contains('hidden')).to.be.true; - expect(actionMenu).to.exist; - expect(actionMenuContent).to.exist; - catalogCard.toggleActionMenu(); - - }); it('should have and interact with quantity-selector', async () => { const plansCard = document.querySelector('merch-card[type="q-ty"]'); @@ -124,13 +92,6 @@ runTests(async () => { }); }); - it('should return title for special offer card', async () => { - const title = document.querySelector( - 'merch-card[variant="special-offers"]', - ).title; - expect(title).to.equal('INDIVIDUALS'); - }); - it('should return title for segment card', async () => { const title = document.querySelector( 'merch-card[variant="segment"]', diff --git a/libs/features/mas/web-components/test/utils.js b/libs/features/mas/web-components/test/utils.js index 1fef69beac..9fc4e11acb 100644 --- a/libs/features/mas/web-components/test/utils.js +++ b/libs/features/mas/web-components/test/utils.js @@ -23,6 +23,12 @@ export async function toggleLargeDesktop() { } catch {} } +export async function toggleMobile() { + try { + await setViewport({ width: 430, height: 932 }); + } catch {} +} + window.skipTests = () => { window.location.hash = ''; sessionStorage.setItem('skipTests', 'true'); From 13b62d5d724ee367952e5009427bfff45d0c9180 Mon Sep 17 00:00:00 2001 From: sonawanesnehal3 <152426902+sonawanesnehal3@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:22:46 +0530 Subject: [PATCH 12/22] Create a container element for search in standalone gnav (#2833) * Adding container for for search in standalone gnav * Updating class name * Adding unit test * Removing space to fix eslint issue * Updating unit test * Updating unit test * Accommodating review comments * Fixing Eslint * Accommodating review comments * Updating client search * Updating client search --------- Co-authored-by: Snehal Sonawane Co-authored-by: Snehal Sonawane --- libs/blocks/global-navigation/global-navigation.js | 1 + libs/navigation/navigation.js | 2 +- test/blocks/global-navigation/global-navigation.test.js | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index 90e85421d7..5722a778c9 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -822,6 +822,7 @@ class Gnav { ${isDesktop.matches ? '' : this.decorateSearch()} ${this.elements.mainNav} ${isDesktop.matches ? this.decorateSearch() : ''} + ${getConfig().searchEnabled === 'on' ? toFragment`` : ''}
`; diff --git a/libs/navigation/navigation.js b/libs/navigation/navigation.js index e31dd77195..36e70eab8b 100644 --- a/libs/navigation/navigation.js +++ b/libs/navigation/navigation.js @@ -4,7 +4,7 @@ const blockConfig = [ name: 'global-navigation', targetEl: 'header', appendType: 'prepend', - params: ['imsClientId'], + params: ['imsClientId', 'searchEnabled'], }, { key: 'footer', diff --git a/test/blocks/global-navigation/global-navigation.test.js b/test/blocks/global-navigation/global-navigation.test.js index 4af0799b83..945328606d 100644 --- a/test/blocks/global-navigation/global-navigation.test.js +++ b/test/blocks/global-navigation/global-navigation.test.js @@ -620,4 +620,11 @@ describe('global navigation', () => { expect(document.querySelector(`${selectors.brandImage} img`).getAttribute('src')).to.equal('http://localhost:2000/test/blocks/global-navigation/mocks/adobe-dark-logo.svg'); }); }); + + describe('Client search feature in global navigation', () => { + it('should append the feds-client-search div when search is enabled', async () => { + await createFullGlobalNavigation({ customConfig: { searchEnabled: 'on' } }); + expect(document.querySelector(selectors.topNavWrapper).classList.contains('feds-client-search')).to.exist; + }); + }); }); From 2ee8d3b257411cff632dc51a2be8ddd411e6a8e0 Mon Sep 17 00:00:00 2001 From: Suhani Jain <110388864+suhjainadobe@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:22:53 +0530 Subject: [PATCH 13/22] MWPW-157682 Check CLS for photoshop page (#2860) * eagerloading icons * eagerloading icons * eagerloading icons * eagerloading icons * eagerloading icons * eagerloading icons * eagerloading icons * eagerloading icons * eagerloading icons --------- Co-authored-by: Suhani Co-authored-by: Suhani --- libs/blocks/hero-marquee/hero-marquee.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js index 52faa48051..91a0499e68 100644 --- a/libs/blocks/hero-marquee/hero-marquee.js +++ b/libs/blocks/hero-marquee/hero-marquee.js @@ -132,14 +132,14 @@ function parseKeyString(str) { return result; } -function loadContentType(el, key, classes) { +async function loadContentType(el, key, classes) { if (classes !== undefined && classes.length) el.classList.add(...classes); switch (key) { case 'bgcolor': decorateBg(el); break; case 'lockup': - decorateLockupRow(el, classes); + await decorateLockupRow(el, classes); break; case 'qrcode': decorateQr(el); @@ -237,6 +237,7 @@ export default async function init(el) { } }); + const promiseArr = []; [...rows].forEach(async (row) => { const cols = row.querySelectorAll(':scope > div'); const firstCol = cols[0]; @@ -248,7 +249,9 @@ export default async function init(el) { firstCol.parentElement.classList.add(`row-${parsed.key}`, 'con-block'); firstCol.remove(); cols[1].classList.add('row-wrapper'); - if (contentTypes.includes(parsed.key)) loadContentType(row, parsed.key, parsed.classes); + if (contentTypes.includes(parsed.key)) { + promiseArr.push(loadContentType(row, parsed.key, parsed.classes)); + } } else { row.classList.add('norm'); decorateBlockHrs(row); @@ -256,4 +259,5 @@ export default async function init(el) { } }); decorateTextOverrides(el, ['-heading', '-body', '-detail'], mainCopy); + await Promise.all(promiseArr); } From 66694aa4b4fb77bc9a95319a007e41286d924e7f Mon Sep 17 00:00:00 2001 From: Megan Thomas Date: Mon, 16 Sep 2024 01:53:00 -0700 Subject: [PATCH 14/22] MWPW-144470 Support article header author without link (#2863) --- libs/blocks/article-header/article-header.js | 9 +++--- .../article-header/article-header.test.js | 15 ++++++++++ test/blocks/article-header/mocks/body.html | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/libs/blocks/article-header/article-header.js b/libs/blocks/article-header/article-header.js index fb4ea8235f..8f94e0b405 100644 --- a/libs/blocks/article-header/article-header.js +++ b/libs/blocks/article-header/article-header.js @@ -32,14 +32,15 @@ function openPopup(e) { } async function buildAuthorInfo(authorEl, bylineContainer) { - const { href, textContent } = authorEl; + const { textContent } = authorEl; + const link = authorEl.href || authorEl.dataset.authorPage; const config = getConfig(); const base = config.miloLibs || config.codeRoot; const authorImg = createTag('div', { class: 'article-author-image' }); authorImg.style.backgroundImage = `url(${base}/blocks/article-header/adobe-logo.svg)`; bylineContainer.prepend(authorImg); - const doc = await validateAuthorUrl(href); + const doc = await validateAuthorUrl(link); if (!doc) { const p = createTag('p', null, textContent); authorEl.replaceWith(p); @@ -48,7 +49,7 @@ async function buildAuthorInfo(authorEl, bylineContainer) { const img = doc.querySelector('img'); if (img) { - img.setAttribute('alt', authorEl.textContent); + img.setAttribute('alt', textContent); authorImg.append(img); if (!img.complete) { img.addEventListener('load', () => { @@ -197,7 +198,7 @@ export default async function init(blockEl) { bylineContainer.firstElementChild.classList.add('article-byline-info'); const authorContainer = bylineContainer.firstElementChild.firstElementChild; - const authorEl = authorContainer.querySelector('a'); + const authorEl = authorContainer.firstElementChild; authorContainer.classList.add('article-author'); buildAuthorInfo(authorEl, bylineContainer); diff --git a/test/blocks/article-header/article-header.test.js b/test/blocks/article-header/article-header.test.js index 7c0a20bd17..3056c62da5 100644 --- a/test/blocks/article-header/article-header.test.js +++ b/test/blocks/article-header/article-header.test.js @@ -105,6 +105,21 @@ describe('article header', () => { const categoryLink = document.querySelector('.article-category a'); expect(categoryLink.href.includes('/topics/')).to.be.true; }); + + it('adds an author image from a link', () => { + const img = document.querySelector('.article-author-image img'); + const link = document.querySelector('.article-author a'); + expect(img).to.exist; + expect(link).to.exist; + }); + + it('adds an author image from data attribute', async () => { + await init(document.querySelector('#article-header-no-author-link')); + const img = document.querySelector('.article-author-image img'); + const author = document.querySelector('.article-author [data-author-page]'); + expect(img).to.exist; + expect(author).to.exist; + }); }); describe('test the invalid article header', () => { diff --git a/test/blocks/article-header/mocks/body.html b/test/blocks/article-header/mocks/body.html index ffe3737645..ec8471006d 100644 --- a/test/blocks/article-header/mocks/body.html +++ b/test/blocks/article-header/mocks/body.html @@ -39,3 +39,31 @@

+
+ +
+
+
+

+ Celebrating a special milestone: 10 things you might not know about Adobe’s 40 years of innovation

+
+
+
+
+

Adobe Communications Team

+

12-05-2022

+
+
+
+
+

+ + + Caption +

+
+
+ From d7ae16e46d3af3414367c01dd11ef43910f5a2cf Mon Sep 17 00:00:00 2001 From: Santoshkumar Nateekar Date: Mon, 16 Sep 2024 01:54:19 -0700 Subject: [PATCH 15/22] [MWPW-158021] Move Nala tests to Milo Repo (#2847) * add nala tests to milo repo * update the nalarun for single run * fix georouting test and add eslint overrides for nala files * add eslint overrides for nala files * fix operator assignment eslint error * update eslint lines * remove eslint disable lines * move prrun to nala folder * update the path correctly * add nala folder to .hlxignore file * update tag for ost --------- Co-authored-by: milo-pr-merge[bot] <169241390+milo-pr-merge[bot]@users.noreply.github.com> Co-authored-by: Santoshkumar Sharanappa Nateekar Co-authored-by: Santoshkumar Sharanappa Nateekar Co-authored-by: Santoshkumar Sharanappa Nateekar --- .eslintrc.js | 11 + .github/workflows/run-nala-default.yml | 48 ++ .hlxignore | 1 + nala/blocks/accordion/accordion.spec.js | 2 +- nala/blocks/actionitem/actionitem.page.js | 33 + nala/blocks/actionitem/actionitem.spec.js | 87 +++ nala/blocks/actionitem/actionitem.test.js | 188 ++++++ nala/blocks/aside/aside.page.js | 65 ++ nala/blocks/aside/aside.spec.js | 107 +++ nala/blocks/aside/aside.test.js | 525 +++++++++++++++ nala/blocks/card/card.page.js | 57 ++ nala/blocks/card/card.spec.js | 84 +++ nala/blocks/card/card.test.js | 192 ++++++ nala/blocks/carousel/carousel.page.js | 222 +++++++ nala/blocks/carousel/carousel.spec.js | 26 + nala/blocks/carousel/carousel.test.js | 115 ++++ nala/blocks/chart/chart.page.js | 49 ++ nala/blocks/chart/chart.spec.js | 91 +++ nala/blocks/chart/chart.test.js | 197 ++++++ nala/blocks/columns/columns.page.js | 52 ++ nala/blocks/columns/columns.spec.js | 70 ++ nala/blocks/columns/columns.test.js | 160 +++++ nala/blocks/figure/figure.page.js | 9 + nala/blocks/figure/figure.spec.js | 23 + nala/blocks/figure/figure.test.js | 51 ++ nala/blocks/howto/howto.page.js | 52 ++ nala/blocks/howto/howto.spec.js | 23 + nala/blocks/howto/howto.test.js | 76 +++ nala/blocks/icon/icon.page.js | 102 +++ nala/blocks/icon/icon.spec.js | 43 ++ nala/blocks/icon/icon.test.js | 58 ++ nala/blocks/iframe/iframe.page.js | 14 + nala/blocks/iframe/iframe.spec.js | 11 + nala/blocks/iframe/iframe.test.js | 24 + nala/blocks/marketo/marketo.page.js | 131 ++++ nala/blocks/marketo/marketo.spec.js | 73 ++ nala/blocks/marketo/marketo.test.js | 278 ++++++++ nala/blocks/marquee/marquee.page.js | 186 ++++++ nala/blocks/marquee/marquee.spec.js | 187 ++++++ nala/blocks/marquee/marquee.test.js | 572 ++++++++++++++++ nala/blocks/media/media.page.js | 79 +++ nala/blocks/media/media.spec.js | 66 ++ nala/blocks/media/media.test.js | 165 +++++ nala/blocks/merchcard/merchcard.pages.js | 99 +++ nala/blocks/merchcard/merchcard.spec.js | 194 ++++++ nala/blocks/merchcard/merchcard.test.js | 392 +++++++++++ nala/blocks/modal/modal.page.js | 62 ++ nala/blocks/modal/modal.spec.js | 46 ++ nala/blocks/modal/modal.test.js | 114 ++++ nala/blocks/quote/quote.page.js | 64 ++ nala/blocks/quote/quote.spec.js | 73 ++ nala/blocks/quote/quote.test.js | 151 +++++ nala/blocks/review/review.page.js | 89 +++ nala/blocks/review/review.spec.js | 31 + nala/blocks/review/review.test.js | 48 ++ nala/blocks/table/table.page.js | 75 +++ nala/blocks/table/table.spec.js | 121 ++++ nala/blocks/table/table.test.js | 195 ++++++ nala/blocks/tabs/tabs.page.js | 26 + nala/blocks/tabs/tabs.spec.js | 37 ++ nala/blocks/tabs/tabs.test.js | 140 ++++ nala/blocks/text/text.page.js | 115 ++++ nala/blocks/text/text.spec.js | 97 +++ nala/blocks/text/text.test.js | 243 +++++++ nala/blocks/video/video.page.js | 72 ++ nala/blocks/video/video.spec.js | 84 +++ nala/blocks/video/video.test.js | 212 ++++++ nala/blocks/zpattern/zpattern.page.js | 39 ++ nala/blocks/zpattern/zpattern.spec.js | 131 ++++ nala/blocks/zpattern/zpattern.test.js | 168 +++++ nala/features/commerce/commerce.page.js | 19 + nala/features/commerce/commerce.spec.js | 89 +++ nala/features/commerce/commerce.test.js | 474 +++++++++++++ nala/features/feds/footer/footer.page.js | 77 +++ nala/features/feds/footer/footer.spec.js | 26 + nala/features/feds/footer/footer.test.js | 164 +++++ nala/features/feds/header/header.page.js | 110 ++++ nala/features/feds/header/header.spec.js | 12 + nala/features/feds/header/header.test.js | 52 ++ nala/features/feds/login/login.page.js | 118 ++++ nala/features/georouting/georouting.page.js | 2 +- nala/features/georouting/georouting.test.js | 1 - nala/features/imslogin/imslogin.page.js | 23 + nala/features/osttools/ost.page.js | 31 + nala/features/osttools/ost.spec.js | 128 ++++ nala/features/osttools/ost.test.js | 695 ++++++++++++++++++++ nala/features/promotions/promotions.page.js | 25 + nala/features/promotions/promotions.spec.js | 163 +++++ nala/features/promotions/promotions.test.js | 534 +++++++++++++++ nala/libs/imslogin.js | 37 ++ nala/utils/pr.run.sh | 60 ++ 91 files changed, 10560 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/run-nala-default.yml create mode 100644 nala/blocks/actionitem/actionitem.page.js create mode 100644 nala/blocks/actionitem/actionitem.spec.js create mode 100644 nala/blocks/actionitem/actionitem.test.js create mode 100644 nala/blocks/aside/aside.page.js create mode 100644 nala/blocks/aside/aside.spec.js create mode 100644 nala/blocks/aside/aside.test.js create mode 100644 nala/blocks/card/card.page.js create mode 100644 nala/blocks/card/card.spec.js create mode 100644 nala/blocks/card/card.test.js create mode 100644 nala/blocks/carousel/carousel.page.js create mode 100644 nala/blocks/carousel/carousel.spec.js create mode 100644 nala/blocks/carousel/carousel.test.js create mode 100644 nala/blocks/chart/chart.page.js create mode 100644 nala/blocks/chart/chart.spec.js create mode 100644 nala/blocks/chart/chart.test.js create mode 100644 nala/blocks/columns/columns.page.js create mode 100644 nala/blocks/columns/columns.spec.js create mode 100644 nala/blocks/columns/columns.test.js create mode 100644 nala/blocks/figure/figure.page.js create mode 100644 nala/blocks/figure/figure.spec.js create mode 100644 nala/blocks/figure/figure.test.js create mode 100644 nala/blocks/howto/howto.page.js create mode 100644 nala/blocks/howto/howto.spec.js create mode 100644 nala/blocks/howto/howto.test.js create mode 100644 nala/blocks/icon/icon.page.js create mode 100644 nala/blocks/icon/icon.spec.js create mode 100644 nala/blocks/icon/icon.test.js create mode 100644 nala/blocks/iframe/iframe.page.js create mode 100644 nala/blocks/iframe/iframe.spec.js create mode 100644 nala/blocks/iframe/iframe.test.js create mode 100644 nala/blocks/marketo/marketo.page.js create mode 100644 nala/blocks/marketo/marketo.spec.js create mode 100644 nala/blocks/marketo/marketo.test.js create mode 100644 nala/blocks/marquee/marquee.page.js create mode 100644 nala/blocks/marquee/marquee.spec.js create mode 100644 nala/blocks/marquee/marquee.test.js create mode 100644 nala/blocks/media/media.page.js create mode 100644 nala/blocks/media/media.spec.js create mode 100644 nala/blocks/media/media.test.js create mode 100644 nala/blocks/merchcard/merchcard.pages.js create mode 100644 nala/blocks/merchcard/merchcard.spec.js create mode 100644 nala/blocks/merchcard/merchcard.test.js create mode 100644 nala/blocks/modal/modal.page.js create mode 100644 nala/blocks/modal/modal.spec.js create mode 100644 nala/blocks/modal/modal.test.js create mode 100644 nala/blocks/quote/quote.page.js create mode 100644 nala/blocks/quote/quote.spec.js create mode 100644 nala/blocks/quote/quote.test.js create mode 100644 nala/blocks/review/review.page.js create mode 100644 nala/blocks/review/review.spec.js create mode 100644 nala/blocks/review/review.test.js create mode 100644 nala/blocks/table/table.page.js create mode 100644 nala/blocks/table/table.spec.js create mode 100644 nala/blocks/table/table.test.js create mode 100644 nala/blocks/tabs/tabs.page.js create mode 100644 nala/blocks/tabs/tabs.spec.js create mode 100644 nala/blocks/tabs/tabs.test.js create mode 100644 nala/blocks/text/text.page.js create mode 100644 nala/blocks/text/text.spec.js create mode 100644 nala/blocks/text/text.test.js create mode 100644 nala/blocks/video/video.page.js create mode 100644 nala/blocks/video/video.spec.js create mode 100644 nala/blocks/video/video.test.js create mode 100644 nala/blocks/zpattern/zpattern.page.js create mode 100644 nala/blocks/zpattern/zpattern.spec.js create mode 100644 nala/blocks/zpattern/zpattern.test.js create mode 100644 nala/features/commerce/commerce.page.js create mode 100644 nala/features/commerce/commerce.spec.js create mode 100644 nala/features/commerce/commerce.test.js create mode 100644 nala/features/feds/footer/footer.page.js create mode 100644 nala/features/feds/footer/footer.spec.js create mode 100644 nala/features/feds/footer/footer.test.js create mode 100644 nala/features/feds/header/header.page.js create mode 100644 nala/features/feds/header/header.spec.js create mode 100644 nala/features/feds/header/header.test.js create mode 100644 nala/features/feds/login/login.page.js create mode 100644 nala/features/imslogin/imslogin.page.js create mode 100644 nala/features/osttools/ost.page.js create mode 100644 nala/features/osttools/ost.spec.js create mode 100644 nala/features/osttools/ost.test.js create mode 100644 nala/features/promotions/promotions.page.js create mode 100644 nala/features/promotions/promotions.spec.js create mode 100644 nala/features/promotions/promotions.test.js create mode 100644 nala/libs/imslogin.js create mode 100755 nala/utils/pr.run.sh diff --git a/.eslintrc.js b/.eslintrc.js index 5e06277005..f24adfa387 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,6 +45,17 @@ module.exports = { files: ['test/**/*.js'], rules: { 'no-console': 0 }, }, + { + // Override for nala test + files: ['nala/**/*.js', 'nala/**/*.test.js'], + rules: { + 'no-console': 0, + 'import/no-extraneous-dependencies': 0, + 'max-len': 0, + 'chai-friendly/no-unused-expressions': 0, + 'no-plusplus': 0, + }, + }, ], ignorePatterns: [ '/libs/deps/*', diff --git a/.github/workflows/run-nala-default.yml b/.github/workflows/run-nala-default.yml new file mode 100644 index 0000000000..4031be7c8d --- /dev/null +++ b/.github/workflows/run-nala-default.yml @@ -0,0 +1,48 @@ +name: Run Nala Tests + +on: + push: + branches: + - stage + - main + pull_request: + branches: + - stage + - main + types: [opened, synchronize, reopened] + +jobs: + run-nala-tests: + name: Running Nala E2E UI Tests + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Set execute permission for nalarun.sh + run: chmod +x ./nala/utils/pr.run.sh + + - name: Run Nala Tests via pr.run.sh + run: ./nala/utils/pr.run.sh + env: + labels: ${{ join(github.event.pull_request.labels.*.name, ' ') }} + branch: ${{ github.event.pull_request.head.ref }} + repoName: ${{ github.repository }} + prUrl: ${{ github.event.pull_request.head.repo.html_url }} + prOrg: ${{ github.event.pull_request.head.repo.owner.login }} + prRepo: ${{ github.event.pull_request.head.repo.name }} + prBranch: ${{ github.event.pull_request.head.ref }} + prBaseBranch: ${{ github.event.pull_request.base.ref }} + GITHUB_ACTION_PATH: ${{ github.workspace }} + IMS_EMAIL: ${{ secrets.IMS_EMAIL }} + IMS_PASS: ${{ secrets.IMS_PASS }} diff --git a/.hlxignore b/.hlxignore index e394ce0b3b..0fdf90bee2 100644 --- a/.hlxignore +++ b/.hlxignore @@ -3,6 +3,7 @@ .* *.md test/* +nala build/* LICENSE web-test-runner.config.mjs diff --git a/nala/blocks/accordion/accordion.spec.js b/nala/blocks/accordion/accordion.spec.js index a40d8740e3..de3a6a19e5 100644 --- a/nala/blocks/accordion/accordion.spec.js +++ b/nala/blocks/accordion/accordion.spec.js @@ -11,7 +11,7 @@ module.exports = { heading1: 'What size PDFs can I compress?', heading2: 'How do I check my PDF file size?', }, - tags: '@accordion @t1 @smoke @regression @milo', + tags: '@accordion @smoke @regression @milo', }, { tcid: '1', diff --git a/nala/blocks/actionitem/actionitem.page.js b/nala/blocks/actionitem/actionitem.page.js new file mode 100644 index 0000000000..552c5ce9e1 --- /dev/null +++ b/nala/blocks/actionitem/actionitem.page.js @@ -0,0 +1,33 @@ +export default class ActionItem { + constructor(page, nth = 0) { + this.page = page; + + this.actionItem = this.page.locator('.action-item').nth(nth); + this.small = this.page.locator('.action-item.small').nth(nth); + this.medium = this.page.locator('.action-item.medium').nth(nth); + this.large = this.page.locator('.action-item.large').nth(nth); + this.center = this.page.locator('.action-item.center').nth(nth); + this.rounded = this.page.locator('.action-item.rounded').nth(nth); + this.actionItemFloat = this.page.locator('.action-item.float-button').nth(nth); + this.floatButton = this.page.locator('.action-item.float-button > div > div> p > a'); + this.libraryContainerStart = this.page.locator('.library-container-start').nth(nth); + this.libraryContainerEnd = this.page.locator('.library-container-end').nth(nth); + this.actionScroller = this.page.locator('.action-scroller').nth(nth); + this.scroller = this.page.locator('.scroller ').nth(nth); + this.scrollerActionItems = this.scroller.locator('.action-item'); + + this.navigationPrevious = this.actionScroller.locator('.nav-grad.previous'); + this.navigationNext = this.actionScroller.locator('.nav-grad.next'); + this.nextButton = this.navigationNext.locator('.nav-button.next-button'); + this.previousButton = this.navigationPrevious.locator('.nav-button.previous-button'); + + this.scrollerActionItems = this.scroller.locator('.action-item'); + + this.mainImage = this.actionItem.locator('.main-image').nth(nth); + this.mainImageDark = this.actionItem.locator('.main-image.dark').nth(nth); + this.image = this.mainImage.locator('img').nth(0); + this.bodyText = this.actionItem.locator('p').nth(1); + this.bodyTextLink = this.actionItem.locator('a').nth(0); + this.floatOutlineButton = this.mainImage.locator('a'); + } +} diff --git a/nala/blocks/actionitem/actionitem.spec.js b/nala/blocks/actionitem/actionitem.spec.js new file mode 100644 index 0000000000..54b8f92efe --- /dev/null +++ b/nala/blocks/actionitem/actionitem.spec.js @@ -0,0 +1,87 @@ +module.exports = { + FeatureName: 'Action Item Block', + features: [ + { + tcid: '0', + name: '@Action-item (small)', + path: '/drafts/nala/blocks/action-item/action-item-small', + data: { + bodyText: 'Body XS Regular - Image min-height 56px', + imgMinHeight: '56px', + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Action-item (medium)', + path: '/drafts/nala/blocks/action-item/action-item-medium', + data: { + bodyText: 'Body S Regular - Image min-height 80px', + imgMinHeight: '80px', + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Action-item (large)', + path: '/drafts/nala/blocks/action-item/action-item-large', + data: { + bodyText: 'Body M Regular - Image min-height 104px', + imgMinHeight: '104px', + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Action-item (center)', + path: '/drafts/nala/blocks/action-item/action-item-center', + data: { + bodyText: 'Center content', + margin: '0 auto', + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Action-item (rounded)', + path: '/drafts/nala/blocks/action-item/action-item-rounded', + data: { + bodyText: 'Border radius 4px', + borderRadius: '4px', + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '5', + name: '@Action-item (float-button)', + path: '/drafts/nala/blocks/action-item/action-item-float-button', + data: { + bodyText: 'Float button', + floatButtonText: 'Edit', + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Action-item (scroller)', + path: '/drafts/nala/blocks/action-item/action-scroller', + data: { + bodyText: 'content', + floatButtonText: 'Edit', + actionItemsCount: 6, + }, + tags: '@action-item @smoke @regression @milo', + }, + { + tcid: '7', + name: '@Action-item (scroller with navigation)', + path: '/drafts/nala/blocks/action-item/action-scroller-navigation', + data: { + bodyText: 'content', + floatButtonText: 'Edit', + actionItemsCount: 8, + }, + tags: '@action-item @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/actionitem/actionitem.test.js b/nala/blocks/actionitem/actionitem.test.js new file mode 100644 index 0000000000..75cda610ba --- /dev/null +++ b/nala/blocks/actionitem/actionitem.test.js @@ -0,0 +1,188 @@ +import { expect, test } from '@playwright/test'; +import { features } from './actionitem.spec.js'; +import ActionItem from './actionitem.page.js'; + +let actionItem; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Action-Item block test suite', () => { + test.beforeEach(async ({ page }) => { + actionItem = new ActionItem(page); + }); + + // Test 0 : Action-Item (Small) + test(`0: @Action-item (small), ${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.small).toBeVisible(); + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.image).toHaveCSS('min-height', data.imgMinHeight); + + await expect(await actionItem.bodyTextLink).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + }); + }); + + // Test 1 : Action-Item (Medium) + test(`1: @Action-item (medium), ${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.medium).toBeVisible(); + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.image).toHaveCSS('min-height', data.imgMinHeight); + + await expect(await actionItem.bodyTextLink).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + }); + }); + + // Test 2 : Action-Item (Large) + test(`2: @Action-item (large), ${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.large).toBeVisible(); + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.image).toHaveCSS('min-height', data.imgMinHeight); + + await expect(await actionItem.bodyTextLink).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + }); + }); + + // Test 3 : Action-Item (Center) + test(`3: @Action-item (center), ${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.center).toBeVisible(); + await expect(await actionItem.image).toBeVisible(); + + await expect(await actionItem.bodyTextLink).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + }); + }); + + // Test 4 : Action-Item (Rounded) + test(`4: @Action-item (rounded), ${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.rounded).toBeVisible(); + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.image).toHaveCSS('border-radius', data.borderRadius); + + await expect(await actionItem.bodyTextLink).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + }); + }); + + // Test 5 : Action-Item (Float Button) + test(`5: @Action-item (float-button), ${features[5].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[5].path}${miloLibs}`; + console.info(`[Test Page]: ${testPage}`); + const { data } = features[5]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.actionItemFloat).toBeVisible(); + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.floatOutlineButton).toBeVisible(); + await expect(await actionItem.floatOutlineButton).toContainText(data.floatButtonText); + }); + await test.step('step-3: Click the float button', async () => { + await actionItem.floatButton.click(); + expect(await page.url()).not.toBe(testPage); + }); + }); + + // Test 6 : Action-Item (scroller) + test(`6: @Action-item (scroller), ${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[6].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.actionScroller).toBeVisible(); + await expect(await actionItem.scroller).toBeVisible(); + await expect(await actionItem.scrollerActionItems).toHaveCount(data.actionItemsCount); + + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + }); + }); + + // Test 7 : Action-Item (scroller) + test(`7: @Action-item (scroller with navigation), ${features[7].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[7].path}${miloLibs}`); + const { data } = features[7]; + + await test.step('step-1: Go to Action item block test page', async () => { + await page.goto(`${baseURL}${features[7].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[7].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Action item content/specs', async () => { + await expect(await actionItem.actionScroller).toBeVisible(); + await expect(await actionItem.scroller).toBeVisible(); + await expect(await actionItem.scrollerActionItems).toHaveCount(data.actionItemsCount); + + await expect(await actionItem.image).toBeVisible(); + await expect(await actionItem.bodyText).toContainText(data.bodyText); + + await expect(await actionItem.nextButton).toBeVisible({ timeout: 1000 }); + await actionItem.nextButton.click(); + await expect(await actionItem.previousButton).toBeVisible({ timeout: 1000 }); + await expect(await actionItem.navigationNext).toHaveAttribute('hide-btn', 'false'); + }); + }); +}); diff --git a/nala/blocks/aside/aside.page.js b/nala/blocks/aside/aside.page.js new file mode 100644 index 0000000000..15cc507475 --- /dev/null +++ b/nala/blocks/aside/aside.page.js @@ -0,0 +1,65 @@ +/* eslint-disable import/no-import-module-exports */ + +export default class Aside { + constructor(page) { + this.page = page; + + this.aside = page.locator('.aside'); + this.textField = this.aside.locator('.text'); + this.textFieldSmall = this.aside.locator('p.body-s').first(); + this.textFieldMedium = this.aside.locator('p.body-m').first(); + this.textFieldLarge = this.aside.locator('p.body-l').first(); + // ASIDE HEADINGS: + this.h2TitleXLarge = this.aside.locator('h2.heading-xl'); + this.h3TitleXLarge = this.aside.locator('h3.heading-xl'); + this.h2TitleLarge = this.aside.locator('h2.heading-l'); + this.h3TitleLarge = this.aside.locator('h3.heading-l'); + this.h2TitleSmall = this.aside.locator('h2.heading-s'); + this.h3TitleSmall = this.aside.locator('h3.heading-s'); + // ASIDE BLOCK ELEMENTS: + this.icon = this.aside.locator('p.icon-area picture'); + this.image = this.aside.locator('div.image'); + this.noImage = this.aside.locator('div.no-image'); + this.iconArea = this.aside.locator('p.icon-area'); + this.detailLabel = this.aside.locator('p.detail-m'); + this.actionArea = this.aside.locator('p.action-area'); + this.textLink = this.textField.locator('a').first(); + this.linkTextCta = this.aside.locator('a[daa-ll*="Link"], a[daa-ll*="link"], a[daa-ll*="action"]'); + this.actionLinks = this.aside.locator('div[data-valign="middle"] a'); + this.actionButtons = this.aside.locator('p.action-area a'); + this.blueButtonCta = this.aside.locator('a.con-button.blue'); + this.blackButtonCta = this.aside.locator('a.con-button.outline'); + // ASIDE DEFAULT BLOCKS: + this.asideSmall = page.locator('div.aside.small'); + this.asideMedium = page.locator('div.aside.medium'); + this.asideLarge = page.locator('div.aside.large'); + // ASIDE INLINE BLOCKS: + this.asideInline = page.locator('div.aside.inline'); + this.asideInlineDark = page.locator('div.aside.inline.dark'); + // ASIDE SPLIT BLOCKS: + this.asideSplitSmallDark = page.locator('div.aside.split.small.dark'); + this.asideSplitSmallHalfDark = page.locator('div.aside.split.small.half.dark'); + this.asideSplitMedium = page.locator('div.aside.split.medium'); + this.asideSplitMedidumHalf = page.locator('div.aside.split.medium.half'); + this.asideSplitLarge = page.locator('div.aside.split.large'); + this.asideSplitLargeHalfDark = page.locator('div.aside.split.large.half.dark'); + // ASIDE NOTIFICATION BLOCKS: + this.asideNotifSmall = page.locator('div.aside.notification.small'); + this.asideNotifMedium = page.locator('div.aside.notification.medium'); + this.asideNotifLarge = page.locator('div.aside.notification.large'); + this.asideNotifMediumCenter = page.locator('div.aside.notification.center'); + this.asideNotifLargeCenter = page.locator('div.aside.notification.large.center'); + this.asideNotifExtraSmallDark = page.locator('div.aside.notification.extra-small.dark'); + + // ASIDE PROPS: + this.props = { + background: { + black: 'rgb(17, 17, 17)', + darkGrey: 'rgb(171, 171, 171)', + lightGrey1: 'rgb(238, 238, 238)', + lightGrey2: 'rgb(245, 245, 245)', + lightGrey3: 'rgb(249, 249, 249)', + }, + }; + } +} diff --git a/nala/blocks/aside/aside.spec.js b/nala/blocks/aside/aside.spec.js new file mode 100644 index 0000000000..aa73285964 --- /dev/null +++ b/nala/blocks/aside/aside.spec.js @@ -0,0 +1,107 @@ +module.exports = { + name: 'Aside Block', + features: [ + { + name: '@Aside-Small', + path: '/drafts/nala/blocks/aside/aside-small', + browserParams: '?georouting=off', + tags: '@aside @aside-small @smoke @regression @milo', + }, + { + name: '@Aside-Medium', + path: '/drafts/nala/blocks/aside/aside-medium', + browserParams: '?georouting=off', + tags: '@aside @aside-medium @smoke @regression @milo', + }, + { + name: '@Aside-Large', + path: '/drafts/nala/blocks/aside/aside-large', + browserParams: '?georouting=off', + tags: '@aside @aside-large @smoke @regression @milo', + }, + { + name: '@Aside-Split-Small-Dark', + path: '/drafts/nala/blocks/aside/aside-split-small-dark', + browserParams: '?georouting=off', + tags: '@aside @aside-split-small-dark @smoke @regression @milo', + }, + { + name: '@Aside-Split-Small-Half-Dark', + path: '/drafts/nala/blocks/aside/aside-split-small-half-dark', + browserParams: '?georouting=off', + tags: '@aside @aside-split-small-half-dark @smoke @regression @milo', + }, + { + name: '@Aside-Split-Medium', + path: '/drafts/nala/blocks/aside/aside-split-medium', + browserParams: '?georouting=off', + tags: '@aside @aside-split-medium @smoke @regression @milo', + }, + { + name: '@Aside-Split-Medium-Half', + path: '/drafts/nala/blocks/aside/aside-split-medium-half', + browserParams: '?georouting=off', + tags: '@aside @aside-split-medium-half @smoke @regression @milo', + }, + { + name: '@Aside-Split-Large', + path: '/drafts/nala/blocks/aside/aside-split-large', + browserParams: '?georouting=off', + tags: '@aside @aside-split-large @smoke @regression @milo', + }, + { + name: '@Aside-Split-Large-Half-Dark', + path: '/drafts/nala/blocks/aside/aside-split-large-half-dark', + browserParams: '?georouting=off', + tags: '@aside @aside-split-large-half-dark @smoke @regression @milo', + }, + { + name: '@Aside-Inline', + path: '/drafts/nala/blocks/aside/aside-inline', + browserParams: '?georouting=off', + tags: '@aside @aside-inline @smoke @regression @milo', + }, + { + name: '@Aside-Inline-Dark', + path: '/drafts/nala/blocks/aside/aside-inline-dark', + browserParams: '?georouting=off', + tags: '@aside @aside-inline-dark @smoke @regression @milo', + }, + { + name: '@Aside-Notif-Extra-Small-Dark', + path: '/drafts/nala/blocks/aside/aside-notification-extrasmall-dark', + browserParams: '?georouting=off', + tags: '@aside @aside-notif-extra-small-dark @smoke @regression @milo', + }, + { + name: '@Aside-Notif-Small', + path: '/drafts/nala/blocks/aside/aside-notification-small', + browserParams: '?georouting=off', + tags: '@aside @aside-notif-small @smoke @regression @milo', + }, + { + name: '@Aside-Notif-Medium', + path: '/drafts/nala/blocks/aside/aside-notification-medium', + browserParams: '?georouting=off', + tags: '@aside @aside-notif-medium @smoke @regression @milo', + }, + { + name: '@Aside-Notif-Medium-Center', + path: '/drafts/nala/blocks/aside/aside-notification-medium-center', + browserParams: '?georouting=off', + tags: '@aside @aside-notif-medium-center @smoke @regression @milo', + }, + { + name: '@Aside-Notif-Large', + path: '/drafts/nala/blocks/aside/aside-notification-large', + browserParams: '?georouting=off', + tags: '@aside @aside-notif-large @smoke @regression @milo', + }, + { + name: '@Aside-Notif-Large-Center', + path: '/drafts/nala/blocks/aside/aside-notification-large-center', + browserParams: '?georouting=off', + tags: '@aside @aside-notif-large-center @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/aside/aside.test.js b/nala/blocks/aside/aside.test.js new file mode 100644 index 0000000000..2fe6e659f1 --- /dev/null +++ b/nala/blocks/aside/aside.test.js @@ -0,0 +1,525 @@ +import { expect, test } from '@playwright/test'; +import { features } from './aside.spec.js'; +import AsideBlock from './aside.page.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Aside Block test suite', () => { + // Aside Small Checks: + test(`${features[0].name}, ${features[0].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[0].path}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[0].path}${features[0].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${features[0].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSmall).toBeVisible(); + await expect(Aside.icon).toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h2TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.textLink).toBeVisible(); + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideSmall.evaluate( + (e) => window.getComputedStyle(e).getPropertyValue('background-color'), + ); + expect(bgdColor).toBe(Aside.props.background.lightGrey1); + }); + }); + + // Aside Medium Checks: + test(`${features[1].name}, ${features[1].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[1].path}${features[1].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${features[1].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideMedium).toBeVisible(); + await expect(Aside.icon).toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideMedium.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.lightGrey1); + }); + }); + + // Aside Large Checks: + test(`${features[2].name}, ${features[2].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[2].path}${features[2].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${features[2].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideLarge).toBeVisible(); + await expect(Aside.icon).toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).not.toBeVisible(); + await expect(Aside.linkTextCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideLarge.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.lightGrey1); + }); + }); + + // Aside Split Small Dark Checks: + test(`${features[3].name}, ${features[3].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[3].path}${features[3].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${features[3].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSplitSmallDark).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideSplitSmallDark.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.black); + }); + }); + + // Aside Split Small Half Dark Checks: + test(`${features[4].name}, ${features[4].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[4].path}${features[4].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${features[4].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSplitSmallHalfDark).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideSplitSmallHalfDark.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.black); + }); + }); + + // Aside Split Medium Checks: + test(`${features[5].name}, ${features[5].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[5].path}${features[5].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${features[5].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSplitMedium).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideSplitMedium.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.lightGrey3); + }); + }); + + // Aside Split Medium Half Checks: + test(`${features[6].name}, ${features[6].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[6].path}${features[6].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${features[6].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSplitMedidumHalf).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideSplitMedidumHalf.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.lightGrey3); + }); + }); + + // Aside Split Large Checks: + test(`${features[7].name}, ${features[7].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[7].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[7].path}${features[7].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[7].path}${features[7].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSplitLarge).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // !Note: Aside Split Large doesn't have default background! + }); + }); + + // Aside Split Large Half Dark Checks: + test(`${features[8].name}, ${features[8].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[8].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[8].path}${features[8].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[8].path}${features[8].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideSplitLargeHalfDark).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).toBeVisible(); + await expect(Aside.h3TitleXLarge).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideSplitLargeHalfDark.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.black); + }); + }); + + // Aside Inline Checks: + test(`${features[9].name}, ${features[9].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[9].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[9].path}${features[9].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[9].path}${features[9].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideInline).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h3TitleSmall).toBeVisible(); + await expect(Aside.textFieldMedium).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideInline.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.lightGrey2); + }); + }); + + // Aside Inline Dark Checks: + test(`${features[10].name}, ${features[10].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[10].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[10].path}${features[10].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[10].path}${features[10].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideInline).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h3TitleSmall).toBeVisible(); + await expect(Aside.textFieldMedium).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + await expect(Aside.linkTextCta).not.toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideInline.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.black); + }); + }); + + // Aside Notification Extra Small Dark: + test(`${features[11].name}, ${features[11].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[11].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[11].path}${features[11].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[11].path}${features[11].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideNotifExtraSmallDark).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.noImage).toBeVisible(); + await expect(Aside.actionArea).not.toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h2TitleSmall).not.toBeVisible(); + await expect(Aside.h2TitleXLarge).not.toBeVisible(); + await expect(Aside.h3TitleSmall).not.toBeVisible(); + await expect(Aside.h3TitleXLarge).not.toBeVisible(); + await expect(Aside.textFieldSmall).not.toBeVisible(); + await expect(Aside.textFieldMedium).not.toBeVisible(); + await expect(Aside.textFieldLarge).not.toBeVisible(); + // Check Aside block buttons: + await expect(Aside.linkTextCta.first()).toBeVisible(); + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).not.toBeVisible(); + expect(await Aside.actionLinks.count()).toEqual(2); + // Check Aside block background: + const bgdColor = await Aside.asideNotifExtraSmallDark.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.black); + }); + }); + + // Aside Notification Small: + test(`${features[12].name}, ${features[12].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[12].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[12].path}${features[12].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[12].path}${features[12].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideNotifSmall).toBeVisible(); + await expect(Aside.icon).toBeVisible(); + await expect(Aside.image).not.toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h2TitleSmall).not.toBeVisible(); + await expect(Aside.h2TitleXLarge).not.toBeVisible(); + await expect(Aside.h3TitleSmall).not.toBeVisible(); + await expect(Aside.h3TitleXLarge).not.toBeVisible(); + await expect(Aside.textFieldMedium).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.textLink).toBeVisible(); + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideNotifSmall.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.darkGrey); + }); + }); + + // Aside Notification Medium: + test(`${features[13].name}, ${features[13].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[13].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[13].path}${features[13].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[13].path}${features[13].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideNotifMedium).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h3TitleSmall).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.linkTextCta).toBeVisible(); + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideNotifMedium.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.darkGrey); + }); + }); + + // Aside Notification Medium Center: + test(`${features[14].name}, ${features[14].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[14].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[14].path}${features[14].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[14].path}${features[14].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideNotifMedium).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).not.toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h3TitleSmall).toBeVisible(); + await expect(Aside.textFieldSmall).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.linkTextCta).toBeVisible(); + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideNotifMedium.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.darkGrey); + }); + }); + + // Aside Notification Large: + test(`${features[15].name}, ${features[15].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[15].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[15].path}${features[15].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[15].path}${features[15].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideNotifLarge).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h3TitleLarge).toBeVisible(); + await expect(Aside.textFieldMedium).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.linkTextCta).toBeVisible(); + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideNotifLarge.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.darkGrey); + }); + }); + + // Aside Notification Large Center: + test(`${features[16].name}, ${features[16].tags}`, async ({ page, baseURL }) => { + const Aside = new AsideBlock(page); + console.info(`[Test Page]: ${baseURL}${features[16].path}${miloLibs}`); + + await test.step('Navigate to page with Aside block', async () => { + await page.goto(`${baseURL}${features[16].path}${features[16].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[16].path}${features[16].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Aside block content', async () => { + await expect(Aside.asideNotifLargeCenter).toBeVisible(); + await expect(Aside.icon).not.toBeVisible(); + await expect(Aside.image).not.toBeVisible(); + await expect(Aside.actionArea).toBeVisible(); + await expect(Aside.detailLabel).not.toBeVisible(); + await expect(Aside.h3TitleLarge).toBeVisible(); + await expect(Aside.textFieldMedium).toBeVisible(); + // Check Aside block buttons: + await expect(Aside.linkTextCta).toBeVisible(); + await expect(Aside.blueButtonCta).not.toBeVisible(); + await expect(Aside.blackButtonCta).toBeVisible(); + expect(await Aside.actionButtons.count()).toEqual(1); + // Check Aside block background: + const bgdColor = await Aside.asideNotifLargeCenter.evaluate((e) => window.getComputedStyle(e).getPropertyValue('background-color')); + expect(bgdColor).toBe(Aside.props.background.darkGrey); + }); + }); +}); diff --git a/nala/blocks/card/card.page.js b/nala/blocks/card/card.page.js new file mode 100644 index 0000000000..3e634e38ca --- /dev/null +++ b/nala/blocks/card/card.page.js @@ -0,0 +1,57 @@ +export default class Card { + constructor(page, nth = 0) { + this.page = page; + // card locators + this.card = this.page.locator('.card').nth(nth); + + // One half card locators + this.oneHalfCard = this.page.locator('.consonant-OneHalfCard').nth(nth); + this.oneHalfCardImage = this.oneHalfCard.locator('.consonant-OneHalfCard-img'); + this.oneHalfCardInner = this.oneHalfCard.locator('.consonant-OneHalfCard-inner'); + this.oneHalfCardTitleH3 = this.oneHalfCard.locator('h3.consonant-OneHalfCard-title'); + this.oneHalfCardText = this.oneHalfCard.locator('.consonant-OneHalfCard-text'); + // Double width card locators + this.doubleWidthCard = this.page.locator('.double-width-card').nth(nth); + this.doubleWidthCardImage = this.doubleWidthCard.locator('.consonant-DoubleWideCard-img'); + this.doubleWidthCardInner = this.doubleWidthCard.locator('.consonant-DoubleWideCard-inner'); + this.doubleWidthCardTitleH3 = this.doubleWidthCard.locator('h3.consonant-DoubleWideCard-title'); + this.doubleWidthCardText = this.doubleWidthCard.locator('.consonant-DoubleWideCard-text'); + // Product card locators + this.productCard = this.page.locator('.product-card').nth(nth); + this.productCardImage = this.productCard.locator('.consonant-ProductCard-img'); + this.productCardInner = this.productCard.locator('.consonant-ProductCard-inner'); + this.productCardTitleH3 = this.productCard.locator('h3.consonant-ProductCard-title'); + this.productCardText = this.productCard.locator('.consonant-ProductCard-text'); + // Half height card locators + this.halfHeightCard = this.page.locator('.half-height-card').nth(nth); + this.halfHeightCardImage = this.halfHeightCard.locator('.consonant-HalfHeightCard-img'); + this.halfHeightCardLink = this.halfHeightCard.locator('a.consonant-HalfHeightCard'); + this.halfHeightCardInner = this.halfHeightCard.locator('.consonant-HalfHeightCard-inner'); + this.halfHeightCardTitleH3 = this.halfHeightCard.locator('h3.consonant-HalfHeightCard-title'); + this.halfHeightCardText = this.halfHeightCard.locator('.consonant-HalfHeightCard-text'); + // Horizontal card locators + this.horizontalCard = this.page.locator('.card-horizontal').nth(nth); + this.horizontalCardImage = this.horizontalCard.locator('.card-image'); + this.horizontalCardImg = this.horizontalCard.locator('img'); + this.horizontalCardContent = this.horizontalCard.locator('card-content'); + this.horizontalCardBodyXS = this.horizontalCard.locator('.body-xs'); + this.horizontalCardHeadingXS = this.horizontalCard.locator('h2.heading-xs'); + this.horizontalCardHeadingXSLink = this.horizontalCardHeadingXS.locator('a'); + + // card footer sections + this.footer = this.card.locator('.consonant-CardFooter'); + this.footerOutlineButton = this.card.locator('a.con-button.outline').nth(0); + this.footerOutlineButton2 = this.card.locator('a.con-button.outline').nth(1); + this.footerBlueButton = this.card.locator('a.con-button.blue').nth(0); + this.footerBlueButton2 = this.card.locator('a.con-button.blue').nth(1); + + // card attributes + this.attributes = { + oneHalfCardImage: { style: 'background-image: url' }, + 'card-image': { + loading: 'eager', + fetchpriority: 'hight', + }, + }; + } +} diff --git a/nala/blocks/card/card.spec.js b/nala/blocks/card/card.spec.js new file mode 100644 index 0000000000..bafea2898f --- /dev/null +++ b/nala/blocks/card/card.spec.js @@ -0,0 +1,84 @@ +/* eslint-disable max-len */ + +module.exports = { + FeatureName: 'Consonant Card Block', + features: [ + { + tcid: '0', + name: '@Card', + path: '/drafts/nala/blocks/card/card', + data: { + titleH3: 'Lorem ipsum dolor sit amet', + text: 'Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis', + footerOutlineButtonText: 'Sign up', + footerBlueButtonText: 'Learn more', + }, + tags: '@card @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Card (half-card, border)', + path: '/drafts/nala/blocks/card/half-card-border', + data: { + titleH3: 'Lorem ipsum dolor sit amet', + text: 'Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis', + footerOutlineButtonText: 'Sign up', + footerBlueButtonText: 'Learn more', + }, + tags: '@card @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Card (double-width-card, border)', + path: '/drafts/nala/blocks/card/double-width-card-border', + data: { + titleH3: 'Lorem ipsum dolor sit amet', + text: 'Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis', + }, + tags: '@card @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Card (product-card, border) ', + path: '/drafts/nala/blocks/card/product-card-border', + data: { + titleH3: 'Lorem ipsum dolor sit amet', + text: 'Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis', + footerOutlineButtonText: 'Learn more', + footerBlueButtonText: 'Sign up', + }, + tags: '@card @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Card (half-height-card, border)', + path: '/drafts/nala/blocks/card/half-height-card-border', + data: { titleH3: 'Lorem ipsum dolor sit amet' }, + tags: '@card @smoke @regression @milo', + }, + { + tcid: '5', + name: '@Card-horizontal', + path: '/drafts/nala/blocks/card/card-horizontal', + data: { + bodyXS: 'Body XS Regular', + headingXS: 'Heading XS Bold Lorem ipsum dolo sit amet, consectetur adipis cing elit.', + imgWidth: '180', + imgHeight: '132', + }, + tags: '@card @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Card-horizontal (tile)', + path: '/drafts/nala/blocks/card/card-horizontal-tile', + data: { + bodyXS: 'Body XS Regular', + headingXS: 'Heading XS Bold Lorem ipsum dolo sit amet, consectetur adipis cing elit.', + imgWidth: '80', + imgHeight: '78', + }, + tags: '@card @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/card/card.test.js b/nala/blocks/card/card.test.js new file mode 100644 index 0000000000..bac6dbddd6 --- /dev/null +++ b/nala/blocks/card/card.test.js @@ -0,0 +1,192 @@ +import { expect, test } from '@playwright/test'; +import { features } from './card.spec.js'; +import ConsonantCard from './card.page.js'; + +let card; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Consonant card feature test suite', () => { + test.beforeEach(async ({ page }) => { + card = new ConsonantCard(page); + }); + + // Test 0 : Card + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Card content/specs', async () => { + await expect(await card.oneHalfCard).toBeVisible(); + await expect(await card.oneHalfCardImage).toBeVisible(); + + await expect(await card.oneHalfCardTitleH3).toContainText(data.titleH3); + await expect(await card.oneHalfCardText).toContainText(data.text); + + await expect(await card.footer).toBeVisible(); + await expect(await card.footerOutlineButton).toBeVisible(); + await expect(await card.footerOutlineButton).toContainText(data.footerOutlineButtonText); + + await expect(await card.footerBlueButton).toBeVisible(); + await expect(await card.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 1 : Card (half-card, border) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Half Card with Boarder content/specs', async () => { + await expect(await card.oneHalfCard).toBeVisible(); + + expect(await card.oneHalfCard.getAttribute('class')).toContain('border'); + + await expect(await card.oneHalfCardImage).toBeVisible(); + await expect(await card.oneHalfCardTitleH3).toContainText(data.titleH3); + await expect(await card.oneHalfCardText).toContainText(data.text); + + await expect(await card.footer).toBeVisible(); + await expect(await card.footerOutlineButton).toBeVisible(); + await expect(await card.footerOutlineButton).toContainText(data.footerOutlineButtonText); + + await expect(await card.footerBlueButton).toBeVisible(); + await expect(await card.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 2 : card (double-width-card, border) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-2: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify card (double-width-card, border) content/specs', async () => { + await expect(await card.doubleWidthCard).toBeVisible(); + + expect(await card.doubleWidthCard.getAttribute('class')).toContain('border'); + + await expect(await card.doubleWidthCardImage).toBeVisible(); + await expect(await card.doubleWidthCardTitleH3).toContainText(data.titleH3); + await expect(await card.doubleWidthCardText).toContainText(data.text); + }); + }); + + // Test 3 : Card (product-card, border) + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-2: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Card (product-card, border) content/specs', async () => { + await expect(await card.productCard).toBeVisible(); + + expect(await card.productCard.getAttribute('class')).toContain('border'); + + await expect(await card.productCardTitleH3).toContainText(data.titleH3); + await expect(await card.productCardText).toContainText(data.text); + + await expect(await card.footer).toBeVisible(); + await expect(await card.footerOutlineButton).toBeVisible(); + await expect(await card.footerOutlineButton).toContainText(data.footerOutlineButtonText); + + await expect(await card.footerBlueButton).toBeVisible(); + await expect(await card.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 4 : Card (half-height-card, border) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-2: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Card (half-height-card, border) content/specs', async () => { + await expect(await card.halfHeightCard).toBeVisible(); + + expect(await card.halfHeightCard.getAttribute('class')).toContain('border'); + + await expect(await card.halfHeightCardImage).toBeVisible(); + await expect(await card.halfHeightCardLink).toBeVisible(); + + await expect(await card.halfHeightCardTitleH3).toContainText(data.titleH3); + }); + }); + + // Test 5 : Card-horizontal + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-2: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Card-horizontal content/specs', async () => { + await expect(await card.horizontalCard).toBeVisible(); + await expect(await card.horizontalCardImage).toBeVisible(); + + expect(await card.horizontalCardImg.getAttribute('width')).toContain(data.imgWidth); + expect(await card.horizontalCardImg.getAttribute('height')).toContain(data.imgHeight); + + await expect(await card.horizontalCardBodyXS).toContainText(data.bodyXS); + await expect(await card.horizontalCardHeadingXS).toContainText(data.headingXS); + await expect(await card.horizontalCardHeadingXSLink).toContainText(data.headingXS); + }); + }); + + // Test 6 : Card-horizontal (tile) + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-2: Go to Consonant Card feature test page', async () => { + await page.goto(`${baseURL}${features[6].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Card-horizontal (tile) content/specs', async () => { + await expect(await card.horizontalCard).toBeVisible(); + expect(await card.horizontalCard.getAttribute('class')).toContain('tile'); + + await expect(await card.horizontalCardImage).toBeVisible(); + + expect(await card.horizontalCardImg.getAttribute('width')).toContain(data.imgWidth); + expect(await card.horizontalCardImg.getAttribute('height')).toContain(data.imgHeight); + + await expect(await card.horizontalCardBodyXS).toContainText(data.bodyXS); + await expect(await card.horizontalCardHeadingXS).toContainText(data.headingXS); + await expect(await card.horizontalCardHeadingXSLink).toContainText(data.headingXS); + }); + }); +}); diff --git a/nala/blocks/carousel/carousel.page.js b/nala/blocks/carousel/carousel.page.js new file mode 100644 index 0000000000..e121a54d69 --- /dev/null +++ b/nala/blocks/carousel/carousel.page.js @@ -0,0 +1,222 @@ +export default class Carousel { + constructor(page) { + this.page = page; + // carousel types selectors + this.carouselContainer = page.locator('.carousel.container'); + this.carouselLightbox = page.locator('.carousel.lightbox'); + this.carouselContainerShow2 = page.locator('.carousel.show-2.container'); + this.carousel = page.locator('.carousel'); + + // carousel selectors + this.slides = this.carousel.locator('.carousel-slides'); + this.activeSlide = this.slides.locator('.section.carousel-slide.active'); + this.slidesCount = this.slides.locator('.section.carousel-slide'); + this.indicator = this.carousel.locator('.carousel-indicators'); + this.indicatorCount = this.indicator.locator('.carousel-indicator'); + this.activeIndicator = this.indicator.locator('.carousel-indicator.active'); + this.nextButton = this.carousel.locator('.carousel-next'); + this.previousButton = this.carousel.locator('.carousel-previous'); + this.lightboxExpandButton = this.carouselLightbox.locator('.lightbox-button.carousel-expand'); + this.lightboxCloseButton = this.carouselLightbox.locator('.lightbox-button.carousel-close'); + } + + /** + * Get the index of the current slide. + * @return {Promise}. + */ + async getCurrentSlideIndex() { + const currentIndex = await this.activeSlide.getAttribute('data-index'); + return currentIndex; + } + + /** + * Get the count of slides of carousel. + * @return {Promise}. + */ + async getNumberOfSlides() { + const numberOfSlides = await this.slidesCount.count(); + return numberOfSlides; + } + + /** + * Move to next slide . + */ + async moveToNextSlide() { + await this.nextButton.click(); + } + + /** + * Move to previous slide . + */ + async moveToPreviousSlide() { + await this.previousButton.click(); + } + + /** + * Move to nth slide . + */ + async moveToSlide(index) { + await this.indicator.nth(index).click(); + } + + /** + * Are carousel indictors are displayed. + * @return {Promise}. + */ + async areIndicatorsDisplayed() { + const isDisplayed = await this.indicator.isVisible(); + return isDisplayed; + } + + /** + * Get the active indictor index. + * @return {Promise}. + */ + async getCurrentIndicatorIndex() { + const currentIndex = await this.activeIndicator.getAttribute('tabindex'); + return currentIndex; + } + + /** + * Get the count of indicators of carousel. + * @return {Promise}. + */ + async getNumberOfIndicators() { + const numberOfIndicators = await this.indicatorCount.count(); + return numberOfIndicators; + } + + /** + * Move to nth slide by clicking nth indicator + */ + async moveToIndicator(index) { + await this.indicatorCount.nth(index).click(); + } + + /** + * Check carousel button is visible + * @return {Promise}. + */ + async isNextButtonlVisible() { + const isDisplayed = await this.nextButton.isVisible(); + return isDisplayed; + } + + /** + * Check carousel button is visible + * @return {Promise}. + */ + async isPreviousButtonlVisible() { + const isDisplayed = await this.previousButton.isVisible(); + return isDisplayed; + } + + /** + * Check carousel button is visible + * @return {Promise}. + */ + async isLightboxExpandButtonVisible() { + const isDisplayed = await this.lightboxExpandButton.isVisible(); + return isDisplayed; + } + + /** + * Check carousel button is visible + * @return {Promise}. + */ + async isLightboxCloseButtonVisible() { + const isDisplayed = await this.lightboxCloseButton.isVisible(); + return isDisplayed; + } + + /** + * Click carousel button + */ + async expandLightboxModal() { + await this.lightboxExpandButton.click(); + } + + /** + * Click carousel button + */ + async closeLightboxModal() { + await this.lightboxCloseButton.click(); + } + + /** + * Gets the text content of a specified carousel slide. + * @param {number} index - The index of the carousel slide to get the text from. + * @param {string} tagOrClass - The tag name or class name of the element containing the text. + * @returns {Promise} The text content of the specified carousel slide. + */ + async getSlideText(index, tagOrClass) { + let slideSelector = `.carousel-slide:nth-child(${index}) `; + if (tagOrClass.startsWith('.')) { + slideSelector += tagOrClass; + } else { + slideSelector += `${tagOrClass}`; + } + await this.page.waitForSelector(slideSelector); + const slide = await this.page.$(slideSelector); + const text = await slide.textContent(); + return text; + } + + /** + * Gets the text content of a specified carousel slide. + * @param {number} index - The index of the carousel slide to get the text from. + * @param {string} tagOrClass - The tag name or class name of the element containing the text. + * @param {string} expectedText - The text to be validated. + * @returns {Promise} . + */ + async validateSlideText(index, tagOrClass, expectedText) { + let slideSelector = `.carousel-slide:nth-child(${index}) `; + if (tagOrClass.startsWith('.')) { + slideSelector += tagOrClass; + } else { + slideSelector += `${tagOrClass}`; + } + await this.page.waitForSelector(slideSelector); + const slide = await this.page.$(slideSelector); + const slideText = await slide.textContent(); + if (slideText === expectedText) { + return true; + } + return false; + } + + /** + * Check if the specified carousel type is displayed on the page. + * @param {string} type - The type of carousel to check. + * @return {Promise} Returns a Promise that resolves to true or false. + * @throws {Error} Throws an error if an invalid carousel type is provided. + */ + async isCarouselDisplayed(type, timeout = 3000) { + let isDisplayed; + switch (type) { + case 'carouselLightbox': + await this.carouselLightbox.waitFor({ state: 'visible', timeout }); + isDisplayed = await this.carouselLightbox.isVisible(); + break; + case 'carouselFullpage': + await this.carouselFullpage.waitFor({ state: 'visible', timeout }); + isDisplayed = await this.carouselFullpage.isVisible(); + break; + case 'carouselContainer': + await this.carouselContainer.waitFor({ state: 'visible', timeout }); + isDisplayed = await this.carouselContainer.isVisible(); + break; + case 'carouselShow-2': + await this.carouselContainerShow2.waitFor({ state: 'visible', timeout }); + isDisplayed = await this.carouselContainerShow2.isVisible(); + break; + case 'carousel': + await this.carouselDefault.waitFor({ state: 'visible', timeout }); + isDisplayed = await this.carouselDefault.isVisible(); + break; + default: + throw new Error(`Invalid carousel type: ${type}`); + } + return isDisplayed; + } +} diff --git a/nala/blocks/carousel/carousel.spec.js b/nala/blocks/carousel/carousel.spec.js new file mode 100644 index 0000000000..ffc52f7de7 --- /dev/null +++ b/nala/blocks/carousel/carousel.spec.js @@ -0,0 +1,26 @@ +module.exports = { + BlockName: 'Carousel Block', + features: [ + { + tcid: '0', + name: '@Carousel(container)', + path: '/drafts/nala/blocks/carousel/lightbox', + tags: '@carousel @carousel-container @smoke @regression @milo', + envs: '@milo-live @milo-prod', + }, + { + tcid: '1', + name: '@Carousel(lightbox)', + path: '/drafts/nala/blocks/carousel/fullpage-carousel', + tags: '@carousel @carousel-container @smoke @regression @milo', + envs: '@milo-live milo-prod', + }, + { + tcid: '2', + name: '@Carousel Multi slide(show-2)', + path: '/drafts/nala/blocks/carousel/carousel-show-2', + tags: '@carousel @carousel-container @regression @milo', + envs: '@milo-live milo-prod', + }, + ], +}; diff --git a/nala/blocks/carousel/carousel.test.js b/nala/blocks/carousel/carousel.test.js new file mode 100644 index 0000000000..2a9136e4dd --- /dev/null +++ b/nala/blocks/carousel/carousel.test.js @@ -0,0 +1,115 @@ +import { expect, test } from '@playwright/test'; +import { features } from './carousel.spec.js'; +import CarouselBlock from './carousel.page.js'; + +let carousel; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Carousel Block test suite', () => { + test.beforeEach(async ({ page }) => { + carousel = new CarouselBlock(page); + }); + + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to Carousel block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('networkidle'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Carousel container', async () => { + // verify carousel elements + expect(await carousel.isCarouselDisplayed('carouselContainer')).toBeTruthy(); + + // verify carousel slides count and active slide index + expect(await carousel.getNumberOfSlides()).toBe(4); + expect(await carousel.getCurrentSlideIndex()).toBe('0'); + + // verify carousel indictor and active indicator + expect(await carousel.areIndicatorsDisplayed()).toBeTruthy(); + expect(await carousel.getNumberOfIndicators()).toBe(4); + expect(await carousel.getCurrentIndicatorIndex()).toBe('0'); + + // verify carousel next and previous buttons + expect(await carousel.isNextButtonlVisible()).toBeTruthy(); + expect(await carousel.isPreviousButtonlVisible()).toBeTruthy(); + }); + + await test.step('step-3: Perform carousel slides and controls operation and verify contents', async () => { + // move to next slide by clicking next button and verify h2 tag header + await carousel.moveToNextSlide(); + expect(await carousel.getCurrentSlideIndex()).toBe('1'); + expect(await carousel.getSlideText(1, 'h2', 'Orange Slices')).toBeTruthy(); + + // move to 3rd slide by clicking indicator and verify h2 tag header + await carousel.moveToIndicator(3); + expect(await carousel.getCurrentIndicatorIndex()).toBe('0'); + expect(await carousel.getSlideText(3, 'h2', 'Apples')).toBeTruthy(); + }); + }); + + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to Carousel lightbox block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('networkidle'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify carousel with lightbox features', async () => { + expect(await carousel.isCarouselDisplayed('carouselLightbox')).toBeTruthy(); + + // verify active slide and slides count + expect(await carousel.getNumberOfSlides()).toBe(4); + expect(await carousel.getCurrentSlideIndex()).toBe('0'); + + // verify indicator visibility, count and index of active slide + expect(await carousel.areIndicatorsDisplayed()).toBeTruthy(); + expect(await carousel.getNumberOfIndicators()).toBe(4); + expect(await carousel.getCurrentIndicatorIndex()).toBe('0'); + + expect(await carousel.isNextButtonlVisible()).toBeTruthy(); + expect(await carousel.isPreviousButtonlVisible()).toBeTruthy(); + + // verify expand and close lightbox + expect(await carousel.isLightboxExpandButtonVisible()).toBeTruthy(); + await carousel.expandLightboxModal(); + + expect(await carousel.isLightboxCloseButtonVisible()).toBeTruthy(); + await carousel.closeLightboxModal(); + }); + }); + + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + + await test.step('step-1: Go to Carousel multi-slide show-2 block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('networkidle'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify multi slide carousel show-2 features', async () => { + expect(await carousel.isCarouselDisplayed('carouselShow-2')).toBeTruthy(); + + // In multi-slide 2 number of slides will be n-slides +1 so it will be 5 + expect(await carousel.getNumberOfSlides()).toBe(5); + expect(await carousel.getCurrentSlideIndex()).toBe('0'); + + // In multi-slide carousel indicators are not shown + expect(await carousel.areIndicatorsDisplayed()).toBeFalsy(); + expect(await carousel.isNextButtonlVisible()).toBeTruthy(); + expect(await carousel.isPreviousButtonlVisible()).toBeTruthy(); + }); + + await test.step('step-3: Perform carousel slides and controls operation and verify contents', async () => { + // move to next slide by clicking next button and verify h2 tag header + await carousel.moveToNextSlide(); + expect(await carousel.getSlideText(1, 'h2', 'Melon')).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/chart/chart.page.js b/nala/blocks/chart/chart.page.js new file mode 100644 index 0000000000..91576c07ce --- /dev/null +++ b/nala/blocks/chart/chart.page.js @@ -0,0 +1,49 @@ +export default class Chart { + constructor(page, nth = 0) { + this.page = page; + // chart locators + this.chart = this.page.locator('.chart').nth(nth); + this.type = this.page.locator('.chart').nth(nth); + + this.title = this.chart.locator('.title'); + this.subTitle = this.chart.locator('.subtitle').nth(0); + this.container = this.chart.locator('.chart-container'); + this.svgImg = this.container.locator('svg'); + this.svgImgCircle = this.container.locator('circle'); + this.svgImgCircleNumber = this.container.locator('text.number'); + this.svgImgCircleSubTitle = this.container.locator('text.subtitle'); + + this.legendChrome = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Chrome' }); + this.legendFirefox = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Firefox' }); + this.legendEdge = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Edge' }); + this.legendSafari = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Safari' }); + this.legendOpera = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Opera' }); + this.legendChromeAndroid = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Chrome Android' }); + this.legendFirefoxAndroid = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Firefox Android' }); + this.legendSafariIos = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Safari iOS' }); + this.legendOperaAndroid = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Opera Android' }); + this.legendSamsungInternet = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Samsung Internet' }); + this.legendAdobeAcrobat = page.locator('text[dominant-baseline="central"][fill="#333"]').filter({ hasText: 'Adobe Acrobat' }); + this.legendAdobeExperienceManager = page.locator('text[dominant-baseline="central"][fill="#333"]').filter({ hasText: 'Adobe Experience Manager' }); + + this.x_axisMonday = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Monday' }); + this.x_axisTuesday = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Tuesday' }); + this.x_axisSunday = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Sunday' }); + this.y_axis50K = page.locator('text[dominant-baseline="central"]').filter({ hasText: '50K', exact: true }); + this.y_axis100K = page.locator('text[dominant-baseline="central"]').filter({ hasText: '100K', exact: true }); + this.y_axis250K = page.locator('text[dominant-baseline="central"]').filter({ hasText: '250K', exact: true }); + this.y_axis300K = page.locator('text[dominant-baseline="central"]').filter({ hasText: '300K', exact: true }); + + this.donutTitle = page.locator('text[dominant-baseline="central"]').filter({ hasText: 'Hello World', exact: true }); + + this.pieChartLabelAdobeSign = page.locator('text[dominant-baseline="central"][text-anchor="end"]').filter({ hasText: 'Adobe Sign' }); + this.pieChartLabelAdobePhotoshop = page.locator('text[dominant-baseline="central"][text-anchor="end"]').filter({ hasText: 'Adobe Photoshop' }); + this.pieChartLabelAdobePremier = page.locator('text[dominant-baseline="central"][text-anchor="end"]').filter({ hasText: 'Adobe Premier' }); + + // chart footer + this.footNote = this.chart.locator('.footnote'); + + // chart attributes + this.attributes = { svgViewBox: { viewBox: '0 0 430 430' } }; + } +} diff --git a/nala/blocks/chart/chart.spec.js b/nala/blocks/chart/chart.spec.js new file mode 100644 index 0000000000..b8ee9b1e52 --- /dev/null +++ b/nala/blocks/chart/chart.spec.js @@ -0,0 +1,91 @@ +module.exports = { + FeatureName: 'Chart Block', + features: [ + { + tcid: '0', + name: '@Chart (area, green, border)', + path: '/drafts/nala/blocks/chart/chart-area-green-border', + data: { + chartType: 'area green border', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Chart (bar, border)', + path: '/drafts/nala/blocks/chart/chart-bar-border', + data: { + chartType: 'bar border large', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Chart (column, border)', + path: '/drafts/nala/blocks/chart/chart-column-border', + data: { + chartType: 'column border large', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Chart (donut, border)', + path: '/drafts/nala/blocks/chart/chart-donut-border', + data: { + chartType: 'donut border large', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Chart (line, border)', + path: '/drafts/nala/blocks/chart/chart-line-border', + data: { + chartType: 'line border large', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + { + tcid: '5', + name: '@Chart (oversized-number, border)', + path: '/drafts/nala/blocks/chart/chart-oversized-number-border', + data: { + chartType: 'oversized-number border large', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + circleNumber: '25', + circleSubTitle: 'Out of 60 days', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Chart (pie, border)', + path: '/drafts/nala/blocks/chart/chart-pie-border', + data: { + chartType: 'pie border large', + titleH3: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + subTitle: 'Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.', + footNote: 'Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.', + }, + tags: '@chart @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/chart/chart.test.js b/nala/blocks/chart/chart.test.js new file mode 100644 index 0000000000..e8e82942e0 --- /dev/null +++ b/nala/blocks/chart/chart.test.js @@ -0,0 +1,197 @@ +import { expect, test } from '@playwright/test'; +import { features } from './chart.spec.js'; +import ChartBlock from './chart.page.js'; + +let chart; +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Chart feature test suite', () => { + test.beforeEach(async ({ page }) => { + chart = new ChartBlock(page); + }); + + // Test 0 : Chart (area, green, border) + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + expect(await chart.type.getAttribute('class')).toContain(data.chartType); + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); + + // Test 1 : Chart (bar, border) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + expect(await chart.type.getAttribute('class')).toContain(data.chartType); + await expect(await chart.legendChrome).toBeVisible(); + await expect(await chart.legendFirefox).toBeVisible(); + await expect(await chart.legendEdge).toBeVisible(); + + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); + + // Test 2 : Chart (column, border) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[2].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + expect(await chart.type.getAttribute('class')).toContain(data.chartType); + + await expect(await chart.x_axisMonday).toBeVisible(); + await expect(await chart.x_axisTuesday).toBeVisible(); + await expect(await chart.x_axisSunday).toBeVisible(); + + await expect(await chart.y_axis100K).toBeVisible(); + await expect(await chart.y_axis250K).toBeVisible(); + await expect(await chart.y_axis300K).toBeVisible(); + + await expect(await chart.legendChrome).toBeVisible(); + await expect(await chart.legendFirefox).toBeVisible(); + await expect(await chart.legendSafari).toBeVisible(); + + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); + + // Test 3 : Chart (donut, border) + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + expect(await chart.type.getAttribute('class')).toContain(data.chartType); + + await expect(await chart.donutTitle).toBeVisible(); + await expect(await chart.x_axisMonday).toBeVisible(); + await expect(await chart.x_axisTuesday).toBeVisible(); + await expect(await chart.x_axisSunday).toBeVisible(); + + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); + + // Test 4 : Chart (line, border) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + expect(await chart.type.getAttribute('class')).toContain(data.chartType); + + await expect(await chart.x_axisMonday).toBeVisible(); + await expect(await chart.x_axisTuesday).toBeVisible(); + await expect(await chart.x_axisSunday).toBeVisible(); + + await expect(await chart.legendChromeAndroid).toBeVisible(); + await expect(await chart.legendFirefoxAndroid).toBeVisible(); + await expect(await chart.legendSafariIos).toBeVisible(); + + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); + + // Test 5 : Chart (oversized-number, border) + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + + expect(await chart.type.getAttribute('class')).toContain(data.chartType); + expect(await chart.svgImg.getAttribute('viewBox')).toContain(chart.attributes.svgViewBox.viewBox); + + await expect(await chart.svgImgCircleNumber).toContainText(data.circleNumber); + await expect(await chart.svgImgCircleSubTitle).toContainText(data.circleSubTitle); + + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); + + // Test 6 : Chart (pie, border) + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-1: Go to Chart feature test page', async () => { + await page.goto(`${baseURL}${features[6].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify chart content/specs', async () => { + await expect(await chart.chart).toBeVisible(); + await expect(await chart.title).toContainText(data.titleH3); + await expect(await chart.subTitle).toContainText(data.subTitle); + + await expect(await chart.pieChartLabelAdobeSign).toBeVisible(); + await expect(await chart.pieChartLabelAdobePhotoshop).toBeVisible(); + await expect(await chart.pieChartLabelAdobePremier).toBeVisible(); + + await expect(await chart.legendAdobeAcrobat).toBeVisible(); + await expect(await chart.legendAdobeExperienceManager).toBeVisible(); + + await expect(await chart.footNote).toContainText(data.footNote); + }); + }); +}); diff --git a/nala/blocks/columns/columns.page.js b/nala/blocks/columns/columns.page.js new file mode 100644 index 0000000000..873a75982c --- /dev/null +++ b/nala/blocks/columns/columns.page.js @@ -0,0 +1,52 @@ +export default class Columns { + constructor(page, nth = 0) { + this.page = page; + // columns locators + this.column = this.page.locator('.columns').nth(nth); + this.rows = this.column.locator('.row'); + this.columns = this.column.locator('.col'); + + // columns blocks css + this.cssProperties = { + '.columns > .row': { + display: 'grid', + gap: /^32px.*/, + 'margin-bottom': '16px', + 'grid-template-columns': /^(\d+(?:\.\d+)?px\s?)+$/, + }, + + '.columns.contained': { + 'max-width': /^\d{2}/, + margin: /^0px.*/, + }, + + '.columns.contained.middle': { 'align-items': 'center' }, + + '.columns.table': { 'font-size': '14px' }, + + '.columns.table > .row:first-child': { + 'text-transform': 'uppercase', + 'font-size': '11px', + 'font-weight': '700', + 'letter-spacing': '1px', + }, + + '.columns.table > .row': { + 'margin-bottom': '0px', + padding: /^16px.*/, + 'grid-template-columns': /^(\d+(?:\.\d+)?px\s?)+$/, + 'border-bottom': /^1px.*/, + 'align-items': 'center', + }, + }; + + // columns blocks attributes + this.attProperties = { + columns: { class: 'columns' }, + 'columns-contained': { class: 'columns contained' }, + 'columns-contained-middle': { class: 'columns contained middle' }, + 'columns-table': { class: 'columns columns-table' }, + 'columns-contained-table': { class: 'columns contained columns-table' }, + }; + } +} diff --git a/nala/blocks/columns/columns.spec.js b/nala/blocks/columns/columns.spec.js new file mode 100644 index 0000000000..14a089b449 --- /dev/null +++ b/nala/blocks/columns/columns.spec.js @@ -0,0 +1,70 @@ +module.exports = { + FeatureName: 'Columns Block', + features: [ + { + tcid: '0', + name: '@Columns', + path: '/drafts/nala/blocks/columns/columns', + data: { + rows: 1, + columns: 3, + col0: 'Other glossary terms', + col1: 'Related Adobe products', + col2: 'Adobe Target', + }, + tags: '@columns @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Columns (contained)', + path: '/drafts/nala/blocks/columns/columns-contained', + data: { + rows: 1, + columns: 2, + col0: 'Market Segmentation', + col1: 'Adobe Analytics', + }, + tags: '@columns @columns-contained @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Columns (contained,middle)', + path: '/drafts/nala/blocks/columns/columns-contained-middle', + data: { + rows: 1, + columns: 2, + col0: 'Descriptive Analytics', + col1: 'Adobe Target', + }, + tags: '@columns @columns-contained-middle @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Columns (table)', + path: '/drafts/nala/blocks/columns/columns-table', + data: { + rows: 4, + columns: 8, + col0: 'PROS', + col1: 'CONS', + col2: 'Detail: Waterfall’s meticulous upfront planning results in detailed project plans.', + col3: 'Rigid: With a strict blueprint, departure from the original plan is difficult.', + }, + tags: '@columns @columns-table @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Columns (contained,table)', + path: '/drafts/nala/blocks/columns/columns-contained-table', + data: { + rows: 10, + columns: 20, + col0: 'Role', + col1: 'Name', + col2: 'Engineering Manager', + col3: 'Chris Millar', + }, + tags: '@columns @columns-contained-table @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/columns/columns.test.js b/nala/blocks/columns/columns.test.js new file mode 100644 index 0000000000..ccab01b1bb --- /dev/null +++ b/nala/blocks/columns/columns.test.js @@ -0,0 +1,160 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './columns.spec.js'; +import ColumnsBlock from './columns.page.js'; + +let column; +let webUtil; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Columns Block test suite', () => { + test.beforeEach(async ({ page }) => { + column = new ColumnsBlock(page); + webUtil = new WebUtil(page); + }); + + // Test 0 : Column default block + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Columns block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Columns block content/specs', async () => { + // verify colums visibility, rows count, columns count and text content + await expect(await column.column).toBeVisible(); + + await expect(await column.rows).toHaveCount(data.rows); + await expect(await column.columns).toHaveCount(data.columns); + + await expect(await column.columns.nth(0)).toContainText(data.col0); + await expect(await column.columns.nth(1)).toContainText(data.col1); + await expect(await column.columns.nth(2)).toContainText(data.col2); + + // verify the css and attributes + expect(await webUtil.verifyCSS(await column.rows.nth(0), column.cssProperties['.columns > .row'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(await column.column, column.attProperties.columns)).toBeTruthy(); + }); + }); + + // Test 1 : Columns (contained) block + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Columns block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Columns(contained) block content/specs', async () => { + // verify colums visibility, rows count, columns count and text content + await expect(await column.column).toBeVisible(); + + await expect(await column.rows).toHaveCount(data.rows); + await expect(await column.columns).toHaveCount(data.columns); + + await expect(await column.columns.nth(0)).toContainText(data.col0); + await expect(await column.columns.nth(1)).toContainText(data.col1); + + // verify the css and attributes + expect(await webUtil.verifyCSS(await column.column, column.cssProperties['.columns.contained'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(await column.column, column.attProperties['columns-contained'])).toBeTruthy(); + }); + }); + + // Test 2 : Columns (contained,middle) block + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Columns block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Columns(contained,middle) block content/specs', async () => { + // verify colums visibility, rows count, columns count and text content + await expect(await column.column).toBeVisible(); + + await expect(await column.rows).toHaveCount(data.rows); + await expect(await column.columns).toHaveCount(data.columns); + + await expect(await column.columns.nth(0)).toContainText(data.col0); + await expect(await column.columns.nth(1)).toContainText(data.col1); + + // verify the css and attributes + expect(await webUtil.verifyCSS(await column.column, column.cssProperties['.columns.contained'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(await column.column, column.attProperties['columns-contained-middle'])).toBeTruthy(); + }); + }); + + // Test 3 : Columns (table) block + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Columns block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Columns(table) block content/specs', async () => { + // verify colums visibility, rows count, columns count and text content + await expect(await column.column).toBeVisible(); + + await expect(await column.rows).toHaveCount(data.rows); + await expect(await column.columns).toHaveCount(data.columns); + + await expect(await column.columns.nth(0)).toContainText(data.col0); + await expect(await column.columns.nth(1)).toContainText(data.col1); + await expect(await column.columns.nth(2)).toContainText(data.col2); + await expect(await column.columns.nth(3)).toContainText(data.col3); + + // verify the css and attributes + expect(await webUtil.verifyCSS(await column.rows.nth(0), column.cssProperties['.columns.table > .row:first-child'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await column.rows.nth(1), column.cssProperties['.columns.table > .row'])).toBeTruthy(); + + expect(await webUtil.verifyAttributes(await column.column, column.attProperties['columns-table'])).toBeTruthy(); + }); + }); + + // Test 4 : Columns (contained,table) block + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Columns block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Columns(contained,table) block content/specs', async () => { + // verify colums visibility, rows count, columns count and text content + await expect(await column.column).toBeVisible(); + + await expect(await column.rows).toHaveCount(data.rows); + await expect(await column.columns).toHaveCount(data.columns); + + await expect(await column.columns.nth(0)).toContainText(data.col0); + await expect(await column.columns.nth(1)).toContainText(data.col1); + await expect(await column.columns.nth(2)).toContainText(data.col2); + await expect(await column.columns.nth(3)).toContainText(data.col3); + + // verify the css and attributes + expect(await webUtil.verifyCSS(await column.rows.nth(0), column.cssProperties['.columns.table > .row:first-child'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await column.rows.nth(1), column.cssProperties['.columns.table > .row'])).toBeTruthy(); + + expect(await webUtil.verifyAttributes(await column.column, column.attProperties['columns-contained-table'])).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/figure/figure.page.js b/nala/blocks/figure/figure.page.js new file mode 100644 index 0000000000..785efa832d --- /dev/null +++ b/nala/blocks/figure/figure.page.js @@ -0,0 +1,9 @@ +export default class Figure { + constructor(page) { + this.page = page; + // figure block locators + this.figure = this.page.locator('figure.figure'); + this.image = this.figure.locator('picture img'); + this.figCaption = this.figure.locator('figcaption .caption'); + } +} diff --git a/nala/blocks/figure/figure.spec.js b/nala/blocks/figure/figure.spec.js new file mode 100644 index 0000000000..84e8b37193 --- /dev/null +++ b/nala/blocks/figure/figure.spec.js @@ -0,0 +1,23 @@ +module.exports = { + FeatureName: 'Figure Block', + features: [ + { + tcid: '0', + name: '@Image with caption', + path: '/drafts/nala/blocks/figure/image-with-caption', + data: { figCaption: '100 Orange Captions' }, + tags: '@figure @smoke @regression @milo', + }, + { + tcid: '01', + name: '@Multiple images with caption', + path: '/drafts/nala/blocks/figure/multiple-images-with-captions', + data: { + figBlockCount: 2, + figCaption_1: 'Adobe Logo', + figCaption_2: 'Adobe Products', + }, + tags: '@figure @figure-with-mulitple-images @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/figure/figure.test.js b/nala/blocks/figure/figure.test.js new file mode 100644 index 0000000000..0486b96207 --- /dev/null +++ b/nala/blocks/figure/figure.test.js @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { features } from './figure.spec.js'; +import FigureBlock from './figure.page.js'; + +let figureBlock; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Figure Block test suite', () => { + test.beforeEach(async ({ page }) => { + figureBlock = new FigureBlock(page); + }); + + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to figure Block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify figure block ', async () => { + const { data } = features[0]; + await expect(await figureBlock.figure).toBeVisible(); + await expect(await figureBlock.image).toBeVisible(); + await expect(await figureBlock.figCaption).toContainText(data.figCaption); + }); + }); + + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('step-1: Go to figure block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify figure block multiple images with caption ', async () => { + const { data } = features[1]; + await expect(await figureBlock.figure).toHaveCount(data.figBlockCount); + + await expect(await figureBlock.image.nth(0)).toBeVisible(); + await expect(await figureBlock.figCaption.nth(0)).toContainText(data.figCaption_1); + + await expect(await figureBlock.image.nth(1)).toBeVisible(); + await expect(await figureBlock.figCaption.nth(1)).toContainText(data.figCaption_2); + }); + }); +}); diff --git a/nala/blocks/howto/howto.page.js b/nala/blocks/howto/howto.page.js new file mode 100644 index 0000000000..af06eafeee --- /dev/null +++ b/nala/blocks/howto/howto.page.js @@ -0,0 +1,52 @@ +export default class HowTo { + constructor(page, nth = 0) { + this.page = page; + // how-to locators + this.howTo = page.locator('.how-to').nth(nth); + this.foreground = this.howTo.locator('.foreground'); + this.howToLarge = this.page.locator('.how-to.large-image').nth(nth); + this.howToSeo = this.page.locator('.how-to.seo').nth(nth); + this.heading = this.howTo.locator('.how-to-heading'); + this.image = this.howTo.locator('.how-to-media'); + this.list = this.howTo.locator('li'); + this.largeImage = page.locator('.how-to-media img'); + + // howto contents css + this.cssProperties = { + '.how-to .foreground': { + padding: '80px 0px', + 'max-width': /%$/, + display: 'grid', + }, + 'how-to-media': { + 'align-self': 'center', + 'justify-self': 'center', + 'min-height': '100%', + }, + 'body-m': { + 'font-size': '18px', + 'line-height': '27px', + }, + 'how-to-large': { + padding: '80px 24px', + 'max-width': '700px', + }, + 'how-to-large-image': { + display: 'block', + 'grid-template-areas': 'none', + }, + 'how-to-seo': { + display: 'block', + 'grid-template-areas': 'none', + }, + }; + + // howto contents attributes + this.attProperties = { + 'how-to-large-image': { + width: '600', + height: '300', + }, + }; + } +} diff --git a/nala/blocks/howto/howto.spec.js b/nala/blocks/howto/howto.spec.js new file mode 100644 index 0000000000..e62227e90c --- /dev/null +++ b/nala/blocks/howto/howto.spec.js @@ -0,0 +1,23 @@ +module.exports = { + BlockName: 'HowTo Block', + features: [ + { + tcid: '1', + name: '@HowTo', + path: '/drafts/nala/blocks/howto/how-to', + tags: '@howto @smoke @regression @milo', + }, + { + tcid: '2', + name: '@HowTo large Image', + path: '/drafts/nala/blocks/howto/how-to-large', + tags: '@howto @howto-large-image @smoke @regression @milo', + }, + { + tcid: '3', + name: '@HowTo SEO', + path: '/drafts/nala/blocks/howto/how-to-seo', + tags: '@howto @howto-seo @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/howto/howto.test.js b/nala/blocks/howto/howto.test.js new file mode 100644 index 0000000000..ed25ac2227 --- /dev/null +++ b/nala/blocks/howto/howto.test.js @@ -0,0 +1,76 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './howto.spec.js'; +import HowToBlock from './howto.page.js'; + +let webUtil; +let howTo; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo HowTo block test suite', () => { + test.beforeEach(async ({ page }) => { + webUtil = new WebUtil(page); + howTo = new HowToBlock(page); + }); + + // Test 0 : HowTo default block + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to HowTo block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify HowTo specs', async () => { + await expect(howTo.howTo).toBeVisible(); + await expect(await howTo.list).toHaveCount(4); + + expect(await webUtil.verifyCSS(howTo.foreground, howTo.cssProperties['.how-to .foreground'])).toBeTruthy(); + expect(await webUtil.verifyCSS(howTo.heading, howTo.cssProperties['body-m'])).toBeTruthy(); + expect(await webUtil.verifyCSS(howTo.image, howTo.cssProperties['how-to-media'])).toBeTruthy(); + }); + }); + + // Test 1 : how-to (large) block + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('step-1: Go to HowTo large block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify HowTo large specs', async () => { + await expect(howTo.howToLarge).toBeVisible(); + await expect(await howTo.list).toHaveCount(4); + + expect(await webUtil.verifyCSS(howTo.heading, howTo.cssProperties['body-m'])).toBeTruthy(); + expect(await webUtil.verifyCSS(howTo.howToLarge, howTo.cssProperties['how-to-large-image'])).toBeTruthy(); + // eslint-disable-next-line max-len + expect(await webUtil.verifyAttributes(await howTo.largeImage, howTo.attProperties['how-to-large-image'])).toBeTruthy(); + }); + }); + + // Test 2 : how-to (seo) block + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + + await test.step('step-1: Go to HowTo SEO block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify HowTo SEO specs', async () => { + await expect(howTo.howToSeo).toBeVisible(); + await expect(await howTo.list).toHaveCount(4); + + expect(await webUtil.verifyCSS(howTo.heading, howTo.cssProperties['body-m'])).toBeTruthy(); + expect(await webUtil.verifyCSS(howTo.howToSeo, howTo.cssProperties['how-to-seo'])).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/icon/icon.page.js b/nala/blocks/icon/icon.page.js new file mode 100644 index 0000000000..bcd170dca9 --- /dev/null +++ b/nala/blocks/icon/icon.page.js @@ -0,0 +1,102 @@ +/* eslint-disable import/no-extraneous-dependencies, max-len, no-console, no-await-in-loop, import/extensions */ +import { expect } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; + +export default class Icon { + constructor(page) { + this.page = page; + this.webUtil = new WebUtil(page); + // Icon block locators + this.icon = this.page.locator('.icon-block').nth(0); + this.iconImage = this.icon.locator('img'); + this.iconHeadingL = this.icon.locator('.heading-l'); + this.iconHeadingXL = this.icon.locator('.heading-xl'); + this.iconBodyM = this.icon.locator('p.body-m').nth(0); + this.iconActionAreaBodyM = this.icon.locator('p.body-m.action-area').nth(0); + this.iconActionAreaLink = this.icon.locator('.action-area a').nth(0); + this.iconActionArea = this.icon.locator('.action-area'); + this.iconSupplemental = this.icon.locator('.supplemental-text'); + + // icon blocks css + this.cssProperties = { + '.icon-block': { + display: 'block', + width: /^\d+px$/, + position: 'relative', + }, + '.con-block.xl-spacing-static-bottom': { 'padding-bottom': '56px' }, + '.con-block.xl-spacing-static-top': { 'padding-top': '104px' }, + }; + + // icon blocks attributes + this.attProperties = { + icon: { class: 'icon-block' }, + 'icon-fullwidth-medium': { class: 'icon-block full-width medium con-block' }, + 'icon-fullwidth-medium-intro': { + class: + 'icon-block full-width medium intro con-block xxxl-spacing-top xl-spacing-static-bottom', + }, + 'icon-fullwidth-large': { class: 'icon-block full-width large con-block' }, + }; + } + + /** + * Verifies the visibility, css, attributes, styles, of elements or sections of + * the specified Icon block. + * + * @param {string} iconType - The type of the Icon block to verify. + * Possible values are 'icon-block (fullwidth, medium) ', 'icon-block (fullwidth, medium, intro)', and + * 'icon-block (fullwidth, large)'. + * @returns {Promise} - Returns true if the specified Quote type has the expected values. + */ + async verifyIcon(iconType, data) { + // verify icon block and image visibility + await expect(await this.icon).toBeVisible(); + await expect(await this.iconImage).toBeVisible(); + + switch (iconType) { + case 'icon block (fullwidth, medium)': + // verify icon block contents + await expect(await this.iconHeadingL).toContainText(data.headline); + await expect(await this.iconBodyM).toContainText(data.body); + await expect(await this.iconActionAreaBodyM).toContainText(data.buttonText); + await expect(await this.iconSupplemental).toContainText(data.supplementalText); + + // verify icon block attributes and css + expect(await this.webUtil.verifyAttributes(await this.icon, this.attProperties['icon-fullwidth-medium'])).toBeTruthy(); + + expect(await this.webUtil.verifyCSS(await this.icon, this.cssProperties['.icon-block'])).toBeTruthy(); + + return true; + case 'icon block (fullwidth, medium, intro)': + // verify icon block contents + await expect(await this.iconHeadingL).toContainText(data.headline); + await expect(await this.iconBodyM).toContainText(data.body); + await expect(await this.iconActionAreaBodyM).toContainText(data.buttonText); + await expect(await this.iconSupplemental).toContainText(data.supplementalText); + + // verify icon block attributes and css + expect(await this.webUtil.verifyAttributes(await this.icon, this.attProperties['icon-fullwidth-medium-intro'])).toBeTruthy(); + + expect(await this.webUtil.verifyCSS(await this.icon, this.cssProperties['.icon-block'])).toBeTruthy(); + expect(await this.webUtil.verifyCSS(await this.icon, this.cssProperties['.con-block.xl-spacing-static-bottom'])).toBeTruthy(); + expect(await this.webUtil.verifyCSS(await this.icon, this.cssProperties['.con-block.xl-spacing-static-top'])).toBeTruthy(); + + return true; + case 'icon block (fullwidth, large)': + // verify icon block contents + await expect(await this.iconHeadingXL).toContainText(data.headline); + await expect(await this.iconBodyM).toContainText(data.body); + await expect(await this.iconActionAreaLink).toContainText(data.linkText); + + // verify icon block attributes and css + expect(await this.webUtil.verifyAttributes(await this.icon, this.attProperties['icon-fullwidth-large'])).toBeTruthy(); + + expect(await this.webUtil.verifyCSS(await this.icon, this.cssProperties['.icon-block'])).toBeTruthy(); + + return true; + default: + throw new Error(`Unsupported Text type: ${this.iconType}`); + } + } +} diff --git a/nala/blocks/icon/icon.spec.js b/nala/blocks/icon/icon.spec.js new file mode 100644 index 0000000000..407aad1090 --- /dev/null +++ b/nala/blocks/icon/icon.spec.js @@ -0,0 +1,43 @@ +module.exports = { + FeatureName: 'Icon Block', + features: [ + { + tcid: '0', + name: '@Icon block (fullwidth, medium)', + path: '/drafts/nala/blocks/icon/icon-fullwidth-medium', + data: { + image: 'photoshop', + headline: 'Heading L Bold Icon Block', + body: 'Body M Lorem ipsum dolor sit', + buttonText: 'Learn more', + supplementalText: 'Body M BOLD Text link', + }, + tags: '@icon @icon-fullwidth-medium @smoke @regression @milo,', + }, + { + tcid: '1', + name: '@Icon block (fullwidth, medium, intro)', + path: '/drafts/nala/blocks/icon/fullwidth-medium-intro', + data: { + image: 'photoshop', + headline: 'Heading L Bold Icon Block', + body: 'Body M Lorem ipsum dolor sit', + buttonText: 'Learn more', + supplementalText: 'Body M BOLD Text link', + }, + tags: '@icon @icon-fullwidth-medium-intro @smoke @regression @milo,', + }, + { + tcid: '2', + name: '@Icon block (fullwidth,large)', + path: '/drafts/nala/blocks/icon/fullwidth-large', + data: { + image: 'photoshop', + headline: 'Heading XL Bold Icon Block', + body: 'Body M Lorem ipsum dolor sit', + linkText: 'Body M BOLD Text link', + }, + tags: '@icon @icon-fullwidth-large, @smoke @regression @milo,', + }, + ], +}; diff --git a/nala/blocks/icon/icon.test.js b/nala/blocks/icon/icon.test.js new file mode 100644 index 0000000000..c6f6e9fb91 --- /dev/null +++ b/nala/blocks/icon/icon.test.js @@ -0,0 +1,58 @@ +import { expect, test } from '@playwright/test'; +import { features } from './icon.spec.js'; +import IconBlock from './icon.page.js'; + +let icon; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Icon Block test suite', () => { + test.beforeEach(async ({ page }) => { + icon = new IconBlock(page); + }); + + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to Icon block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Icon block content/specs', async () => { + const { data } = features[0]; + expect(await icon.verifyIcon('icon block (fullwidth, medium)', data)).toBeTruthy(); + }); + }); + + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('step-1: Go to Icon block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Icon block content/specs', async () => { + const { data } = features[1]; + expect(await icon.verifyIcon('icon block (fullwidth, medium, intro)', data)).toBeTruthy(); + }); + }); + + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + + await test.step('step-1: Go to Icon block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Icon block content/specs', async () => { + const { data } = features[2]; + expect(await icon.verifyIcon('icon block (fullwidth, large)', data)).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/iframe/iframe.page.js b/nala/blocks/iframe/iframe.page.js new file mode 100644 index 0000000000..33dbf3762c --- /dev/null +++ b/nala/blocks/iframe/iframe.page.js @@ -0,0 +1,14 @@ +/* eslint-disable import/no-import-module-exports */ + +export default class Iframe { + constructor(page) { + this.page = page; + + // IFRAME ELEMENTS: + this.miloIframeContainer = page.locator('.milo-iframe'); + this.iframeContainer = this.miloIframeContainer.locator('iframe'); + + // IFRAME PROPS: + this.props = {}; + } +} diff --git a/nala/blocks/iframe/iframe.spec.js b/nala/blocks/iframe/iframe.spec.js new file mode 100644 index 0000000000..d5def97fc5 --- /dev/null +++ b/nala/blocks/iframe/iframe.spec.js @@ -0,0 +1,11 @@ +module.exports = { + name: 'Iframe Block', + features: [ + { + name: '@Iframe', + path: '/drafts/nala/blocks/iframe/iframe', + browserParams: '?georouting=off', + tags: '@iframe @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/iframe/iframe.test.js b/nala/blocks/iframe/iframe.test.js new file mode 100644 index 0000000000..7f42c37e8c --- /dev/null +++ b/nala/blocks/iframe/iframe.test.js @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { features } from './iframe.spec.js'; +import IframeBlock from './iframe.page.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Iframe Block test suite', () => { + // Iframe Block Checks: + test(`${features[0].name}, ${features[0].tags}`, async ({ page, baseURL }) => { + const Iframe = new IframeBlock(page); + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('Navigate to page with Iframe block', async () => { + await page.goto(`${baseURL}${features[0].path}${features[0].browserParams}&${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${features[0].browserParams}&${miloLibs}`); + }); + + await test.step('Validate Iframe block content', async () => { + await expect(Iframe.miloIframeContainer).toBeVisible(); + await expect(Iframe.iframeContainer).toBeVisible(); + }); + }); +}); diff --git a/nala/blocks/marketo/marketo.page.js b/nala/blocks/marketo/marketo.page.js new file mode 100644 index 0000000000..47bfe2533c --- /dev/null +++ b/nala/blocks/marketo/marketo.page.js @@ -0,0 +1,131 @@ +/* eslint-disable import/no-extraneous-dependencies, max-len, no-console, no-await-in-loop, import/extensions */ +import { expect } from '@playwright/test'; + +const FIRST_NAME = 'firstNameTest'; +const LAST_NAME = 'lastNameTest'; +const PHONE = '415-123-4567'; +const EMAIL = 'test+nosub@adobetest.com'; +const COMPANY = 'Adobe'; +const POSTAL_CODE = '94111'; + +export default class Marketo { + constructor(page) { + this.page = page; + this.marketo = this.page.locator('.marketo'); + this.firstName = this.marketo.locator('input[name="FirstName"]'); + this.lastName = this.marketo.locator('input[name="LastName"]'); + this.email = this.marketo.locator('input[name="Email"]'); + this.phone = this.marketo.locator('input[name="Phone"]'); + this.company = this.marketo.locator('input[name="mktoFormsCompany"]'); + this.functionalArea = this.marketo.locator( + 'select[name="mktoFormsFunctionalArea"]', + ); + this.country = this.marketo.locator('select[name="Country"]'); + this.state = this.marketo.locator('select[name="State"]'); + this.postalCode = this.marketo.locator('input[name="PostalCode"]'); + this.jobTitle = this.marketo.locator('select[name="mktoFormsJobTitle"]'); + this.primaryProductInterest = this.marketo.locator( + 'select[name="mktoFormsPrimaryProductInterest"]', + ); + this.companyType = this.marketo.locator( + 'select[name="mktoFormsCompanyType"]', + ); + this.submitButton = this.marketo.locator('#mktoButton_new'); + this.message = this.marketo.locator('.ty-message'); + } + + async submitFullTemplateForm(poi) { + await this.country.selectOption({ index: 1 }); + await this.jobTitle.selectOption({ index: 1 }); + await this.company.fill(COMPANY); + await this.firstName.fill(FIRST_NAME); + await this.lastName.fill(LAST_NAME); + await this.email.fill(EMAIL); + await this.phone.fill(PHONE); + await this.functionalArea.selectOption({ index: 1 }); + await this.postalCode.fill(POSTAL_CODE); + + // Setting index 2 to test so that the 'Company Type' field doesn't display + await this.primaryProductInterest.selectOption(poi !== undefined ? poi : { index: 2 }); + + await this.state.selectOption({ index: 1 }); + await this.selectCompanyType(); + await this.submitButton.click(); + } + + async submitExpandedTemplateForm() { + await this.country.selectOption({ index: 1 }); + await this.jobTitle.selectOption({ index: 1 }); + await this.firstName.fill(FIRST_NAME); + await this.lastName.fill(LAST_NAME); + await this.email.fill(EMAIL); + await this.functionalArea.selectOption({ index: 1 }); + await this.company.fill(COMPANY); + await this.selectCompanyType(); + await this.submitButton.click(); + } + + async submitEssentialTemplateForm() { + await this.country.selectOption({ index: 1 }); + await this.firstName.fill(FIRST_NAME); + await this.lastName.fill(LAST_NAME); + await this.email.fill(EMAIL); + await this.company.fill(COMPANY); + await this.selectCompanyType(); + await this.submitButton.click(); + } + + async getPOI() { + const poi = await this.page.evaluate( + 'window.mcz_marketoForm_pref.program.poi', + ); + return poi; + } + + async getFormTemplate() { + const template = await this.page.evaluate( + 'window.mcz_marketoForm_pref.form.template', + ); + return template; + } + + async selectCompanyType() { + // The company type field will display if the poi is one of the below + const expectedPOI = ['Commerce', 'ADOBEADVERTISINGCLOUD']; + + if (expectedPOI.includes(await this.getPOI())) { + this.companyType.selectOption({ index: 1 }); + } + } + + /** + * @description Checks that the input fields have the placeholder attribute + * and that the value isn't empty. + */ + async checkInputPlaceholders() { + const template = await this.page.evaluate( + 'window.mcz_marketoForm_pref.form.template', + ); + + if (!template) { + throw new Error('Template not found'); + } + + const inputFields = [ + this.firstName, + this.lastName, + this.email, + this.company, + ]; + + if (template === 'flex_contact') inputFields.push(this.phone, this.postalCode); + + inputFields.forEach(async (field) => { + await expect(async () => { + expect(await field).toHaveAttribute('placeholder', { timeout: 10000 }); + const placeholder = await field.getAttribute('placeholder'); + expect(placeholder.length).toBeGreaterThan(1); + }).toPass(); + }); + } +} diff --git a/nala/blocks/marketo/marketo.spec.js b/nala/blocks/marketo/marketo.spec.js new file mode 100644 index 0000000000..755f63ac5c --- /dev/null +++ b/nala/blocks/marketo/marketo.spec.js @@ -0,0 +1,73 @@ +module.exports = { + name: 'Marketo Forms block', + features: [ + { + tcid: '0', + name: '@marketo full template', + path: [ + '/drafts/nala/blocks/marketo/full', + ], + tags: '@marketo @marketoFullRedirect @marketoRedirect @milo @smoke @regression', + }, + { + tcid: '1', + name: '@marketo full template with company type', + path: [ + '/drafts/nala/blocks/marketo/full-with-company-type', + ], + tags: '@marketo @marketoFullRedirect @marketoRedirect @milo @smoke @regression', + }, + { + tcid: '2', + name: '@marketo expanded template', + path: [ + '/drafts/nala/blocks/marketo/expanded', + '/drafts/nala/blocks/marketo/expanded-with-company-type', + ], + tags: '@marketo @marketoExpandedRedirect @marketoRedirect @milo @smoke @regression', + }, + { + tcid: '3', + name: '@marketo essential template', + path: [ + '/drafts/nala/blocks/marketo/essential', + '/drafts/nala/blocks/marketo/essential-with-company-type', + ], + tags: '@marketo @marketoEssentialRedirect @marketoRedirect @milo @smoke @regression', + }, + { + tcid: '4', + name: '@marketo full template message', + path: [ + '/drafts/nala/blocks/marketo/full-message', + ], + tags: '@marketo @marketoFullMessage @marketoMessage @milo @smoke @regression', + }, + { + tcid: '5', + name: '@marketo full template with company type', + path: [ + '/drafts/nala/blocks/marketo/full-message-with-company-type', + ], + tags: '@marketo @marketoFullMessage @marketoMessage @milo @smoke @regression', + }, + { + tcid: '6', + name: '@marketo expanded template', + path: [ + '/drafts/nala/blocks/marketo/expanded-message', + '/drafts/nala/blocks/marketo/expanded-message-with-company-type', + ], + tags: '@marketo @marketoExpandedMessage @marketoMessage @milo @smoke @regression', + }, + { + tcid: '7', + name: '@marketo essential template', + path: [ + '/drafts/nala/blocks/marketo/essential-message', + '/drafts/nala/blocks/marketo/essential-message-with-company-type', + ], + tags: '@marketo @marketoEssentialMessage @marketoMessage @milo @smoke @regression', + }, + ], +}; diff --git a/nala/blocks/marketo/marketo.test.js b/nala/blocks/marketo/marketo.test.js new file mode 100644 index 0000000000..d303896a32 --- /dev/null +++ b/nala/blocks/marketo/marketo.test.js @@ -0,0 +1,278 @@ +import { expect, test } from '@playwright/test'; +import { features } from './marketo.spec.js'; +import MarketoBlock from './marketo.page.js'; + +let marketoBlock; +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Marketo block test suite', () => { + test.beforeAll(async ({ browserName }) => { + if (browserName === 'chromium' && process.env.CI) test.skip('TODO: debug why this is failing on github actions'); + + if (process.env.CI) test.setTimeout(1000 * 60 * 3); // 3 minutes + }); + + test.beforeEach(async ({ page }) => { + marketoBlock = new MarketoBlock(page); + }); + + features[0].path.forEach((path) => { + test(`0: @marketo full template (redirect), ${features[0].tags}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block full template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitFullTemplateForm(); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await marketoBlock.submitButton.waitFor({ state: 'detached' }); + const redirectedUrl = await page.url(); + await expect(redirectedUrl).toContain('?submissionid'); + }).toPass(); + }); + }); + }); + + features[1].path.forEach((path) => { + test(`1: @marketo full template (redirect) with company type, ${features[1].tags}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block full template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitFullTemplateForm('Digital commerce'); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await marketoBlock.submitButton.waitFor({ state: 'detached' }); + const redirectedUrl = await page.url(); + await expect(redirectedUrl).toContain('?submissionid'); + }).toPass(); + }); + }); + }); + + features[2].path.forEach((path) => { + test(`2: @marketo expanded template (redirect), ${features[2].tags}}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block expanded template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitExpandedTemplateForm(); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await marketoBlock.submitButton.waitFor({ state: 'detached' }); + const redirectedUrl = await page.url(); + await expect(redirectedUrl).toContain('?submissionid'); + }).toPass(); + }); + }); + }); + + features[3].path.forEach((path) => { + test(`3: @marketo essential template (redirect), ${features[3].tags}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block essential template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitEssentialTemplateForm(); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await marketoBlock.submitButton.waitFor({ state: 'detached' }); + const redirectedUrl = await page.url(); + await expect(redirectedUrl).toContain('?submissionid'); + }).toPass(); + }); + }); + }); + + features[4].path.forEach((path) => { + test(`4: @marketo full template (message), ${features[4].tags}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block full template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitFullTemplateForm(); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await expect(marketoBlock.message).toBeAttached(); + await expect(page.url()).toBe(testPage); + }).toPass(); + }); + }); + }); + + features[5].path.forEach((path) => { + test(`5: @marketo full template (message) with company type, ${features[5].tags}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block full template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitFullTemplateForm('Digital commerce'); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await expect(marketoBlock.message).toBeAttached(); + await expect(page.url()).toBe(testPage); + }).toPass(); + }); + }); + }); + + features[6].path.forEach((path) => { + test(`6: @marketo expanded (message) template, ${features[6].tags}}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block expanded template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitExpandedTemplateForm(); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await expect(marketoBlock.message).toBeAttached(); + await expect(page.url()).toBe(testPage); + }).toPass(); + }); + }); + }); + + features[7].path.forEach((path) => { + test(`7: @marketo essential (message) template, ${features[7].tags}, path: ${path}`, async ({ + page, + baseURL, + }) => { + const testPage = `${baseURL}${path}${miloLibs}`.toLowerCase(); + console.info(`[Test Page]: ${testPage}`); + + await test.step('step-1: Go to the Marketo block essential template test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toBe(testPage); + await expect(marketoBlock.email).toBeVisible({ timeout: 10000 }); + }); + + await test.step('step-2: check the input field placeholders', async () => { + await marketoBlock.checkInputPlaceholders(); + }); + + await test.step('step-3: Submit the form with valid inputs', async () => { + await marketoBlock.submitEssentialTemplateForm(); + }); + + await test.step('step-4: Verify the form submission redirect', async () => { + await expect(async () => { + await expect(marketoBlock.message).toBeAttached(); + await expect(page.url()).toBe(testPage); + }).toPass(); + }); + }); + }); +}); diff --git a/nala/blocks/marquee/marquee.page.js b/nala/blocks/marquee/marquee.page.js new file mode 100644 index 0000000000..84673f0110 --- /dev/null +++ b/nala/blocks/marquee/marquee.page.js @@ -0,0 +1,186 @@ +export default class Marquee { + constructor(page, nth = 0) { + this.page = page; + // marquee types locators + this.marquee = page.locator('.marquee').nth(nth); + this.marqueeLight = page.locator('.marquee.light'); + this.marqueeSmall = page.locator('.marquee.small'); + this.marqueeSmallLight = page.locator('.marquee.small.light'); + this.marqueeSmallDark = page.locator('.marquee.small.dark'); + this.marqueeLarge = page.locator('.marquee.large'); + this.marqueeLargeLight = page.locator('.marquee.large.light'); + this.marqueeLargeDark = page.locator('.marquee.large.dark'); + this.marqueeQuiet = page.locator('.marquee.quiet'); + this.marqueeInline = page.locator('.marquee'); + this.marqueeSplitSmall = page.locator('.marquee.split.small'); + this.marqueeSplitLarge = page.locator('.marquee.split.large'); + this.marqueeSplitLargeLight = page.locator('.marquee.split.one-third.large.light'); + this.marqueeSplitOneThirdLargeLight = page.locator('.marquee.split.one-third.large.light'); + this.marqueeSplitOneThird = page.locator('.marquee.split.one-third'); + this.marqueeSplitOneThirdSmallLight = page.locator('.marquee.split.one-third.small.light'); + + // marque section(s) locators + // marquee details + this.detailM = this.marquee.locator('.detail-m'); + this.detailL = this.marquee.locator('.detail-l'); + this.brandImage = this.marquee.locator('.detail-m'); + + // marquee headings + this.headingXL = this.marquee.locator('.heading-xl'); + this.headingXXL = this.marquee.locator('.heading-xxl'); + + // marquee body area + this.bodyM = this.marquee.locator('.body-m'); + this.bodyXL = this.marquee.locator('.body-xl'); + + // marquee actions area + this.actionArea = this.marquee.locator('.action-area'); + this.outlineButton = this.marquee.locator('.con-button.outline'); + this.outlineButtonS = this.marquee.locator('.con-button.outline.button-s'); + this.outlineButtonM = this.marquee.locator('.con-button.outline.button-m'); + this.outlineButtonL = this.marquee.locator('.con-button.outline.button-l'); + this.outlineButtonXL = this.marquee.locator('.con-button.outline.button-xl'); + + this.blueButton = this.marquee.locator('.con-button.blue'); + this.blueButtonL = this.marquee.locator('.con-button.blue.button-l'); + this.blueButtonXL = this.marquee.locator('.con-button.blue.button-xl'); + this.filledBlueButton = this.marquee.locator('.con-button.blue'); + this.filledButtonM = this.marquee.locator('.con-button.blue.button-s'); + this.filledButtonM = this.marquee.locator('.con-button.blue.button-m'); + this.filledButtonL = this.marquee.locator('.con-button.blue.button-l'); + this.filledButtonXL = this.marquee.locator('.con-button.blue.button-xl'); + + this.actionLink1 = this.marquee.locator('a').nth(0); + this.actionLink2 = this.marquee.locator('a').nth(1); + + // background images + this.background = this.marquee.locator('.background'); + this.backgroundImage = this.marquee.locator('div.background img'); + this.backgroundImageMobile = this.marquee.locator('div .background .mobile-only img'); + this.backgroundImageTablet = this.marquee.locator('div.background .tablet-only img'); + this.backgroundImageDesktop = this.marquee.locator('div.background .desktop-only img'); + + // background video + this.backgroundVideo = this.marquee.locator('div video'); + this.backgroundVideoDesktop = this.marquee.locator('div .desktop-only video'); + + // foreground images + this.foreground = this.marquee.locator('.foreground'); + this.foregroundImage = this.marquee.locator('div.foreground img'); + this.iconImage = this.foreground.locator('.icon-area img'); + + // media images + this.mediaImage = this.marquee.locator('div.asset img'); + + // marquee attributes + this.attributes = { + 'marquee.light': { + backgroundImg: { + loading: 'eager', + fetchpriority: 'high', + width: '1369', + height: '685', + }, + }, + 'marquee.small': { + backgroundImg: { + loading: 'eager', + fetchpriority: 'high', + width: '750', + height: '375', + }, + }, + 'marquee.small.light': { + backgroundImg: { + loading: 'eager', + fetchpriority: 'high', + width: '750', + height: '375', + }, + }, + 'marquee.large': { + backgroundImg: { + loading: 'eager', + fetchpriority: 'high', + width: '750', + height: '375', + }, + }, + 'marquee.large.light': { + backgroundImg: { + loading: 'eager', + fetchpriority: 'high', + width: '750', + height: '375', + style: 'object-position: left center;', + }, + }, + 'marquee.split.small': { style: /^background:\s+rgb\(0, 0, 0\)$/ }, + 'marquee.split.large': { + iconImg: { + loading: 'eager', + fetchpriority: 'high', + width: '49', + height: '48', + }, + mediaImg: { + loading: 'eager', + fetchpriority: 'high', + width: '720', + height: '520', + }, + }, + 'marquee.split.one-third-large': { + style: /^background:\s+rgb\(245, 245, 245\)$/, + iconImg: { + loading: 'eager', + fetchpriority: 'high', + width: '200', + height: '80', + }, + mediaImg: { + loading: 'eager', + fetchpriority: 'high', + width: '720', + height: '520', + }, + }, + 'marquee.split.one-third': { + style: /^background:\s+rgb\(0, 0, 0\)$/, + iconImg: { + loading: 'eager', + fetchpriority: 'high', + width: '65', + height: '64', + }, + mediaImg: { + loading: 'eager', + fetchpriority: 'high', + width: '720', + height: '520', + }, + }, + backgroundMobileImg: { + loading: 'eager', + fetchpriority: 'high', + }, + 'backgroundVideo.inline': { + playsinline: '', + autoplay: '', + loop: '', + muted: '', + }, + 'backgroundVideo.loopOnce': { + playsinline: '', + autoplay: '', + muted: '', + }, + 'backgroundVideo.controls': { + controls: '', + autoplay: '', + loop: '', + muted: '', + }, + }; + } +} diff --git a/nala/blocks/marquee/marquee.spec.js b/nala/blocks/marquee/marquee.spec.js new file mode 100644 index 0000000000..87b18b33ee --- /dev/null +++ b/nala/blocks/marquee/marquee.spec.js @@ -0,0 +1,187 @@ +module.exports = { + name: 'Marquee Block', + features: [ + { + tcid: '0', + name: '@Marquee (light)', + path: '/drafts/nala/blocks/marquee/marquee-light', + data: { + h2Text: 'Heading XL Marquee standard medium left light', + bodyText: 'Body M Lorem ipsum dolor sit amet,', + outlineButtonText: 'Lorem ipsum', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-light @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Marquee (small)', + path: '/drafts/nala/blocks/marquee/marquee-small', + data: { + h2Text: 'Marquee standard small dark', + bodyText: 'Lorem ipsum dolor sit amet', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-small @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Marquee (small,light)', + path: '/drafts/nala/blocks/marquee/marquee-small-light', + data: { + detailText: 'Detail', + h2Text: 'Heading XL Marquee standard small light', + bodyText: 'Lorem ipsum dolor sit amet', + outlineButtonText: 'Lorem ipsum', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-small-light @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Marquee (large)', + path: '/drafts/nala/blocks/marquee/marquee-large', + data: { + h2Text: 'Marquee Large Dark', + bodyText: 'Lorem ipsum dolor sit amet', + outlineButtonText: 'Lorem ipsum', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-large @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Marquee (large,light)', + path: '/drafts/nala/blocks/marquee/marquee-large-light', + data: { + h2Text: 'Marquee Large Light', + bodyText: 'Lorem ipsum dolor sit amet', + outlineButtonText: 'Secondary action', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-large-light @smoke @regression @milo', + }, + { + tcid: '5', + name: '@Marquee (quiet)', + path: '/drafts/nala/blocks/marquee/marquee-quiet', + data: { + detailText: 'Detail', + h2Text: 'Marquee quiet', + bodyText: 'Marquee’s variants are small,', + blueButtonText: 'Watch the video', + }, + tags: '@marquee @marquee-quiet @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Marquee (inline)', + path: '/drafts/nala/blocks/marquee/marquee-inline', + data: { + detailText: 'Detail', + h2Text: 'Marquee inline', + bodyText: 'Marquee’s variants are small,', + }, + tags: '@marquee @marquee-inline @smoke @regression @milo', + }, + { + tcid: '7', + name: '@Marquee (split,small)', + path: '/drafts/nala/blocks/marquee/marquee-split-small', + data: { + detailText: 'DETAIL M BOLD 12/15 OPTIONAL', + h2Text: 'Marquee Split ½ dark', + bodyText: 'Lorem ipsum dolor sit amet', + outlineButtonText: 'Secondary action', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-split-small @smoke @regression @milo', + }, + { + tcid: '8', + name: '@Marquee (split,large)', + path: '/drafts/nala/blocks/marquee/marquee-split-large', + data: { + detailText: 'DETAIL L BOLD 16/20', + h2Text: 'Heading XXL 44/55 Lorem', + bodyText: 'Body XL Regular (22/33) Lorem ipsum dolor sit amet', + blueButtonText: 'Call to action', + linkText: 'Body M 18/27', + }, + tags: '@marquee @marquee-split-large @smoke @regression @milo', + }, + { + tcid: '9', + name: '@Marquee (split,one-third,large,light)', + path: '/drafts/nala/blocks/marquee/marquee-split-one-third-large-light', + data: { + detailText: 'DETAIL L BOLD 16/20', + h2Text: 'Heading XXL 44/55 Lorem', + bodyText: 'Body XL Regular (22/33) Lorem ipsum dolor sit amet', + blueButtonText: 'Call to action', + linkText: 'Body M 18/27', + }, + tags: '@marquee @marquee-split-one-third-large-light @smoke @regression @milo', + }, + { + tcid: '10', + name: '@Marquee (split,one-third)', + path: '/drafts/nala/blocks/marquee/marquee-split-one-third', + data: { + detailText: 'DETAIL M BOLD 12/15 OPTIONAL', + h2Text: 'Heading XL 36/45 Lorem', + bodyText: 'Body M Regular (18/27) Lorem ipsum dolor sit amet', + blueButtonText: 'Call to action', + linkText: 'Body M 18/27', + }, + tags: '@marquee @marquee-split-one-third @smoke @regression @milo', + }, + { + tcid: '11', + name: '@Marquee (split,one-third,small,light)', + path: '/drafts/nala/blocks/marquee/marquee-split-one-third-small-light', + data: { + detailText: 'DETAIL M BOLD 12/15 OPTIONAL', + h2Text: 'Heading XL 36/45 Lorem', + bodyText: 'Body M Regular (18/27) Lorem ipsum dolor sit amet', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-split-one-third-small-light @smoke @regression @milo', + }, + { + tcid: '12', + name: '@Marquee small (background video playsinline)', + path: '/drafts/nala/blocks/marquee/marquee-small-background-video', + data: { + h2Text: 'Marquee standard small dark', + bodyText: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + blueButtonText: 'Call to action', + }, + tags: '@marquee @marquee-video @smoke @regression @milo', + }, + { + tcid: '13', + name: '@Marquee large (background video playsinline desktop)', + path: '/drafts/nala/blocks/marquee/marquee-large-desktop-video-autoplay', + data: { + h2Text: 'Desktop video only', + bodyText: 'From amazing AI-generated images in Photoshop', + blueButtonText: 'Free trial', + linkText: 'See all plans', + }, + tags: '@marquee @marquee-video @smoke @regression @milo', + }, + { + tcid: '14', + name: '@Marquee large (background video playsinline loop once)', + path: '/drafts/nala/blocks/marquee/video-autoplay-loop-once', + data: { + detailText: 'DETAIL L 16/20', + h2Text: 'Heading XL 36/45 Media (large, dark)', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet', + blueButtonText: 'Learn More', + }, + tags: '@marquee @marquee-video @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/marquee/marquee.test.js b/nala/blocks/marquee/marquee.test.js new file mode 100644 index 0000000000..f0b61239a9 --- /dev/null +++ b/nala/blocks/marquee/marquee.test.js @@ -0,0 +1,572 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './marquee.spec.js'; +import MarqueeBlock from './marquee.page.js'; + +let webUtil; +let marquee; +let consoleErrors = []; + +const miloLibs = process.env.MILO_LIBS || ''; +const knownConsoleErrors = [ + 'Access-Control-Allow-Origin', + 'Failed to load resource: net::ERR_FAILED', + 'adobeid-na1-stg1.services', + 'Attestation check for Topics', + 'Access to fetch at', + 'net::ERR_HTTP2_PROTOCOL_ERROR', +]; + +test.describe('Milo Marquee Block test suite', () => { + test.beforeEach(async ({ page }) => { + webUtil = new WebUtil(page); + marquee = new MarqueeBlock(page); + + page.on('console', (exception) => { + if (exception.type() === 'error') { + consoleErrors.push(exception.text()); + } + }); + }); + + test.afterEach(async () => { + consoleErrors = []; + }); + + // Test 0 : Marquee (light) + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Marquee (light) block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify marquee(light) specs', async () => { + await expect(await marquee.marqueeLight).toBeVisible(); + + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.outlineButton).toContainText(data.outlineButtonText); + await expect(await marquee.blueButton).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundImage, marquee.attributes['marquee.light'].backgroundImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await marquee.marqueeLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.outlineButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.outlineButtonText, 1, data.h2Text)); + await expect(await marquee.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify browser console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 1 : Marquee (small) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Marquee (small) block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (small) specs', async () => { + await expect(await marquee.marqueeSmall).toBeVisible(); + + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.blueButton).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundImage, marquee.attributes['marquee.small'].backgroundImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSmall).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + + await test.step('step-4: Verify browser console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 2 : Marquee (small,light) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Marquee (small, light ) block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (small, light) specs', async () => { + await expect(await marquee.marqueeSmallLight).toBeVisible(); + + await expect(await marquee.detailM).toContainText(data.detailText); + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.outlineButton).toContainText(data.outlineButtonText); + await expect(await marquee.blueButton).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundImage, marquee.attributes['marquee.small.light'].backgroundImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSmallLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.outlineButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.outlineButtonText, 1, data.h2Text)); + await expect(await marquee.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify browser console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 3 : Marquee (large) + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Marquee (large ) block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (large) specs', async () => { + await expect(await marquee.marqueeLarge).toBeVisible(); + + await expect(await marquee.headingXXL).toContainText(data.h2Text); + await expect(await marquee.bodyXL).toContainText(data.bodyText); + await expect(await marquee.outlineButtonXL).toContainText(data.outlineButtonText); + await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundImage, marquee.attributes['marquee.large'].backgroundImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeLarge).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.outlineButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.outlineButtonText, 1, data.h2Text)); + await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify browser console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 4 : Marquee (large,light) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Marquee (large, light ) block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (large, light) specs', async () => { + await expect(await marquee.marqueeLargeLight).toBeVisible(); + + await expect(await marquee.headingXXL).toContainText(data.h2Text); + await expect(await marquee.bodyXL).toContainText(data.bodyText); + await expect(await marquee.outlineButtonXL).toContainText(data.outlineButtonText); + await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundImage, marquee.attributes['marquee.large.light'].backgroundImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeLargeLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.outlineButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.outlineButtonText, 1, data.h2Text)); + await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 5 : Marquee (quiet) + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-1: Go to Marquee (quiet ) block test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (quiet) specs', async () => { + await expect(await marquee.marqueeQuiet).toBeVisible(); + + await expect(await marquee.detailM).toContainText(data.detailText); + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.blueButton).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundImage).toBeHidden(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeQuiet).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 6 : Marquee (inline) + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-1: Go to Marquee (inline ) block test page', async () => { + await page.goto(`${baseURL}${features[6].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (inline) specs', async () => { + await expect(await marquee.marqueeInline).toBeVisible(); + + await expect(await marquee.detailM).toContainText(data.detailText); + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + + await expect(await marquee.backgroundImage).toBeHidden(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeInline).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 7 : Marquee (split,small) + test(`${features[7].name},${features[7].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[7].path}${miloLibs}`); + const { data } = features[7]; + + await test.step('step-1: Go to Marquee (split, small ) block test page', async () => { + await page.goto(`${baseURL}${features[7].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[7].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (split, small) specs', async () => { + await expect(marquee.marqueeSplitSmall).toBeVisible(); + + await expect(await marquee.detailM).toContainText(data.detailText); + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.outlineButton).toContainText(data.outlineButtonText); + await expect(await marquee.blueButton).toContainText(data.blueButtonText); + + expect(await webUtil.verifyAttributes(marquee.marqueeSplitSmall, marquee.attributes['marquee.split.small'].style)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSplitSmall).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.outlineButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.outlineButtonText, 1, data.h2Text)); + await expect(await marquee.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 8 : Marquee (split,large) + test(`${features[8].name},${features[8].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[8].path}${miloLibs}`); + const { data } = features[8]; + + await test.step('step-1: Go to Marquee (split, large ) block test page', async () => { + await page.goto(`${baseURL}${features[8].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[8].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (split, large) specs ', async () => { + await expect(await marquee.marqueeSplitLarge).toBeVisible(); + + await expect(await marquee.detailL).toContainText(data.detailText); + await expect(await marquee.headingXXL).toContainText(data.h2Text); + await expect(await marquee.bodyXL).toContainText(data.bodyText); + await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText); + await expect(await marquee.actionLink2).toContainText(data.linkText); + + await expect(await marquee.iconImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.iconImage, marquee.attributes['marquee.split.large'].iconImg)).toBeTruthy(); + + await expect(await marquee.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.mediaImage, marquee.attributes['marquee.split.large'].mediaImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSplitLarge).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + await expect(await marquee.actionLink2).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 9 : Marquee (split,one-third,large,light) + test(`${features[9].name},${features[9].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[9].path}${miloLibs}`); + const { data } = features[9]; + + await test.step('step-1: Go to Marquee (split, one-third, large, light ) block test page', async () => { + await page.goto(`${baseURL}${features[9].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[9].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (split, one-third, large, light) specs', async () => { + await expect(marquee.marqueeSplitOneThirdLargeLight).toBeVisible(); + + await expect(await marquee.detailL).toContainText(data.detailText); + await expect(await marquee.headingXXL).toContainText(data.h2Text); + await expect(await marquee.bodyXL).toContainText(data.bodyText); + await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText); + await expect(await marquee.actionLink2).toContainText(data.linkText); + + await expect(await marquee.iconImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.iconImage, marquee.attributes['marquee.split.one-third-large'].iconImg)).toBeTruthy(); + + await expect(await marquee.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.mediaImage, marquee.attributes['marquee.split.one-third-large'].mediaImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSplitOneThirdLargeLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + await expect(await marquee.actionLink2).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 2, data.h2Text)); + }); + + await test.step('step-3: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 10 : Marquee (split,one-third) + test(`${features[10].name},${features[10].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[10].path}${miloLibs}`); + const { data } = features[10]; + + await test.step('step-1: Go to Marquee (split, one-third ) block test page', async () => { + await page.goto(`${baseURL}${features[10].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[10].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (split, one-third) specs', async () => { + await expect(await marquee.marqueeSplitOneThird).toBeVisible(); + + await expect(await marquee.detailM).toContainText(data.detailText); + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.blueButtonL).toContainText(data.blueButtonText); + await expect(await marquee.actionLink2).toContainText(data.linkText); + + await expect(await marquee.iconImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.iconImage, marquee.attributes['marquee.split.one-third'].iconImg)).toBeTruthy(); + + await expect(await marquee.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.mediaImage, marquee.attributes['marquee.split.one-third'].mediaImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSplitOneThird).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButtonL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + await expect(await marquee.actionLink2).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 11 : Marquee (split,one-third,small,light) + test(`${features[11].name},${features[11].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[11].path}${miloLibs}`); + const { data } = features[11]; + + await test.step('step-1: Go to Marquee (split,one-third,small,light ) block test page', async () => { + await page.goto(`${baseURL}${features[11].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[11].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (split,one-third,small,light) specs', async () => { + await expect(marquee.marqueeSplitOneThirdSmallLight).toBeVisible(); + + await expect(await marquee.detailM).toContainText(data.detailText); + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.blueButtonL).toContainText(data.blueButtonText); + + await expect(await marquee.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.mediaImage, marquee.attributes['marquee.split.one-third'].mediaImg)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSplitOneThirdSmallLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButtonL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 12 : Marquee small (background video playsinline) + test(`${features[12].name},${features[12].tags}`, async ({ page, baseURL, browserName }) => { + test.slow(); + test.skip(browserName === 'webkit', 'This feature is failing on Webkit browsers'); + console.info(`[Test Page]: ${baseURL}${features[12].path}${miloLibs}`); + const { data } = features[12]; + + await test.step('step-1: Go to Marquee (small) block test page', async () => { + await page.goto(`${baseURL}${features[12].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[12].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (small) background video playsinline specs', async () => { + await expect(await marquee.marqueeSmallDark).toBeVisible(); + + await expect(await marquee.headingXL).toContainText(data.h2Text); + await expect(await marquee.bodyM).toContainText(data.bodyText); + await expect(await marquee.blueButton).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundVideo).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundVideo, marquee.attributes['backgroundVideo.inline'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeSmallDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButtonL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 13 : Marquee large (background video playsinline desktop) + test(`${features[13].name},${features[13].tags}`, async ({ page, baseURL, browserName }) => { + test.skip(browserName === 'webkit', 'This feature is failing on Webkit browsers'); + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[13].path}${miloLibs}`); + const { data } = features[13]; + + await test.step('step-1: Go to Marquee (large, light ) block test page', async () => { + await page.goto(`${baseURL}${features[13].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[13].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (large, light) desktop background specs', async () => { + await expect(await marquee.marqueeLargeLight).toBeVisible(); + + await expect(await marquee.headingXXL).toContainText(data.h2Text); + await expect(await marquee.bodyXL).toContainText(data.bodyText); + await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText); + await expect(await marquee.actionLink2).toContainText(data.linkText); + + await expect(await marquee.backgroundVideoDesktop).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundVideoDesktop, marquee.attributes['backgroundVideo.inline'])).toBeTruthy(); + + const sourceElement = await marquee.backgroundVideoDesktop.locator('source'); + expect(await sourceElement.getAttribute('src')).toContain('.mp4'); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeLargeLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1)); + await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + await expect(await marquee.actionLink2).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 2, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); + + // Test 14 : Marquee large (background video playsinline loop once) + test(`${features[14].name},${features[14].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[14].path}${miloLibs}`); + const { data } = features[14]; + + await test.step('step-1: Go to Marquee (large, dark ) block test page', async () => { + await page.goto(`${baseURL}${features[14].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[14].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Marquee (large, dark) background specs', async () => { + await expect(await marquee.marqueeLargeDark).toBeVisible(); + + await expect(await marquee.headingXXL).toContainText(data.h2Text); + await expect(await marquee.bodyXL).toContainText(data.bodyText); + await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText); + + await expect(await marquee.backgroundVideo).toBeVisible(); + expect(await webUtil.verifyAttributes(marquee.backgroundVideo, marquee.attributes['backgroundVideo.loopOnce'])).toBeTruthy(); + + const sourceElement = await marquee.backgroundVideo.locator('source'); + expect(await sourceElement.getAttribute('src')).toContain('.mp4'); + expect(await sourceElement.getAttribute('type')).toContain('video/mp4'); + }); + + await test.step('step-3: Verify analytic attributes', async () => { + await expect(await marquee.marqueeLargeDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 2)); + await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + + await test.step('step-4: Verify and log if any console errors', async () => { + consoleErrors.length > knownConsoleErrors.length && console.log('[Console error]:', consoleErrors); + expect.soft(consoleErrors.length).toBeLessThanOrEqual(knownConsoleErrors.length); + }); + }); +}); diff --git a/nala/blocks/media/media.page.js b/nala/blocks/media/media.page.js new file mode 100644 index 0000000000..c1cfe4e606 --- /dev/null +++ b/nala/blocks/media/media.page.js @@ -0,0 +1,79 @@ +export default class Media { + constructor(page, nth = 0) { + this.page = page; + // media types + this.media = page.locator('.media').nth(nth); + this.mediaSmall = page.locator('.media.small'); + this.mediaLargeDark = page.locator('.media.large'); + + // media details + this.detailM = this.media.locator('.detail-m'); + this.detailL = this.media.locator('.detail-l'); + + // media headings + this.headingXS = this.media.locator('.heading-xs'); + this.headingM = this.media.locator('.heading-m'); + this.headingXL = this.media.locator('.heading-xl'); + + // media body area + this.bodyS = this.media.locator('.body-s').nth(0); + this.bodyM = this.media.locator('.body-m').nth(0); + this.bodyXL = this.media.locator('.body-xl').nth(0); + this.bodyTextM = this.media.locator('p:nth-of-type(2)'); + this.bodyTextS = this.media.locator('p:nth-of-type(2)'); + + // media actions area + this.actionArea = this.media.locator('.action-area'); + this.outlineButton = this.media.locator('.con-button.outline'); + this.blueButton = this.media.locator('.con-button.blue'); + + // media image + this.mediaImage = this.media.locator('.image'); + this.mediaImg = this.mediaImage.locator('img'); + + // background video + this.backgroundVideo = this.media.locator('div video'); + this.backgroundVideoDesktop = this.media.locator('div .desktop-only video'); + + // media attributes + this.attributes = { + 'media.small': { + image: { + loading: 'eager', + fetchpriority: 'high', + width: '400', + height: '300', + }, + }, + 'media.large': { + image: { + loading: 'lazy', + width: '700', + height: '525', + }, + }, + 'backgroundVideo.inline': { + playsinline: '', + autoplay: '', + loop: '', + muted: '', + }, + 'backgroundVideo.loopOnce': { + playsinline: '', + autoplay: '', + muted: '', + }, + 'backgroundVideo.controls': { + controls: '', + autoplay: '', + loop: '', + muted: '', + }, + analytics: { + 'media.daa-lh': { 'daa-lh': /b[1-9]|media|default|default/ }, + 'section.daa-lh': { 'daa-lh': /s[1-9]/ }, + 'content.daa-lh': { 'daa-lh': /b[1-9]|content|default|default/ }, + }, + }; + } +} diff --git a/nala/blocks/media/media.spec.js b/nala/blocks/media/media.spec.js new file mode 100644 index 0000000000..015b928cac --- /dev/null +++ b/nala/blocks/media/media.spec.js @@ -0,0 +1,66 @@ +module.exports = { + BlockName: 'Media Block', + features: [ + { + tcid: '0', + name: '@Media (small)', + path: '/drafts/nala/blocks/media/media-small', + data: { + detailText: 'Detail M 12/15', + h2Text: 'Heading XS 18/22 Media (small)', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'Learn More', + outlineButtonText: 'Watch the Video', + }, + tags: '@media @media-small @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Media', + path: '/drafts/nala/blocks/media/media', + data: { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 Media', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'Learn More', + }, + tags: '@media @smoke @regression @milo', + }, + { + tcid: '2', + name: '@media (large, dark)', + path: '/drafts/nala/blocks/media/media-large-dark', + data: { + detailText: 'Detail L 16/20', + h2Text: 'Heading XL 36/45 Media (large, dark)', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet,', + blueButtonText: 'Learn More', + }, + tags: '@media @media-large-dark @smoke @regression @milo', + }, + { + tcid: '3', + name: '@media (large, dark) video, autoplay infinite looping', + path: '/drafts/nala/blocks/media/media-video-autoplay-infinite-loop', + data: { + detailText: 'Detail L 16/20', + h2Text: 'Heading XL 36/45 Media (large, dark)', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet,', + blueButtonText: 'Learn More', + }, + tags: '@media @media-video @smoke @regression @milo', + }, + { + tcid: '4', + name: '@media video, autoplay loop once', + path: '/drafts/nala/blocks/media/media-video-autoplay-loop-once', + data: { + detailText: 'Detail L 16/20', + h2Text: 'Heading XL 36/45 Media (large, dark)', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet,', + blueButtonText: 'Learn More', + }, + tags: '@media @media-video @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/media/media.test.js b/nala/blocks/media/media.test.js new file mode 100644 index 0000000000..b76bc03e4e --- /dev/null +++ b/nala/blocks/media/media.test.js @@ -0,0 +1,165 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './media.spec.js'; +import MediaBlock from './media.page.js'; + +let webUtil; +let media; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Media Block test suite', () => { + test.beforeEach(async ({ page }) => { + webUtil = new WebUtil(page); + media = new MediaBlock(page); + }); + + // Test 0 : Media (small) + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Media (small) block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify media (small) block specs', async () => { + await expect(media.mediaSmall).toBeVisible(); + + await expect(await media.detailM).toContainText(data.detailText); + await expect(await media.headingXS).toContainText(data.h2Text); + await expect(await media.bodyS).toContainText(data.bodyText); + await expect(await media.outlineButton).toContainText(data.outlineButtonText); + await expect(await media.blueButton).toContainText(data.blueButtonText); + + await expect(await media.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(media.mediaImg, media.attributes['media.small'].image)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await media.mediaSmall).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('media', 1)); + await expect(await media.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + }); + + // Test 1 : Media + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to media block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify media block specs', async () => { + await expect(media.media).toBeVisible(); + + await expect(await media.detailM).toContainText(data.detailText); + await expect(await media.headingM).toContainText(data.h2Text); + await expect(await media.bodyS).toContainText(data.bodyText); + await expect(await media.blueButton).toContainText(data.blueButtonText); + + await expect(await media.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(media.mediaImg, media.attributes['media.small'].image)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await media.media).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('media', 1)); + await expect(await media.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + }); + + // Test 2 : Media (large, dark) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to media block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify media (large, dark) block specs', async () => { + await expect(media.mediaLargeDark).toBeVisible(); + + await expect(await media.detailL).toContainText(data.detailText); + await expect(await media.headingXL).toContainText(data.h2Text); + await expect(await media.bodyM).toContainText(data.bodyText); + await expect(await media.blueButton).toContainText(data.blueButtonText); + + await expect(await media.mediaImage).toBeVisible(); + expect(await webUtil.verifyAttributes(media.mediaImg, media.attributes['media.large'].image)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await media.mediaLargeDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('media', 1)); + await expect(await media.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + }); + + // Test 3 : Media (large, dark) video, autoplay infinite looping + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL, browserName }) => { + test.skip(browserName === 'webkit', 'This feature is failing on Webkit browsers'); + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to media block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify media (large, dark) block specs', async () => { + await expect(media.mediaLargeDark).toBeVisible(); + + await expect(await media.detailL).toContainText(data.detailText); + await expect(await media.headingXL).toContainText(data.h2Text); + await expect(await media.bodyM).toContainText(data.bodyText); + await expect(await media.blueButton).toContainText(data.blueButtonText); + + await expect(await media.backgroundVideo).toBeVisible(); + expect(await webUtil.verifyAttributes(media.backgroundVideo, media.attributes['backgroundVideo.inline'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await media.mediaLargeDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('media', 2)); + await expect(await media.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + }); + + // Test 5 : Media (large, dark) video, autoplay loop once + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to media block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify media (large, dark) block specs', async () => { + await expect(media.mediaLargeDark).toBeVisible(); + + await expect(await media.detailL).toContainText(data.detailText); + await expect(await media.headingXL).toContainText(data.h2Text); + await expect(await media.bodyM).toContainText(data.bodyText); + await expect(await media.blueButton).toContainText(data.blueButtonText); + + await expect(await media.backgroundVideo).toBeVisible(); + expect(await webUtil.verifyAttributes(media.backgroundVideo, media.attributes['backgroundVideo.loopOnce'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await media.mediaLargeDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('media', 2)); + await expect(await media.blueButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text)); + }); + }); +}); diff --git a/nala/blocks/merchcard/merchcard.pages.js b/nala/blocks/merchcard/merchcard.pages.js new file mode 100644 index 0000000000..4bbac09c5c --- /dev/null +++ b/nala/blocks/merchcard/merchcard.pages.js @@ -0,0 +1,99 @@ +export default class Merchcard { + constructor(page, nth = 0) { + this.page = page; + + // merch card locators + this.merchCard = this.page.locator('.merch-card').nth(nth); + this.segment = this.page.locator('.merch-card.segment').nth(nth); + this.sepcialOffers = this.page.locator('.merch-card.special-offers').nth(nth); + this.plans = this.page.locator('.merch-card.plans').nth(nth); + this.catalog = this.page.locator('.merch-card.catalog').nth(nth); + + // inline price and strikethrough price + this.inlinePrice1 = this.merchCard.locator('span.placeholder-resolved').nth(0); + this.inlinePrice2 = this.merchCard.locator('span.placeholder-resolved').nth(1); + this.price = this.inlinePrice1.locator('.price'); + this.priceCurrencySymbol = this.inlinePrice1.locator('.price-currency-symbol'); + this.priceInteger = this.inlinePrice1.locator('.price-integer'); + this.priceDecimalDelimiter = this.inlinePrice1.locator('.price-decimals-delimiter'); + this.priceDecimals = this.inlinePrice1.locator('.price-decimals'); + this.priceRecurrence = this.inlinePrice1.locator('.price-recurrence'); + + this.strikethroughPrice = this.inlinePrice2.locator('.price'); + this.strikethroughPriceCurrencySymbol = this.inlinePrice2.locator('.price-currency-symbol'); + this.strikethroughPriceInteger = this.inlinePrice2.locator('.price-integer'); + this.strikethroughPriceDecimalDelimiter = this.inlinePrice2.locator('.price-decimals-delimiter'); + this.strikethroughPriceDecimals = this.inlinePrice2.locator('.price-decimals'); + this.strikethroughPriceRecurrence = this.inlinePrice2.locator('.price-recurrence'); + + // merch-card segment locators + this.segmentRibbon = this.merchCard.locator('.segment-badge'); + this.segmentTitle = this.segment.locator('h3[slot="heading-xs"]').nth(0); + this.segmentDescription1 = this.segment.locator('div[slot="body-xs"] p').nth(0); + this.segmentDescription2 = this.segment.locator('div[slot="body-xs"] p').nth(1); + + this.linkText1 = this.segmentDescription2.locator('a').nth(0); + this.linkText2 = this.segmentDescription2.locator('a').nth(1); + + // merch-card special offers + this.sepcialOffersImage = this.sepcialOffers.locator('div[slot="bg-image"] img'); + this.sepcialOffersRibbon = this.merchCard.locator('.special-offers-badge'); + this.sepcialOffersTitleH4 = this.sepcialOffers.locator('h4[slot="detail-m"]').nth(0); + this.sepcialOffersTitleH5 = this.sepcialOffers.locator('h5[slot="body-xs"]'); + this.sepcialOffersTitleH3 = this.sepcialOffers.locator('h3[slot="heading-xs"]').nth(0); + + this.sepcialOffersDescription1 = this.sepcialOffers.locator('div[slot="body-xs"] p').nth(1); + this.sepcialOffersDescription2 = this.sepcialOffers.locator('div[slot="body-xs"] p').nth(2); + this.sepcialOffersDescription3 = this.sepcialOffers.locator('div[slot="body-xs"] p').nth(3); + this.sepcialOffersLinkText3 = this.sepcialOffersDescription3.locator('a').nth(0); + + this.seeTermsTextLink = this.merchCard.locator('a:has-text("See terms")'); + + // merch-card plans locators + this.productIcon = this.plans.locator('img'); + this.plansRibbon = this.plans.locator('.plans-badge'); + this.plansCardTitleH3 = this.plans.locator('h3[slot="heading-xs"]'); + this.plansCardTitleH4 = this.plans.locator('h4[slot="body-xxs"]'); + this.plansCardTitleH5 = this.plans.locator('h5[slot="body-xxs"]'); + this.plansCardDescription1 = this.plans.locator('div[slot="body-xs"] p').nth(1); + this.plansCardDescription2 = this.plans.locator('div[slot="body-xs"] p').nth(2); + this.plansCardDescription3 = this.plans.locator('div[slot="body-xs"] p').nth(3); + this.seePlansTextLink = this.merchCard.locator('a:has-text("See plan & pricing details")'); + + // merch-card catalog + this.catalogProductIcon = this.catalog.locator('#shadow-root div.icons'); + this.catalogRibbon = this.catalog.locator('.catalog-badge'); + this.catalogActionMenu = this.catalog.locator('div[slot="action-menu-content"]'); + this.catalogActionMenuList = this.catalogActionMenu.locator('ul li'); + this.catalogActionMenuPText1 = this.catalogActionMenu.locator('p').nth(0); + this.catalogActionMenuPText2 = this.catalogActionMenu.locator('p').nth(1); + this.catalogActionMenuPText3 = this.catalogActionMenu.locator('p').nth(2); + this.catalogActionMenuPText4 = this.catalogActionMenu.locator('p').nth(3); + this.catalogActionMenuPText5 = this.catalogActionMenu.locator('p ').nth(4); + this.systemRequirementTextLink = this.merchCard.locator('a:has-text("See system requirements")'); + + this.catalogCardTitleH3 = this.catalog.locator('h3[slot="heading-xs"]'); + this.catalogCardTitleH4 = this.catalog.locator('h4[slot="body-xxs"]'); + this.catalogCardDescription2 = this.catalog.locator('div[slot="body-xs"] p').nth(2); + this.seeWhatsIncludedTextLink = this.merchCard.locator('a:has-text("See what’s included")'); + this.learnMoreTextLink = this.merchCard.locator('a:has-text("Learn more")'); + + // merch-card footer sections + this.footer = this.merchCard.locator('div[slot="footer"]'); + this.footerCheckbox = this.page.locator('#stock-checkbox input[type="checkbox"]'); + this.footerCheckboxLabel = this.merchCard.locator('#stock-checkbox'); + this.secureTransactionIcon = this.merchCard.locator('.secure-transaction-icon'); + this.secureTransactionLabel = this.merchCard.locator('.secure-transaction-label'); + this.footerOutlineButton = this.merchCard.locator('a.con-button.outline'); + this.footerOutlineButton2 = this.merchCard.locator('a.con-button.outline').nth(1); + this.footerBlueButton = this.merchCard.locator('a.con-button.blue').nth(0); + this.footerBlueButton2 = this.merchCard.locator('a.con-button.blue').nth(1); + + // merch-card attributes + this.attributes = { + segmentRibbon: { style: /background-color:\s*#EDCC2D;\s*color:\s*#000000;\s*/ }, + specialOfferRibbon: { style: /background-color:\s* #F68D2E;\s*color:\s*#000000;\s*/ }, + plansRibbon: { style: /background-color:\s*#EDCC2D;\s*color:\s*#000000;\s*/ }, + }; + } +} diff --git a/nala/blocks/merchcard/merchcard.spec.js b/nala/blocks/merchcard/merchcard.spec.js new file mode 100644 index 0000000000..bfc98221f0 --- /dev/null +++ b/nala/blocks/merchcard/merchcard.spec.js @@ -0,0 +1,194 @@ +/* eslint-disable max-len */ + +module.exports = { + FeatureName: 'Merch Card Block', + features: [ + { + tcid: '0', + name: '@Merch-card (Segment)', + path: '/drafts/nala/blocks/merch-card/merch-card-segment', + data: { + title: 'Individuals', + price: 'US$59.99/mo', + strikethroughPrice: 'US$89.99/mo', + description: 'Save over 25% on 20+ apps, including Photoshop, Illustrator, and more. First year only. Ends 27', + link1Text: 'See what\'s included', + link2Text: 'Learn more', + footerOutlineButtonText: 'Learn More', + footerBlueButtonText: 'Save now', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Merch-card (Segment) with Badge', + path: '/drafts/nala/blocks/merch-card/merch-card-segment-with-badge', + data: { + title: 'Individuals', + badgeText: 'Best value', + price: 'US$59.99/mo', + strikethroughPrice: 'US$89.99/mo', + description: 'Save over 25% on 20+ apps, including Photoshop, Illustrator, and more. First year only. Ends 27', + link1Text: 'See what\'s included', + link2Text: 'Learn more', + footerOutlineButtonText: 'Learn More', + footerBlueButtonText: 'Save now', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Merch-card (special-offers) ', + path: '/drafts/nala/blocks/merch-card/merch-card-special-offers', + data: { + titleH4: 'INDIVIDUALS', + titleH3: 'Save over 30% on Creative Cloud All Apps.', + description1: 'Get 20+ creative apps and save big when you choose a yearly plan instead of a monthly plan.', + description2: 'Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar 20.', + footerBlueButtonText: 'Save now', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Merch-card (special-offers) with badge', + path: '/drafts/nala/blocks/merch-card/merch-card-special-offers-with-badge', + data: { + titleH4: 'INDIVIDUALS', + titleH3: 'Get 10% off Photoshop.', + badgeText: 'LIMITED TIME WEEKLY OFFER', + price: 'US$383.88/yr', + strikethroughPrice: 'US$32.99/mo', + description: 'Create gorgeous images, rich graphics, and incredible art. Save 10% for the first year. Ends Mar 20.', + link1Text: 'See terms', + footerOutlineButtonText: 'Learn More', + footerBlueButtonText: 'Save now', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Merch-card (plans)', + path: '/drafts/nala/blocks/merch-card/merch-cards-plans', + data: { + titleH3: 'Creative Cloud All Apps', + titleH5: 'Desktop', + price: 'US$79.99/mo', + description: 'Get 20+ Creative Cloud apps including Photoshop, Illustrator, Adobe Express, Premiere Pro, and Acrobat Pro. (Substance 3D apps are not included.)', + link1Text: 'See plan & pricing details', + footerBlueButtonText: 'Buy now', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '5', + name: '@Merch-card (plans) with badge', + path: '/drafts/nala/blocks/merch-card/merch-card-plans-with-badge', + data: { + titleH3: 'Creative Cloud All Apps', + titleH4: 'Desktop', + badgeText: 'Best value', + price: 'US$79.99/mo', + description: 'Get 20+ Creative Cloud apps including Photoshop, Illustrator, Adobe Express, Premiere Pro, and Acrobat Pro. (Substance 3D apps are not included.)', + link1Text: 'See plan & pricing details', + footerBlueButtonText: 'Buy now', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Merch-card (plans, secure)', + path: '/drafts/nala/blocks/merch-card/merch-card-plans-secure', + data: { + titleH3: 'Acrobat', + titleH5: 'Desktop + Mobile', + price: 'US$79.99/mo', + description: 'The complete PDF solution for working anywhere (includes desktop, web, and mobile access).', + link1Text: 'See plan & pricing details', + checkboxLabel: 'Add a 30-day free trial of Adobe Stock.*', + secureLabel: /Secure transaction/i, + footerBlueButton1Text: 'Buy now', + footerBlueButton2Text: 'Buy now', + footerOutlineButtonText: 'Free trial', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '7', + name: '@Merch-card (plans, secure) with badge', + path: '/drafts/nala/blocks/merch-card/merch-cards-plans-secure-with-badge', + data: { + titleH3: 'Creative Cloud All Apps', + titleH5: 'Desktop', + badgeText: 'Best value', + price: 'US$79.99/mo', + description: 'Get 20+ Creative Cloud apps including Photoshop, Illustrator, Adobe Express, Premiere Pro, and Acrobat Pro. (Substance 3D apps are not included.)', + link1Text: 'See plan & pricing details', + checkboxLabel: 'Add a 30-day free trial of Adobe Stock.*', + secureLabel: /Secure transaction/i, + footerBlueButton1Text: /Buy now/i, + footerOutlineButtonText: 'Free trial', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '8', + name: '@Merch-card (catalog)', + path: '/drafts/nala/blocks/merch-card/merch-cards-catalog', + data: { + titleH3: 'Creative Cloud All Apps', + titleH4: 'Desktop', + price: 'US$79.99/mo', + description: 'Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. (Substance 3D apps are not included.)', + link1Text: 'See what’s included', + link2Text: 'Learn more', + footerBlueButton1Text: 'Buy now', + footerOutlineButtonText: 'free trial', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '9', + name: '@Merch-card (catalog) with badge', + path: '/drafts/nala/blocks/merch-card/march-cards-catalog-with-badge', + data: { + titleH3: 'Creative Cloud All Apps', + titleH4: 'Desktop', + badgeText: 'Most popular', + badgeBgColor: '#EDCC2D', + badgeColor: '#000000', + price: 'US$79.99/mo', + description: 'Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. (Substance 3D apps are not included.)', + link1Text: 'See what’s included', + link2Text: 'Learn more', + footerBlueButton1Text: 'Buy now', + footerOutlineButtonText: 'free trial', + }, + tags: '@merch-card @smoke @regression @milo', + }, + { + tcid: '10', + name: '@Merch-card (catalog) with more info and badge', + path: '/drafts/nala/blocks/merch-card/merch-cards-catalog-with-more-info-and-badge', + data: { + titleH3: 'Creative Cloud All Apps', + titleH4: 'Desktop', + badgeText: 'Most popular', + badgeBgColor: '#EDCC2D', + badgeColor: '#000000', + actionMenuListCount: 4, + actionMenuText1: 'Best for', + actionMenuText2: 'Storage', + actionMenuText3: '100 GB of cloud storage', + actionMenuText4: '100 GB of cloud storage', + price: 'US$79.99/mo', + description: 'Get 20+ creative apps including Photoshop, Illustrator, Premiere Pro, Acrobat Pro, and Adobe Express. (Substance 3D apps are not included.)', + link1Text: 'See what’s included', + link2Text: 'Learn more', + footerBlueButton1Text: 'Buy now', + footerOutlineButtonText: 'free trial', + }, + tags: '@merch-card @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/merchcard/merchcard.test.js b/nala/blocks/merchcard/merchcard.test.js new file mode 100644 index 0000000000..2874e61728 --- /dev/null +++ b/nala/blocks/merchcard/merchcard.test.js @@ -0,0 +1,392 @@ +import { expect, test } from '@playwright/test'; +import { features } from './merchcard.spec.js'; +import MerchCard from './merchcard.pages.js'; + +let merchCard; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Merchcard block test suite', () => { + test.beforeEach(async ({ page, browserName }) => { + merchCard = new MerchCard(page); + if (browserName === 'chromium') { + await page.setExtraHTTPHeaders({ 'sec-ch-ua': '"Chromium";v="123", "Not:A-Brand";v="8"' }); + } + }); + + // Test 0 : Merch Card (Segment) + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}`); + }); + + await test.step('step-2: Verify Merch Card content/specs', async () => { + await expect(await merchCard.segment).toBeVisible(); + await expect(await merchCard.segmentTitle).toContainText(data.title); + // await expect(await merchCard.price).toContainText(data.price); + // await expect(await merchCard.strikethroughPrice).toContainText(data.strikethroughPrice); + + await expect(await merchCard.segmentDescription1).toContainText(data.description); + await expect(await merchCard.linkText1).toContainText(data.link1Text); + await expect(await merchCard.linkText2).toContainText(data.link2Text); + await expect(await merchCard.footer).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toContainText(data.footerOutlineButtonText); + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 1 : Merch Card (Segment) with Badge + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card with Badge content/specs', async () => { + await expect(await merchCard.segment).toBeVisible(); + await expect(await merchCard.segmentTitle).toContainText(data.title); + + await expect(await merchCard.segmentRibbon).toBeVisible(); + await expect(await merchCard.segmentRibbon).toContainText(data.badgeText); + + // await expect(await merchCard.price).toContainText(data.price); + // await expect(await merchCard.strikethroughPrice).toContainText(data.strikethroughPrice); + + await expect(await merchCard.segmentDescription1).toContainText(data.description); + await expect(await merchCard.linkText1).toContainText(data.link1Text); + await expect(await merchCard.linkText2).toContainText(data.link2Text); + + await expect(await merchCard.footer).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toContainText(data.footerOutlineButtonText); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + + await test.step('step-3: Verify Merch Card attributes', async () => { + await expect(await merchCard.segmentRibbon).toHaveAttribute('style', merchCard.attributes.segmentRibbon.style); + }); + }); + + // Test 2 : Merch Card (Special Offers) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card special offers content/specs', async () => { + await expect(await merchCard.sepcialOffers).toBeVisible(); + await expect(await merchCard.sepcialOffersImage).toBeVisible(); + + await expect(await merchCard.sepcialOffersTitleH4).toBeVisible(); + await expect(await merchCard.sepcialOffersTitleH4).toContainText(data.titleH4); + await expect(await merchCard.sepcialOffersTitleH3).toContainText(data.titleH3); + + await expect(await merchCard.sepcialOffersDescription1).toContainText(data.description1); + await expect(await merchCard.sepcialOffersDescription2).toContainText(data.description2); + + await expect(await merchCard.footer).toBeVisible(); + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 3 : Merch Card (Special Offers) with badge + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card special offers content/specs', async () => { + await expect(await merchCard.sepcialOffers).toBeVisible(); + await expect(await merchCard.sepcialOffersImage).toBeVisible(); + + await expect(await merchCard.sepcialOffersRibbon).toBeVisible(); + await expect(await merchCard.sepcialOffersRibbon).toContainText(data.badgeText); + + await expect(await merchCard.sepcialOffersTitleH3).toContainText(data.titleH3); + await expect(await merchCard.sepcialOffersTitleH4).toContainText(data.titleH4); + + await expect(await merchCard.sepcialOffersDescription1).toContainText(data.description); + await expect(await merchCard.seeTermsTextLink).toContainText(data.link1Text); + + await expect(await merchCard.footer).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toContainText(data.footerOutlineButtonText); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + + await test.step('step-3: Verify Merch Card attributes', async () => { + await expect(await merchCard.sepcialOffersRibbon).toHaveAttribute( + 'style', + merchCard.attributes.specialOfferRibbon.style, + ); + }); + }); + + // Test 4 : Merch Card (plans) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card special offers content/specs', async () => { + await expect(await merchCard.plans).toBeVisible(); + await expect(await merchCard.productIcon).toBeVisible(); + + await expect(await merchCard.plansCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.plansCardTitleH5).toContainText(data.titleH5); + + // await expect(await merchCard.price).toContainText(data.price); + await expect(await merchCard.plansCardDescription1).toContainText(data.description); + await expect(await merchCard.seePlansTextLink).toContainText(data.link1Text); + + await expect(await merchCard.footer).toBeVisible(); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 5 : Merch Card (plans) with badge + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card special offers content/specs', async () => { + await expect(await merchCard.plans).toBeVisible(); + await expect(await merchCard.productIcon).toBeVisible(); + + await expect(await merchCard.plansRibbon).toBeVisible(); + await expect(await merchCard.plansRibbon).toContainText(data.badgeText); + + await expect(await merchCard.plansCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.plansCardTitleH4).toContainText(data.titleH4); + + // await expect(await merchCard.price).toContainText(data.price); + await expect(await merchCard.plansCardDescription2).toContainText(data.description); + await expect(await merchCard.seePlansTextLink).toContainText(data.link1Text); + + await expect(await merchCard.footer).toBeVisible(); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButtonText); + }); + }); + + // Test 6 : Merch Card (plans) with secure + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[6].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card special offers content/specs', async () => { + await expect(await merchCard.plans).toBeVisible(); + await expect(await merchCard.productIcon).toBeVisible(); + + await expect(await merchCard.plansCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.plansCardTitleH5).toContainText(data.titleH5); + + // await expect(await merchCard.price).toContainText(data.price); + await expect(await merchCard.plansCardDescription1).toContainText(data.description); + await expect(await merchCard.seePlansTextLink).toContainText(data.link1Text); + + await expect(await merchCard.footer).toBeVisible(); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButton1Text); + + await expect(await merchCard.secureTransactionLabel).toContainText(data.secureLabel); + }); + }); + + // Test 7 : Merch Card (plans, secure) with badge + test(`${features[7].name},${features[7].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[7].path}${miloLibs}`); + const { data } = features[7]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[7].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[7].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card special offers content/specs', async () => { + await expect(await merchCard.plans).toBeVisible(); + await expect(await merchCard.productIcon).toBeVisible(); + + await expect(await merchCard.plansRibbon).toBeVisible(); + await expect(await merchCard.plansRibbon).toContainText(data.badgeText); + + await expect(await merchCard.plansCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.plansCardTitleH5).toContainText(data.titleH5); + + // await expect(await merchCard.price).toContainText(data.price); + await expect(await merchCard.plansCardDescription1).toContainText(data.description); + await expect(await merchCard.seePlansTextLink).toContainText(data.link1Text); + + await expect(await merchCard.footer).toBeVisible(); + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButton1Text); + + await expect(await merchCard.secureTransactionLabel).toContainText(data.secureLabel); + }); + }); + + // Test 8 : Merch Card (catalog) + test(`${features[8].name},${features[8].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[8].path}${miloLibs}`); + const { data } = features[8]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[8].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[8].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card catalog content/specs', async () => { + await expect(await merchCard.catalog).toBeVisible(); + await expect(await merchCard.catalogCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.catalogCardTitleH4).toContainText(data.titleH4); + + // await expect(await merchCard.price).toContainText(data.price); + + await expect(await merchCard.catalogCardDescription2).toContainText(data.description); + await expect(await merchCard.seeWhatsIncludedTextLink).toContainText(data.link1Text); + await expect(await merchCard.learnMoreTextLink).toContainText(data.link2Text); + + await expect(await merchCard.footer).toBeVisible(); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButton1Text); + + await expect(await merchCard.footerOutlineButton).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toContainText(data.footerOutlineButtonText); + }); + }); + + // Test 9 : Merch Card (catalog) with badge + test(`${features[9].name},${features[9].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[9].path}${miloLibs}`); + const { data } = features[9]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[9].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[9].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card catalog with badge content/specs', async () => { + await expect(await merchCard.catalog).toBeVisible(); + + await expect(await merchCard.catalog).toHaveAttribute('badge-background-color', data.badgeBgColor); + await expect(await merchCard.catalog).toHaveAttribute('badge-color', data.badgeColor); + await expect(await merchCard.catalog).toHaveAttribute('badge-text', data.badgeText); + + await expect(await merchCard.catalogCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.catalogCardTitleH4).toContainText(data.titleH4); + + // await expect(await merchCard.price).toContainText(data.price); + + await expect(await merchCard.catalogCardDescription2).toContainText(data.description); + await expect(await merchCard.seeWhatsIncludedTextLink).toContainText(data.link1Text); + await expect(await merchCard.learnMoreTextLink).toContainText(data.link2Text); + + await expect(await merchCard.footer).toBeVisible(); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButton1Text); + + await expect(await merchCard.footerOutlineButton).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toContainText(data.footerOutlineButtonText); + }); + }); + + // Test 10 : Merch Card (catalog) with more info and badge + test(`${features[10].name},${features[10].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[10].path}${miloLibs}`); + const { data } = features[10]; + + await test.step('step-1: Go to Merch Card feature test page', async () => { + await page.goto(`${baseURL}${features[10].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[10].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Merch Card catalog with badge content/specs', async () => { + await expect(await merchCard.catalog).toBeVisible(); + + await expect(await merchCard.catalog).toHaveAttribute('badge-background-color', data.badgeBgColor); + await expect(await merchCard.catalog).toHaveAttribute('badge-color', data.badgeColor); + await expect(await merchCard.catalog).toHaveAttribute('badge-text', data.badgeText); + + await expect(await merchCard.catalogCardTitleH3).toContainText(data.titleH3); + await expect(await merchCard.catalogCardTitleH4).toContainText(data.titleH4); + + // await expect(await merchCard.price).toContainText(data.price); + + await expect(await merchCard.catalogCardDescription2).toContainText(data.description); + await expect(await merchCard.seeWhatsIncludedTextLink).toContainText(data.link1Text); + await expect(await merchCard.learnMoreTextLink).toContainText(data.link2Text); + + await expect(await merchCard.footer).toBeVisible(); + + await expect(await merchCard.footerBlueButton).toBeVisible(); + await expect(await merchCard.footerBlueButton).toContainText(data.footerBlueButton1Text); + + await expect(await merchCard.footerOutlineButton).toBeVisible(); + await expect(await merchCard.footerOutlineButton).toContainText(data.footerOutlineButtonText); + }); + + await test.step('step-3: click more info link and verify action menu list', async () => { + await merchCard.catalog.hover(); + await merchCard.catalog.click(); + await page.waitForTimeout(1000); + + await expect(await merchCard.catalogActionMenuList).toHaveCount(data.actionMenuListCount); + await expect(await merchCard.catalogActionMenuPText1).toContainText(data.actionMenuText1); + await expect(await merchCard.catalogActionMenuPText2).toContainText(data.actionMenuText2); + await expect(await merchCard.catalogActionMenuPText3).toContainText(data.actionMenuText3); + }); + }); +}); diff --git a/nala/blocks/modal/modal.page.js b/nala/blocks/modal/modal.page.js new file mode 100644 index 0000000000..4fc19bff09 --- /dev/null +++ b/nala/blocks/modal/modal.page.js @@ -0,0 +1,62 @@ +export default class Modal { + constructor(page) { + this.page = page; + // modal locators + this.dialog = this.page.locator('.dialog-modal'); + this.modal = this.page.locator('.dialog-modal'); + this.fragment = this.modal.locator('.fragment'); + this.headingXL = this.page.locator('.heading-xl'); + this.bodyM = this.page.locator('.body-m').nth(2); + this.modalCloseButton = this.modal.locator('.dialog-close'); + this.dialogCloseButton = this.modal.locator('.dialog-close').nth(0); + this.marqueeLight = this.dialog.locator('.marquee.light'); + this.modelSelector = '.dialog-modal'; + + // text block + this.textBlock = this.modal.locator('.text').nth(0); + this.textBlockHeading = this.textBlock.locator('h2'); + this.textBlockBodyM = this.textBlock.locator('.body-m'); + + // media block + this.mediaBlock = this.modal.locator('.media').nth(0); + this.mediaBlockdetailM = this.mediaBlock.locator('.detail-m'); + this.mediaBlockTextHeading = this.mediaBlock.locator('h2'); + this.mediaBlockTextBodyS = this.mediaBlock.locator('.body-s').first(); + + // video block + this.video = this.modal.locator('video').nth(0); + + // modal contents attributes + this.attributes = { + 'modal-link': { class: 'modal link-block ' }, + 'video.inline': { + playsinline: '', + autoplay: '', + loop: '', + muted: '', + }, + }; + } + + /** + * Gets the modal link based on the modal id. + * Waits for the link to be visible before returning the locator. + * @param {Object} data - The data object containing modalId. + * @param {number} [timeout=3000] - Optional timeout for waiting. + * @returns {Promise} - The locator for the modal link. + * @throws Will throw an error if the link is not found within the timeout. + */ + async getModalLink(modalId, timeout = 1000) { + if (!modalId) { + throw new Error('Invalid data, "modalId" property is required.'); + } + const selector = `a[href="#${modalId}"]`; + const modalLink = this.page.locator(selector); + try { + await modalLink.waitFor({ state: 'visible', timeout }); + return modalLink; + } catch (error) { + throw new Error(`The modal link with selector "${selector}" could not be found within ${timeout}ms.`); + } + } +} diff --git a/nala/blocks/modal/modal.spec.js b/nala/blocks/modal/modal.spec.js new file mode 100644 index 0000000000..432e754edb --- /dev/null +++ b/nala/blocks/modal/modal.spec.js @@ -0,0 +1,46 @@ +module.exports = { + FeatureName: 'Modal Block', + features: [ + { + tcid: '0', + name: '@Modal Text', + path: '/drafts/nala/blocks/modal/modal-text-intro', + data: { + modalId: 'modal-text-intro', + fragment: 'text', + contentType: 'text (intro)', + h2Text: 'Text (intro)', + bodyText: 'Body M Regular Lorem ipsum dolor sit amet', + }, + tags: '@modal @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Modal Media', + path: '/drafts/nala/blocks/modal/modal-media', + data: { + modalId: 'modal-media', + detailText: 'Detail M 12/15', + fragment: 'media', + contentType: 'media', + h2Text: 'Heading M 24/30 Media', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed', + }, + tags: '@modal @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Modal Autoplay Video', + path: '/drafts/nala/blocks/modal/modal-autoplay-video', + data: { + modalId: 'modal-video-autoplay', + detailText: 'Detail M 12/15', + fragment: 'media', + contentType: 'media', + h2Text: 'Heading M 24/30 Media', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed', + }, + tags: '@modal @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/modal/modal.test.js b/nala/blocks/modal/modal.test.js new file mode 100644 index 0000000000..be47c5be00 --- /dev/null +++ b/nala/blocks/modal/modal.test.js @@ -0,0 +1,114 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './modal.spec.js'; +import ModalBlock from './modal.page.js'; + +let modal; +let webUtil; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Modal feature test suite', () => { + test.beforeEach(async ({ page }) => { + modal = new ModalBlock(page); + webUtil = new WebUtil(page); + }); + + // Test 0 : Modal with Text block + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to Modal feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Modal text fragment content/specs', async () => { + const { data } = features[0]; + const modalLink = await modal.getModalLink(data.modalId); + await expect(await modalLink).toBeVisible(); + await expect(await modalLink).toHaveAttribute('class', modal.attributes['modal-link'].class); + + // click the modal link + await modalLink.click(); + await expect(await modal.dialog).toBeVisible(); + + await expect(await modal.textBlock).toBeVisible(); + await expect(await modal.textBlockHeading).toContainText(data.h2Text); + await expect(await modal.textBlockBodyM).toContainText(data.bodyText); + + expect(await WebUtil.isModalInViewport(modal.page, modal.modalSelector)).toBeTruthy(); + + // click the modal close button + await expect(await modal.dialogCloseButton).toBeVisible(); + await modal.dialogCloseButton.click(); + }); + }); + + // Test 1 : Modal with Media block + test(`${features[1].name}, ${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('step-1: Go to Modal feature test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Modal media fragement content/specs', async () => { + const { data } = features[1]; + // expect(await modal.verifyModal(modalData)).toBeTruthy(); + + const modalLink = await modal.getModalLink(data.modalId); + await expect(await modalLink).toBeVisible(); + await expect(await modalLink).toHaveAttribute('class', modal.attributes['modal-link'].class); + + // click the modal link + await modalLink.click(); + await expect(await modal.dialog).toBeVisible(); + + await expect(await modal.mediaBlock).toBeVisible(); + await expect(await modal.mediaBlockdetailM).toContainText(data.detailText); + await expect(await modal.mediaBlockTextHeading).toContainText(data.h2Text); + await expect(await modal.mediaBlockTextBodyS).toContainText(data.bodyText); + + expect(await WebUtil.isModalInViewport(modal.page, modal.modalSelector)).toBeTruthy(); + + // close the modal using escape key press + await expect(await modal.dialogCloseButton).toBeVisible(); + await modal.page.keyboard.press('Escape'); + }); + }); + + // Test 2 : Modal with Video Autoplay + test(`${features[2].name}, ${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + + await test.step('step-1: Go to Modal feature test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Modal media fragement content/specs', async () => { + const { data } = features[2]; + + const modalLink = await modal.getModalLink(data.modalId); + await expect(await modalLink).toBeVisible(); + await expect(await modalLink).toHaveAttribute('class', modal.attributes['modal-link'].class); + + // click the modal link and verify video autoplay + await modalLink.click(); + await expect(await modal.dialog).toBeVisible(); + expect(await WebUtil.isModalInViewport(modal.page, modal.modalSelector)).toBeTruthy(); + + await expect(await modal.video).toBeVisible(); + expect(await webUtil.verifyAttributes(await modal.video, modal.attributes['video.inline'])).toBeTruthy(); + + // close the modal using escape key press + await expect(await modal.dialogCloseButton).toBeVisible(); + await modal.page.keyboard.press('Escape'); + }); + }); +}); diff --git a/nala/blocks/quote/quote.page.js b/nala/blocks/quote/quote.page.js new file mode 100644 index 0000000000..581e5ac442 --- /dev/null +++ b/nala/blocks/quote/quote.page.js @@ -0,0 +1,64 @@ +export default class Quote { + constructor(page, nth = 0) { + this.page = page; + // quote locators + this.quote = this.page.locator('.quote').nth(nth); + this.quoteImage = this.quote.locator('.quote-image'); + this.quoteCopy = this.quote.locator('p.quote-copy'); + this.quoteFigCaption = this.quote.locator('p.figcaption'); + this.quoteFigCaptionCite = this.quote.locator('cite p'); + this.sectionDark = this.page.locator('.section.dark'); + + // quote blocks css + this.cssProperties = { + quote: { + 'text-align': 'center', + margin: /^0px.*/, + }, + + 'quote-contained': { + 'text-align': 'center', + margin: /^0px.*/, + }, + + 'quote-align-right': { + 'text-align': 'right', + margin: /^0px.*/, + }, + + 'quote-copy': { + 'font-size': '24px', + 'font-weight': 'bold', + }, + + 'quote-inline-figure': { + display: 'flex', + 'align-content': 'center', + flex: '1 0 40%', + margin: '0px', + 'justify-content': 'center', + }, + + 'quote-inline-image': { + height: '200px', + 'max-height': '200px', + }, + + figcaption: { + 'font-size': '16px', + 'font-weight': 'bold', + }, + }; + + // quote blocks attributes + this.attProperties = { + quote: { class: 'quote con-block' }, + 'quote-contained': { class: 'quote contained con-block' }, + 'quote-inline': { class: 'quote inline contained con-block' }, + 'quote-borders': { class: 'quote borders contained con-block' }, + 'quote-align-right': { class: 'quote contained align-right con-block' }, + 'quote-xl-spacing': { class: 'quote contained xl-spacing con-block' }, + 'section-dark': { style: 'background: rgb(102, 102, 102);' }, + }; + } +} diff --git a/nala/blocks/quote/quote.spec.js b/nala/blocks/quote/quote.spec.js new file mode 100644 index 0000000000..ac1f36019f --- /dev/null +++ b/nala/blocks/quote/quote.spec.js @@ -0,0 +1,73 @@ +/* eslint-disable max-len */ + +module.exports = { + FeatureName: 'Quote Block', + features: [ + { + tcid: '0', + name: '@Quote ', + path: '/drafts/nala/blocks/quote/quote', + data: { + quoteCopy: '3D is a crucial part of how we explore the brand in a digital workflow', + figCaption: 'Benny Lee', + cite: 'Global Manager of Experiential Design, Coca-Cola Company', + }, + tags: '@Quote @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Quote (contained)', + path: '/drafts/nala/blocks/quote/quote-contained', + data: { + quoteCopy: '3D is a crucial part of how we explore the brand in a digital workflow', + figCaption: 'Benny Lee', + cite: 'Global Manager of Experiential Design, Coca-Cola Company', + }, + tags: '@Quote @quote-contained @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Quote (inline,contained)', + path: '/drafts/nala/blocks/quote/quote-inline-contained', + data: { + quoteCopy: 'We are the guardians of a 135-year-old brand.', + figCaption: 'Rapha Abreu', + cite: 'Global Vice President of Design, Coca-Cola Company', + }, + tags: '@Quote @quote-inline @smoke @regression @milo,', + }, + { + tcid: '3', + name: '@Quote (borders,contained)', + path: '/drafts/nala/blocks/quote/quote-borders-contained', + data: { + quoteCopy: 'This was our opportunity to be one of the first teams to delve into Adobe Experience Platform', + figCaption: 'Ron Nagy', + cite: 'Sr. Evangelist, Adobe@Adobe', + }, + tags: '@Quote @quote-borders @smoke @regression @milo,', + }, + { + tcid: '4', + name: '@Quote (contained, align-right)', + path: '/drafts/nala/blocks/quote/quote-contained-align-right', + data: { + quoteCopy: '“This was our opportunity to be one of the first teams to delve into Adobe Experience Platform, and we wanted to show people just how powerful it can be.”', + figCaption: 'Ron Nagy', + cite: 'Sr. Evangelist, Adobe@Adobe', + }, + tags: '@Quote @quote-align-right @smoke @regression @milo,', + }, + { + tcid: '5', + name: '@Quote (xl-spaced)', + path: '/drafts/nala/blocks/quote/quote-xl-spaced', + data: { + quoteCopy: 'This was our opportunity to be one of the first teams to delve into Adobe Experience Platform', + figCaption: 'Ron Nagy', + cite: 'Sr. Evangelist, Adobe@Adobe', + }, + tags: '@Quote @quote-xl-spaced @smoke @regression @milo,', + }, + ], +}; diff --git a/nala/blocks/quote/quote.test.js b/nala/blocks/quote/quote.test.js new file mode 100644 index 0000000000..70574691bd --- /dev/null +++ b/nala/blocks/quote/quote.test.js @@ -0,0 +1,151 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './quote.spec.js'; +import QuoteBlock from './quote.page.js'; + +let quote; +let webUtil; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Quote Block test suite', () => { + test.beforeEach(async ({ page }) => { + webUtil = new WebUtil(page); + quote = new QuoteBlock(page); + }); + + // Test 0 : Quote default block + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Quote block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Quote block content/specs', async () => { + await expect(await quote.quoteImage).toBeVisible(); + await expect(await quote.quoteCopy).toContainText(data.quoteCopy); + await expect(await quote.quoteFigCaption).toContainText(data.figCaption); + await expect(await quote.quoteFigCaptionCite).toContainText(data.cite); + + expect(await webUtil.verifyAttributes(await quote.quote, quote.attProperties.quote)).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quote, quote.cssProperties.quote)).toBeTruthy(); + }); + }); + + // Test 1 : quote (contained) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Quote block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Quote (contained) block content/specs', async () => { + await expect(await quote.quoteImage).toBeVisible(); + await expect(await quote.quoteCopy).toContainText(data.quoteCopy); + await expect(await quote.quoteFigCaption).toContainText(data.figCaption); + await expect(await quote.quoteFigCaptionCite).toContainText(data.cite); + + expect(await webUtil.verifyAttributes(await quote.quote, quote.attProperties['quote-contained'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quote, quote.cssProperties['quote-contained'])).toBeTruthy(); + }); + }); + + // Test 2 : Quote (inline,contained) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Quote (inline) block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Quote (inline) block content/specs', async () => { + await expect(await quote.quoteImage).toBeVisible(); + await expect(await quote.quoteCopy).toContainText(data.quoteCopy); + await expect(await quote.quoteFigCaption).toContainText(data.figCaption); + await expect(await quote.quoteFigCaptionCite).toContainText(data.cite); + + expect(await webUtil.verifyAttributes(await quote.quote, quote.attProperties['quote-inline'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quote, quote.cssProperties.quote)).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quoteImage, quote.cssProperties['quote-inline-figure'])).toBeTruthy(); + }); + }); + + // Test 3 : quote (borders) + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[MiloInfo] Checking page: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Quote (borders) block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Quote (borders) block content/specs', async () => { + await expect(await quote.quoteImage).not.toBeVisible(); + await expect(await quote.quoteCopy).toContainText(data.quoteCopy); + await expect(await quote.quoteFigCaption).toContainText(data.figCaption); + await expect(await quote.quoteFigCaptionCite).toContainText(data.cite); + + expect(await webUtil.verifyAttributes(await quote.quote, quote.attProperties['quote-borders'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quote, quote.cssProperties.quote)).toBeTruthy(); + }); + }); + + // Test 4 : quote (align-right) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Quote (align-right) block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Quote (align-right) block content/specs', async () => { + await expect(await quote.quoteImage).toBeVisible(); + await expect(await quote.quoteCopy).toContainText(data.quoteCopy); + await expect(await quote.quoteFigCaption).toContainText(data.figCaption); + await expect(await quote.quoteFigCaptionCite).toContainText(data.cite); + + expect(await webUtil.verifyAttributes(await quote.quote, quote.attProperties['quote-align-right'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quote, quote.cssProperties['quote-align-right'])).toBeTruthy(); + }); + }); + + // Test 5 : quote (xl-spaced) + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-1: Go to Quote (xl-spaced) block test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Quote (xl-spaced) block content/specs', async () => { + await expect(await quote.sectionDark).toBeVisible(); + await expect(await quote.quoteImage).not.toBeVisible(); + await expect(await quote.quoteCopy).toContainText(data.quoteCopy); + await expect(await quote.quoteFigCaption).toContainText(data.figCaption); + await expect(await quote.quoteFigCaptionCite).toContainText(data.cite); + + expect(await webUtil.verifyAttributes(await quote.sectionDark, quote.attProperties['section-dark'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(await quote.quote, quote.attProperties['quote-xl-spacing'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await quote.quote, quote.cssProperties.quote)).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/review/review.page.js b/nala/blocks/review/review.page.js new file mode 100644 index 0000000000..7fb93650c3 --- /dev/null +++ b/nala/blocks/review/review.page.js @@ -0,0 +1,89 @@ +/* eslint-disable import/no-extraneous-dependencies, max-len, no-console */ +import { expect } from '@playwright/test'; + +export default class Review { + constructor(page) { + this.page = page; + // review block locators + this.review = this.page.locator('.review'); + this.reviewTitle = this.review.locator('.hlx-reviewTitle'); + this.reviewFieldSet = this.review.locator('form input'); + this.reviewTextArea = this.review.locator('#rating-comments'); + this.sendButton = this.review.locator('input[type="submit"]'); + } + + /** + * Verifies milo review block . + * @param {string} data - data required to verify review block. + * @returns {Promise} - A Promise that resolves to true if the verification + * is successful, or false if an error occurs. + */ + async verifyReview(data) { + try { + // verify review blcok + await expect(await this.review).toBeVisible(); + await expect(await this.reviewTitle).toContainText(data.reviewTitle); + await expect(await this.reviewFieldSet).toHaveCount(data.reviewFields); + + // Expected values review checkboxes + const expectedValues = [ + { tooltip: 'Poor', ariaLabel: 'Poor 1 Star', value: '1' }, + { tooltip: 'Below Average', ariaLabel: 'Below Average 2 Star', value: '2' }, + { tooltip: 'Good', ariaLabel: 'Good 3 Star', value: '3' }, + { tooltip: 'Very Good', ariaLabel: 'Very Good 4 Star', value: '4' }, + { tooltip: 'Outstanding', ariaLabel: 'Outstanding 5 Star', value: '5' }, + ]; + const reviewCheckBoxes = await this.reviewFieldSet.all(); + const checkBoxes = await Promise.all(reviewCheckBoxes.map(async (el) => el)); + // eslint-disable-next-line no-restricted-syntax + for (const checkbox of checkBoxes) { + const tooltip = await checkbox.getAttribute('data-tooltip'); + const ariaLabel = await checkbox.getAttribute('aria-label'); + const value = await checkbox.getAttribute('value'); + // Find the matching expected value + const expectedValue = expectedValues.find((expected) => expected.tooltip === tooltip + && expected.ariaLabel === ariaLabel + && expected.value === value); + // Verify the expected value + if (!expectedValue) { + console.log('Attributes and values are incorrect'); + return false; + } + } + return true; + } catch (error) { + console.error(`Error review block: ${error}`); + return false; + } + } + + /** + * Submits the review rating / form. + * @param {string} checkboxValue - The value of the checkbox to be selected. + * @param {string} textareaValue - The value to be entered in the text area. + * @returns {Promise} - A Promise that resolves to true if the submission is successful, + * or false if an error occurs. + */ + async submitReview(data) { + try { + // Select the n-th rating checkbox + const checkbox = await this.reviewFieldSet.nth(data.rating); + + // if the rating less than 3 then text area field is visible + if (data.rating < 3) { + await checkbox.check(); + await expect(await this.reviewTextArea).toBeVisible(); + await this.reviewTextArea.fill(data.reviewComment); + + // Click the send button + await this.sendButton.click(); + return true; + } + await checkbox.check(); + return true; + } catch (error) { + console.error(`Error submitting the review: ${error}`); + return false; + } + } +} diff --git a/nala/blocks/review/review.spec.js b/nala/blocks/review/review.spec.js new file mode 100644 index 0000000000..575f2e0ae1 --- /dev/null +++ b/nala/blocks/review/review.spec.js @@ -0,0 +1,31 @@ +module.exports = { + FeatureName: 'Review', + features: [ + { + tcid: '0', + name: '@Review low ', + path: '/drafts/nala/blocks/review/review', + data: { + reviewTitle: 'Rate your Experience', + reviewFields: 5, + rating: 4, + reviewComment: 'This is great', + + }, + tags: '@Review @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Review low', + path: '/drafts/nala/blocks/review/review', + data: { + reviewTitle: 'Rate your Experience', + reviewFields: 5, + rating: 2, + reviewComment: 'This is great', + + }, + tags: '@Review @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/review/review.test.js b/nala/blocks/review/review.test.js new file mode 100644 index 0000000000..d3eb7462ab --- /dev/null +++ b/nala/blocks/review/review.test.js @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { features } from './review.spec.js'; +import ReviewBlock from './review.page.js'; + +let review; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Review Block test suite', () => { + test.beforeEach(async ({ page, browser }) => { + // review block requires clearing cookies + const context = await browser.newContext(); + await context.clearCookies(); + review = new ReviewBlock(page); + }); + + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('step-1: Go to review feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify review block and submit the review < 3', async () => { + const { data } = features[0]; + expect(await review.verifyReview(data)).toBeTruthy(); + expect(await review.submitReview(data)).toBeTruthy(); + }); + }); + + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('step-1: Go to review block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify review block and submit the review > 3', async () => { + const { data } = features[1]; + expect(await review.verifyReview(data)).toBeTruthy(); + expect(await review.submitReview(data)).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/table/table.page.js b/nala/blocks/table/table.page.js new file mode 100644 index 0000000000..03c25a2467 --- /dev/null +++ b/nala/blocks/table/table.page.js @@ -0,0 +1,75 @@ +/* eslint-disable no-return-await */ +export default class Table { + constructor(page, nth = 0) { + this.page = page; + // tabel locators + this.table = this.page.locator('.table').nth(nth); + this.highlightTable = this.page.locator('.table.highlight').nth(nth); + this.stickyTable = this.page.locator('.table.sticky').nth(nth); + this.collapseStickyTable = this.page.locator('.table.highlight.collapse.sticky').nth(nth); + this.merchTable = this.page.locator('.table.merch').nth(nth); + this.merchHighlightStickyTable = this.page.locator('.table.merch.highlight.sticky').nth(nth); + + this.highlightRow = this.table.locator('.row-highlight'); + this.headingRow = this.table.locator('.row-heading'); + this.stickyRow = this.table.locator('.row-heading'); + + this.headingRowColumns = this.headingRow.locator('.col'); + this.rows = this.table.locator('.row'); + this.sectionRows = this.table.locator('.section-row'); + } + + async getHighlightRowColumnTitle(colIndex) { + return await this.highlightRow.locator('.col-highlight').nth(colIndex); + } + + async getHeaderColumnTitle(colIndex) { + const headerColumn = await this.headingRow.locator(`.col-${colIndex}`); + return headerColumn.locator('.tracking-header'); + } + + async getHeaderColumnPricing(colIndex) { + const headerColumn = await this.headingRow.locator(`.col-${colIndex}`); + return headerColumn.locator('.pricing'); + } + + async getHeaderColumnImg(colIndex) { + const headerColumn = await this.headingRow.locator(`.col-${colIndex}`); + return headerColumn.locator('img'); + } + + async getHeaderColumnAdditionalText(colIndex) { + const headerColumn = await this.headingRow.locator(`.col-${colIndex}`); + return headerColumn.locator('p').nth(3); + } + + async getHeaderColumnOutlineButton(colIndex) { + const headerColumn = await this.headingRow.locator(`.col-${colIndex}`); + return headerColumn.locator('.con-button.outline'); + } + + async getHeaderColumnBlueButton(colIndex) { + const headerColumn = await this.headingRow.locator(`.col-${colIndex}`); + return headerColumn.locator('.con-button.blue'); + } + + async getSectionRowTitle(index) { + const sectionRow = await this.table.locator('.section-row').nth(index); + return sectionRow.locator('.section-row-title'); + } + + async getSectionRowMerchContent(index) { + const sectionRow = await this.table.locator('.section-row').nth(index); + return sectionRow.locator('.col-merch-content').nth(0); + } + + async getSectionRowMerchContentImg(index) { + const sectionRow = await this.table.locator('.section-row').nth(index); + return sectionRow.locator('.col-merch-content img'); + } + + async getSectionRowCell(rowIndex, colIndex) { + const sectionRow = await this.table.locator('.section-row').nth(rowIndex); + return sectionRow.locator(`.col-${colIndex}`); + } +} diff --git a/nala/blocks/table/table.spec.js b/nala/blocks/table/table.spec.js new file mode 100644 index 0000000000..8ac863fbcf --- /dev/null +++ b/nala/blocks/table/table.spec.js @@ -0,0 +1,121 @@ +module.exports = { + FeatureName: 'Table Block', + features: [ + { + tcid: '0', + name: '@Table (default)', + path: '/drafts/nala/blocks/table/table', + data: { + rowsCount: 9, + headerRowColCount: 5, + sectionRowCount: 8, + headerCell2: { + heading: 'Heading Title-2', + pricingText: 'Pricing-2', + outlineButtonText: 'Free trial', + blueButtonText: 'Buy now', + }, + sectionRow2: { + sectionRowTitle: 'Row-1.1, Title', + cell22: 'Content', + }, + }, + tags: '@table @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Table (highlight)', + path: '/drafts/nala/blocks/table/table-hightlight', + data: { + rowsCount: 10, + headerRowColCount: 5, + sectionRowCount: 8, + hightlightRow: { + cell12: 'Highlight-2', + cell13: 'Highlight-3', + cell14: 'Highlight-4', + }, + headerCell3: { + heading: 'Heading Title-3', + pricingText: 'Pricing-3', + outlineButtonText: 'Free trial', + blueButtonText: 'Buy now', + }, + sectionRow2: { + sectionRowTitle: 'Row-1.1, Title', + cell22: 'Content', + }, + }, + tags: '@table @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Table (sticky)', + path: '/drafts/nala/blocks/table/table-sticky', + data: { + rowsCount: 9, + headerRowColCount: 5, + sectionRowCount: 8, + headerCell4: { + heading: 'Heading Title-4', + pricingText: 'Pricing-4', + outlineButtonText: 'Free trial', + blueButtonText: 'Buy now', + }, + sectionRow2: { + sectionRowTitle: 'Row-1.1, Title', + cell22: 'Content', + }, + }, + tags: '@table @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Table (highlight, collapse, sticky)', + path: '/drafts/nala/blocks/table/table-highlight-collapse-sticky', + data: { + rowsCount: 10, + headerRowColCount: 5, + sectionRowCount: 8, + hightlightRow: { + cell12: 'Highlight-2', + cell13: 'Highlight-3', + cell14: 'Highlight-4', + }, + headerCell5: { + heading: 'Heading Title-5', + pricingText: 'Pricing-5', + outlineButtonText: 'Free trial', + blueButtonText: 'Buy now', + }, + sectionRow2: { + sectionRowTitle: 'Row-1.1, Title', + cell22: 'Content', + }, + }, + tags: '@table @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Table (merch)', + path: '/drafts/nala/blocks/table/table-merch', + data: { + rowsCount: 9, + headerRowColCount: 3, + sectionRowCount: 8, + headerCell1: { + heading: 'Heading Title-1', + pricingText: 'Pricing-1', + AdditionalText: 'Additional Text-1', + outlineButtonText: 'Free trial', + blueButtonText: 'Buy now', + }, + sectionRow2: { + merchContent: 'Section Content-1.1', + image: 'yes', + }, + }, + tags: '@table @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/table/table.test.js b/nala/blocks/table/table.test.js new file mode 100644 index 0000000000..d0b296ce55 --- /dev/null +++ b/nala/blocks/table/table.test.js @@ -0,0 +1,195 @@ +import { expect, test } from '@playwright/test'; +import { features } from './table.spec.js'; +import TableBlock from './table.page.js'; + +let table; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Table block feature test suite', () => { + test.beforeEach(async ({ page }) => { + table = new TableBlock(page); + }); + + // Test 0 : Table block (default) + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Table block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify table content/specs', async () => { + await expect(await table.table).toBeVisible(); + await expect(await table.rows).toHaveCount(data.rowsCount); + await expect(await table.headingRowColumns).toHaveCount(data.headerRowColCount); + await expect(await table.sectionRows).toHaveCount(data.sectionRowCount); + + // verify header row cell + const headerCell = data.headerCell2; + await expect(await table.getHeaderColumnTitle(2)).toContainText(headerCell.heading); + await expect(await table.getHeaderColumnPricing(2)).toContainText(headerCell.pricingText); + await expect(await table.getHeaderColumnOutlineButton(2)).toContainText(headerCell.outlineButtonText); + await expect(await table.getHeaderColumnBlueButton(2)).toContainText(headerCell.blueButtonText); + + // verify section row cell + const sectionCell = data.sectionRow2; + await expect(await table.getSectionRowTitle(2)).toContainText(sectionCell.sectionRowTitle); + await expect(await table.getSectionRowCell(2, 2)).toContainText(sectionCell.cell22); + }); + }); + + // Test 1 : Table (highlight) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Table block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify table content/specs', async () => { + await expect(await table.highlightTable).toBeVisible(); + await expect(await table.highlightRow).toBeVisible(); + + await expect(await table.rows).toHaveCount(data.rowsCount); + await expect(await table.headingRowColumns).toHaveCount(data.headerRowColCount); + await expect(await table.sectionRows).toHaveCount(data.sectionRowCount); + + // verify highlighter row + const highlighter = data.hightlightRow; + await expect(await table.getHighlightRowColumnTitle(1)).toContainText(highlighter.cell12); + await expect(await table.getHighlightRowColumnTitle(2)).toContainText(highlighter.cell13); + await expect(await table.getHighlightRowColumnTitle(3)).toContainText(highlighter.cell14); + + // verify header row cell + const headerCell = data.headerCell3; + await expect(await table.getHeaderColumnTitle(3)).toContainText(headerCell.heading); + await expect(await table.getHeaderColumnPricing(3)).toContainText(headerCell.pricingText); + await expect(await table.getHeaderColumnOutlineButton(3)).toContainText(headerCell.outlineButtonText); + await expect(await table.getHeaderColumnBlueButton(3)).toContainText(headerCell.blueButtonText); + + // verify section row cell + const sectionCell = data.sectionRow2; + await expect(await table.getSectionRowTitle(2)).toContainText(sectionCell.sectionRowTitle); + await expect(await table.getSectionRowCell(2, 2)).toContainText(sectionCell.cell22); + }); + }); + + // Test 2 : Table (sticky) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Table block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify table content/specs', async () => { + // verify sticky table header and attributes + await expect(await table.stickyTable).toBeVisible(); + await expect(await table.stickyRow).toHaveAttribute('class', 'row row-1 row-heading top-border-transparent'); + + // verify table row, column count + await expect(await table.rows).toHaveCount(data.rowsCount); + await expect(await table.headingRowColumns).toHaveCount(data.headerRowColCount); + await expect(await table.sectionRows).toHaveCount(data.sectionRowCount); + + // verify header row cell + const headerCell = data.headerCell4; + await expect(await table.getHeaderColumnTitle(4)).toContainText(headerCell.heading); + await expect(await table.getHeaderColumnPricing(4)).toContainText(headerCell.pricingText); + await expect(await table.getHeaderColumnOutlineButton(4)).toContainText(headerCell.outlineButtonText); + await expect(await table.getHeaderColumnBlueButton(4)).toContainText(headerCell.blueButtonText); + + // verify section row cell + const sectionCell = data.sectionRow2; + await expect(await table.getSectionRowTitle(2)).toContainText(sectionCell.sectionRowTitle); + await expect(await table.getSectionRowCell(2, 2)).toContainText(sectionCell.cell22); + }); + }); + + // Test 3 : Table (highlight, collapse, sticky) + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to Table block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify table content/specs', async () => { + // verify sticky table header and attributes + await expect(await table.collapseStickyTable).toBeVisible(); + await expect(table.highlightRow).toHaveClass(/row.*row-1.*row-highlight/); + await expect(table.stickyRow).toHaveClass(/row.*row-2.*row-heading/); + + // verify table row, column count + await expect(await table.rows).toHaveCount(data.rowsCount); + await expect(await table.headingRowColumns).toHaveCount(data.headerRowColCount); + await expect(await table.sectionRows).toHaveCount(data.sectionRowCount); + + // verify highlighter row + const highlighter = data.hightlightRow; + await expect(await table.getHighlightRowColumnTitle(1)).toContainText(highlighter.cell12); + await expect(await table.getHighlightRowColumnTitle(2)).toContainText(highlighter.cell13); + await expect(await table.getHighlightRowColumnTitle(3)).toContainText(highlighter.cell14); + + // verify header row cell + const headerCell = data.headerCell5; + await expect(await table.getHeaderColumnTitle(5)).toContainText(headerCell.heading); + await expect(await table.getHeaderColumnPricing(5)).toContainText(headerCell.pricingText); + await expect(await table.getHeaderColumnOutlineButton(5)).toContainText(headerCell.outlineButtonText); + await expect(await table.getHeaderColumnBlueButton(5)).toContainText(headerCell.blueButtonText); + + // verify section row cell + const sectionCell = data.sectionRow2; + await expect(await table.getSectionRowTitle(2)).toContainText(sectionCell.sectionRowTitle); + await expect(await table.getSectionRowCell(2, 2)).toContainText(sectionCell.cell22); + }); + }); + + // Test 4 : Table (merch) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Table block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify table content/specs', async () => { + // verify merch table + await expect(await table.merchTable).toBeVisible(); + + // verify table row, column count + await expect(await table.rows).toHaveCount(data.rowsCount); + await expect(await table.headingRowColumns).toHaveCount(data.headerRowColCount); + await expect(await table.sectionRows).toHaveCount(data.sectionRowCount); + + // verify merch table header row cell + const headerCell = data.headerCell1; + await expect(await table.getHeaderColumnTitle(1)).toContainText(headerCell.heading); + await expect(await table.getHeaderColumnPricing(1)).toContainText(headerCell.pricingText); + await expect(await table.getHeaderColumnAdditionalText(1)).toContainText(headerCell.AdditionalText); + await expect(await table.getHeaderColumnOutlineButton(1)).toContainText(headerCell.outlineButtonText); + await expect(await table.getHeaderColumnBlueButton(1)).toContainText(headerCell.blueButtonText); + + // verify merch table section row cell + const sectionCell = data.sectionRow2; + await expect(await table.getSectionRowMerchContent(2)).toContainText(sectionCell.merchContent); + await expect(await table.getSectionRowMerchContentImg(2)).toBeVisible(); + }); + }); +}); diff --git a/nala/blocks/tabs/tabs.page.js b/nala/blocks/tabs/tabs.page.js new file mode 100644 index 0000000000..30aa9718a3 --- /dev/null +++ b/nala/blocks/tabs/tabs.page.js @@ -0,0 +1,26 @@ +export default class Tabs { + constructor(page, nth = 0) { + this.page = page; + // tabs locators + this.tab = this.page.locator('.tabs').nth(nth); + this.xlTab = this.page.locator('.tabs.xl-spacing').nth(nth); + this.queitDarkTab = this.page.locator('.tabs.quiet.dark.center').nth(nth); + // tabs list + this.tabList = this.tab.locator('.tabList'); + this.tabListContainer = this.tabList.locator('.tab-list-container'); + this.tabsCount = this.tabListContainer.locator('button[role="tab"]'); + this.tab1 = this.tabListContainer.locator('button[role="tab"]').nth(0); + this.tab2 = this.tabListContainer.locator('button[role="tab"]').nth(1); + this.tab3 = this.tabListContainer.locator('button[role="tab"]').nth(2); + this.tab9 = this.tabListContainer.locator('button[role="tab"]:nth-child(9)'); + // tabs panel and content + this.tabContent = this.tab.locator('.tab-content > .tab-content-container'); + this.tab1Panel = this.tabContent.locator('div[role="tabpanel"]:nth-child(1)'); + this.tab2Panel = this.tabContent.locator('div[role="tabpanel"]:nth-child(2)'); + this.tab3Panel = this.tabContent.locator('div[role="tabpanel"]:nth-child(3)'); + this.tab9Panel = this.tabContent.locator('div[role="tabpanel"]:nth-child(9)'); + + this.leftArrow = this.tab.locator('.tab-paddles > .paddle-left'); + this.rightArrow = this.tab.locator('.tab-paddles > .paddle-right'); + } +} diff --git a/nala/blocks/tabs/tabs.spec.js b/nala/blocks/tabs/tabs.spec.js new file mode 100644 index 0000000000..1edd7cfc0c --- /dev/null +++ b/nala/blocks/tabs/tabs.spec.js @@ -0,0 +1,37 @@ +module.exports = { + FeatureName: 'Tabs Block', + features: [ + { + tcid: '0', + name: '@Tabs (xl-spacing)', + path: '/drafts/nala/blocks/tabs/tabs-xl-spacing', + data: { + tabsCount: 3, + activeTab: 2, + tab1Text: 'Here is tab 1 content', + tab2Text: 'Here is tab 2 content and it is active tab', + tab3Text: 'Here is tab 3 content', + }, + tags: '@tabs @smoke @regression @milo @t1', + }, + { + tcid: '1', + name: '@Tabs (Quiet, Dark, Center)', + path: '/drafts/nala/blocks/tabs/tabs-quiet-dark-center', + data: { + tabsCount: 3, + activeTab: 2, + tab1Text: 'Here is tab 1 content', + tab2Text: 'Here is tab 2 content and it is active tab', + tab3Text: 'Here is tab 3 content', + }, + tags: '@tabs @smoke @t1 @regression @milo', + }, + { + tcid: '2', + name: 'Tabs scrolling', + path: '/drafts/nala/blocks/tabs/tabs-scrolling', + tags: '@tabs @tabs-scrolling @smoke @regression @milo @bacom', + }, + ], +}; diff --git a/nala/blocks/tabs/tabs.test.js b/nala/blocks/tabs/tabs.test.js new file mode 100644 index 0000000000..be296cf66b --- /dev/null +++ b/nala/blocks/tabs/tabs.test.js @@ -0,0 +1,140 @@ +import { expect, test } from '@playwright/test'; +import { features } from './tabs.spec.js'; +import TabBlock from './tabs.page.js'; + +let tab; + +const miloLibs = process.env.MILO_LIBS || ''; +const INTERVALS = Array(5).fill(1000); + +test.describe('Milo Tab block feature test suite', () => { + test.beforeEach(async ({ page }) => { + tab = new TabBlock(page); + }); + + // Test 0 : Tabs (xl-spacing) + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Tabs block feature test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify tabs content/specs', async () => { + await expect(await tab.xlTab).toBeVisible(); + await expect(await tab.tabsCount).toHaveCount(data.tabsCount); + // verify default tab contents + await expect(await tab.tab2).toHaveAttribute('aria-selected', 'true'); + await expect(await tab.tab2Panel).toBeVisible(); + await expect(await tab.tab2Panel).toContainText(data.tab2Text); + + // click tabs and verify contents + await expect(await tab.tab1).toHaveAttribute('aria-selected', 'false'); + await tab.tab1.click(); + await expect(await tab.tab1Panel).toBeVisible(); + await expect(await tab.tab1Panel).toContainText(data.tab1Text); + + await expect(await tab.tab3).toHaveAttribute('aria-selected', 'false'); + await tab.tab3.click(); + await expect(await tab.tab3Panel).toBeVisible(); + await expect(await tab.tab3Panel).toContainText(data.tab3Text); + }); + }); + + // Test 1 : Tabs (Quiet, Dark, Center) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Tabs block feature test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify tabs content/specs', async () => { + await expect(await tab.queitDarkTab).toBeVisible(); + await expect(await tab.tabsCount).toHaveCount(data.tabsCount); + // verify default tab contents + await expect(await tab.tab2).toHaveAttribute('aria-selected', 'true'); + await expect(await tab.tab2Panel).toBeVisible(); + await expect(await tab.tab2Panel).toContainText(data.tab2Text); + + // click tabs and verify contents + await expect(await tab.tab1).toHaveAttribute('aria-selected', 'false'); + await tab.tab1.click(); + await expect(await tab.tab1Panel).toBeVisible(); + await expect(await tab.tab1Panel).toContainText(data.tab1Text); + + await expect(await tab.tab3).toHaveAttribute('aria-selected', 'false'); + await tab.tab3.click(); + await expect(await tab.tab3Panel).toBeVisible(); + await expect(await tab.tab3Panel).toContainText(data.tab3Text); + }); + }); + + test(`Tabs scrolling with arrow buttons, ${features[2].tags}`, async ({ page, baseURL, isMobile }) => { + console.log(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('networkidle'); + + await test.step('checking the setup', async () => { + await expect(tab.tab1).toBeVisible(); + await expect(tab.tab1Panel).toBeVisible(); + await expect(tab.tab9).toBeVisible(); + await expect(tab.tab9Panel).not.toBeVisible(); + await expect(tab.tab1).toBeInViewport(); + await expect(tab.tab9).not.toBeInViewport(); + await expect(await tab.tab1.getAttribute('aria-selected')).toBe('true'); + await expect(await tab.tab9.getAttribute('aria-selected')).toBe('false'); + }); + + await test.step('select the right tab arrow to get to the last tab', async () => { + if (isMobile) { + await expect(async () => { + await tab.rightArrow.click(); + await expect(tab.tab9).toBeInViewport({ timeout: 1000 }); + await expect(tab.leftArrow).toBeVisible({ timeout: 1000 }); + }).toPass({ intervals: INTERVALS }); + } else { + await tab.rightArrow.click(); + await expect(tab.tab9).toBeInViewport(); + await expect(tab.leftArrow).toBeVisible(); + } + await tab.tab9.click(); + + await expect(await tab.tab1.getAttribute('aria-selected')).toBe('false'); + await expect(await tab.tab9.getAttribute('aria-selected')).toBe('true'); + await expect(tab.tab1).not.toBeInViewport(); + await expect(tab.tab9).toBeInViewport(); + await expect(tab.tab1Panel).not.toBeVisible(); + await expect(tab.tab9Panel).toBeVisible(); + }); + + await test.step('select the left tab arrow to get back to the first tab', async () => { + if (isMobile) { + await expect(async () => { + await tab.leftArrow.click(); + await expect(tab.tab1).toBeInViewport({ timeout: 1000 }); + await expect(tab.rightArrow).toBeVisible({ timeout: 1000 }); + }).toPass({ intervals: INTERVALS }); + } else { + await tab.leftArrow.click(); + await expect(tab.tab1).toBeInViewport(); + await expect(tab.rightArrow).toBeVisible(); + } + + await tab.tab1.click(); + + await expect(await tab.tab1.getAttribute('aria-selected')).toBe('true'); + await expect(await tab.tab9.getAttribute('aria-selected')).toBe('false'); + await expect(tab.tab1).toBeInViewport(); + await expect(tab.tab9).not.toBeInViewport(); + await expect(tab.tab1Panel).toBeVisible(); + await expect(tab.tab9Panel).not.toBeVisible(); + }); + }); +}); diff --git a/nala/blocks/text/text.page.js b/nala/blocks/text/text.page.js new file mode 100644 index 0000000000..ee70ba3b4f --- /dev/null +++ b/nala/blocks/text/text.page.js @@ -0,0 +1,115 @@ +export default class Text { + constructor(page, nth = 0) { + this.page = page; + // text locators + this.text = page.locator('.text').nth(nth); + this.textIntro = this.page.locator('.text.intro'); + this.textFullWidth = this.page.locator('.text.full-width'); + this.textFullWidthLarge = this.page.locator('.text.full-width.large'); + this.textLongFormLarge = this.page.locator('.text.long-form'); + this.textInsetLargeMSpacing = this.page.locator('.text.inset.medium.m-spacing'); + this.textlegal = this.page.locator('.text.legal.text-block.con-block.has-bg'); + this.textLinkFarm = this.page.locator('.text.link-farm.text-block.con-block.has-bg'); + + this.detailM = page.locator('.detail-m'); + this.introDetailM = page.locator('.detail-m'); + this.longFormDetailL = page.locator('.detail-l'); + this.legalDetail = page.locator('.foreground'); + + this.headline = this.text.locator('#text'); + this.introHeadline = this.text.locator('#text-intro'); + this.fullWidthHeadline = this.text.locator('#text-full-width'); + this.fullWidthLargeHeadline = this.text.locator('#text-full-width-large'); + this.longFormLargeHeadline = this.text.locator('#text-long-form-large'); + this.insetLargeMSpacingHeadline = this.text.locator('#text-inset-large-m-spacing'); + this.linkFarmHeadline = this.text.locator('#text-link-farm-title'); + this.linkFarmcolumnheading = this.text.locator('#heading-1'); + + this.linkFarmcolumns = this.text.locator('h3'); + this.linkColumnOne = this.text.locator('div div:nth-child(1) a'); + this.linkFormText = this.text.locator('p').nth(1); + + this.bodyXSS = this.text.locator('.body-xxs').first(); + this.bodyM = this.text.locator('.body-m').first(); + this.bodyL = this.text.locator('.body-l').first(); + this.propertiesHeadingM = this.text.locator('#properties-h3').first(); + + this.outlineButton = this.text.locator('.con-button.outline'); + this.actionAreaLink = this.page.locator('.body-m.action-area a').nth(1); + this.bodyLink = this.page.locator('.body-m a'); + + this.insetLargeMSpacingList1 = this.page.locator('.text.inset.medium.m-spacing ul').nth(0); + this.listOneItems = this.insetLargeMSpacingList1.locator('li'); + + this.insetLargeMSpacingList2 = this.page.locator('.text.inset.medium.m-spacing ul').nth(1); + this.listTwoItems = this.insetLargeMSpacingList2.locator('li'); + + this.generalTermsOfUse = this.textlegal.locator('.body-xxs').nth(1); + this.publishText = this.textlegal.locator('.body-xxs').nth(2); + this.generalTerms = this.textlegal.locator('.body-xxs').nth(4); + this.legalInfoLink = this.textlegal.locator('.body-xxs').nth(5); + + // text block contents css + this.cssProperties = { + 'detail-m': { + 'font-size': '12px', + 'line-height': '15px', + }, + 'detail-l': { + 'font-size': '16px', + 'line-height': '20px', + }, + 'heading-s': { + 'font-size': '20px', + 'line-height': '25px', + }, + 'heading-m': { + 'font-size': '24px', + 'line-height': '30px', + }, + 'heading-l': { + 'font-size': '28px', + 'line-height': '35px', + }, + 'heading-xl': { + 'font-size': '36px', + 'line-height': '45px', + }, + 'body-xss': { + 'font-size': '12px', + 'line-height': '18px', + }, + 'body-m': { + 'font-size': '18px', + 'line-height': '27px', + }, + 'body-l': { + 'font-size': '20px', + 'line-height': '30px', + }, + foreground: { + 'font-size': '12px', + 'line-height': '18px', + }, + }; + + // text block contents attributes + this.attProperties = { + text: { class: 'text text-block con-block' }, + 'text-intro': { + class: 'text intro text-block con-block has-bg max-width-8-desktop xxl-spacing-top xl-spacing-bottom', + style: 'background: rgb(255, 255, 255);', + }, + 'text-full-width': { class: 'text full-width text-block con-block max-width-8-desktop center xxl-spacing' }, + 'text-full-width-large': { class: 'text full-width large text-block con-block max-width-8-desktop center xxl-spacing' }, + 'text-long-form-large': { class: 'text long-form large text-block con-block max-width-8-desktop' }, + 'text-inset-medium-m-spacing': { class: 'text inset medium m-spacing text-block con-block max-width-8-desktop' }, + 'text-legal': { class: 'text legal text-block con-block has-bg' }, + 'text-Link-farm': { + class: 'text link-farm text-block con-block has-bg', + style: 'background: rgb(255, 255, 255);', + }, + headingprops: { id: 'heading-1' }, + }; + } +} diff --git a/nala/blocks/text/text.spec.js b/nala/blocks/text/text.spec.js new file mode 100644 index 0000000000..c89c731333 --- /dev/null +++ b/nala/blocks/text/text.spec.js @@ -0,0 +1,97 @@ +/* eslint-disable max-len */ + +module.exports = { + BlockName: 'Text Block', + features: [ + { + tcid: '0', + name: '@Text', + path: '/drafts/nala/blocks/text/text', + data: { + h3Text: 'Text', + bodyText: 'Kick things off with hundreds of premium and free presets you can access with your Lightroom subscription.', + outlineButtonText: 'Learn more', + linkText: 'Explore the premium collection', + }, + tags: '@text @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Text (intro)', + path: '/drafts/nala/blocks/text/text-intro', + data: { + detailText: 'Detail', + h2Text: 'Text (intro)', + bodyText: 'Body L Regular (20/30) Lorem ipsum dolor sit amet,', + }, + tags: '@text @full-intro @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Text (full-width)', + path: '/drafts/nala/blocks/text/text-full-width', + data: { + h3Text: 'Text (full width)', + bodyText: 'Featuring over 600,000 hand-picked stock photos and graphics, ', + linkText: 'Explore the premium collection', + }, + tags: '@text @full-width @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Text (full-width, large)', + path: '/drafts/nala/blocks/text/text-full-width-large', + data: { + h2Text: 'Text (full width, large)', + bodyText: 'Whether your team is creating multichannel campaign assets,', + linkText: 'Learn more our solution', + }, + tags: '@text @full-width-large @smoke @regression @milo', + }, + { + tcid: '4', + name: '@Text (long-form, large)', + path: '/drafts/nala/blocks/text/text-long-form-large', + data: { + detailText: 'Detail', + h2Text: 'Text (long form, large)', + bodyText: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr,', + }, + tags: '@text @long-form-large @smoke @regression @milo', + }, + { + tcid: '5', + name: '@Text (inset, medium, m-spacing)', + path: '/drafts/nala/blocks/text/text-inset-medium-m-spacing', + data: { + h3Text: 'Text (inset, large, m spacing)', + bodyText: 'Lorem ipsum dolor sit amet.', + listCount1: 3, + }, + tags: '@text @inset-medium-m-spacing @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Text (legal)', + path: '/drafts/nala/blocks/text/text-legal', + data: { + termsOfUseText: 'Adobe General Terms of Use', + publishText: 'Published August 1, 2022. Effective as of September 19, 2022.', + generalTermsText: 'These General Terms of Use (“General Terms”), along with any applicable Additional Terms', + linkText: 'Please read complete legal information', + }, + tags: '@text @legal @smoke @regression @milo', + }, + { + tcid: '7', + name: '@Text (link-farm)', + path: '/drafts/nala/blocks/text/text-link-farm', + data: { + headingColumns: 4, + linksCount: 6, + linkText: 'example of a link', + }, + tags: '@text @link-farm @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/text/text.test.js b/nala/blocks/text/text.test.js new file mode 100644 index 0000000000..2b0ca15cb4 --- /dev/null +++ b/nala/blocks/text/text.test.js @@ -0,0 +1,243 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './text.spec.js'; +import TextBlock from './text.page.js'; + +let text; +let webUtil; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Text Block test suite', () => { + test.beforeEach(async ({ page }) => { + text = new TextBlock(page); + webUtil = new WebUtil(page); + }); + + // Test 0 : Text + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Text block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text specs', async () => { + await expect(await text.text).toBeVisible(); + await expect(await text.headline).toContainText(data.h3Text); + await expect(await text.bodyM).toContainText(data.bodyText); + await expect(await text.outlineButton).toContainText(data.outlineButtonText); + + expect(await webUtil.verifyCSS(text.headline, text.cssProperties['heading-l'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.bodyM, text.cssProperties['body-m'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.actionAreaLink, text.cssProperties['body-m'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.text).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + await expect(await text.outlineButton).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.outlineButtonText, 1, data.h3Text)); + await expect(await text.actionAreaLink).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 2, data.h3Text)); + }); + }); + + // Test 1 : Text (intro) + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to Text (intro) block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (intro) specs', async () => { + await expect(text.textIntro).toBeVisible(); + await expect(await text.introHeadline).toContainText(data.h2Text); + await expect(await text.bodyM).toContainText(data.bodyText); + + expect(await webUtil.verifyAttributes(text.textIntro, text.attProperties['text-intro'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.introDetailM, text.cssProperties['detail-m'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.introHeadline, text.cssProperties['heading-l'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.bodyM, text.cssProperties['body-m'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textIntro).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + }); + }); + + // Test 2 : Text (full-width) + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to Text (full width) block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (full width) specs', async () => { + await expect(text.textFullWidth).toBeVisible(); + + await expect(await text.fullWidthHeadline).toContainText(data.h3Text); + await expect(await text.bodyM).toContainText(data.bodyText); + await expect(await text.bodyLink).toContainText(data.linkText); + + expect(await webUtil.verifyAttributes(await text.textFullWidth, text.attProperties['text-full-width'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await text.fullWidthHeadline, text.cssProperties['heading-l'])).toBeTruthy(); + expect(await webUtil.verifyCSS(await text.bodyM, text.cssProperties['body-m'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textFullWidth).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + await expect(await text.bodyLink).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 1, data.h3Text)); + }); + }); + + // Test 3 : Text (full-width, large) + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to text (full-width, large) block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (full-width, large) specs', async () => { + await expect(text.textFullWidthLarge).toBeVisible(); + + await expect(await text.fullWidthLargeHeadline).toContainText(data.h2Text); + await expect(await text.bodyM).toContainText(data.bodyText); + await expect(await text.bodyLink).toContainText(data.linkText); + + expect(await webUtil.verifyAttributes(text.textFullWidthLarge, text.attProperties['text-full-width-large'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.fullWidthLargeHeadline, text.cssProperties['heading-xl'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.bodyM, text.cssProperties['body-m'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textFullWidthLarge).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + await expect(await text.bodyLink).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 1, data.h2Text)); + }); + }); + + // Test 4 : Text (long-form, large) + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to Text (long form, large) block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (long form, large) specs', async () => { + await expect(await text.textLongFormLarge).toBeVisible(); + + await expect(await text.longFormDetailL).toContainText(data.detailText); + await expect(await text.longFormLargeHeadline).toContainText(data.h2Text); + await expect(await text.bodyL).toContainText(data.bodyText); + + expect(await webUtil.verifyAttributes(text.textLongFormLarge, text.attProperties['text-long-form-large'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.longFormDetailL, text.cssProperties['detail-l'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.longFormLargeHeadline, text.cssProperties['heading-l'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.bodyL, text.cssProperties['body-l'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textLongFormLarge).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + }); + }); + + // Test 5 : Text (inset, medium, m-spacing) + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-1: Go to Text (inset, medium, m-spacing ) block test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (inset, large, m spacing) specs', async () => { + await expect(await text.textInsetLargeMSpacing).toBeVisible(); + + await expect(await text.insetLargeMSpacingHeadline).toContainText(data.h3Text); + await expect(await text.bodyL).toContainText(data.bodyText); + await expect(await text.listOneItems).toHaveCount(data.listCount1); + + expect(await webUtil.verifyAttributes(text.textInsetLargeMSpacing, text.attProperties['text-inset-medium-m-spacing'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.insetLargeMSpacingHeadline, text.cssProperties['heading-m'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.bodyL, text.cssProperties['body-l'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.propertiesHeadingM, text.cssProperties['heading-m'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textInsetLargeMSpacing).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + }); + }); + + // Test 6 : Text (legal) + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-1: Go to Text (legal) block test page', async () => { + await page.goto(`${baseURL}${features[6].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (legal) specs', async () => { + await expect(await text.textlegal).toBeVisible(); + + await expect(await text.generalTermsOfUse).toContainText(data.termsOfUseText); + await expect(await text.publishText).toContainText(data.publishText); + await expect(await text.generalTerms).toContainText(data.generalTermsText); + await expect(await text.legalInfoLink).toContainText(data.linkText); + + expect(await webUtil.verifyAttributes(text.textlegal, text.attProperties['text-legal'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.bodyXSS, text.cssProperties['body-xss'])).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textlegal).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + }); + }); + + test(`${features[7].name},${features[7].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[7].path}${miloLibs}`); + const { data } = features[7]; + + await test.step('step-1: Go to Text (link-farm) block test page', async () => { + await page.goto(`${baseURL}${features[7].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[7].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Text (link-farm) specs', async () => { + await expect(await text.textLinkFarm).toBeVisible(); + + await expect(await text.linkFarmcolumns).toHaveCount(data.headingColumns); + await expect(await text.linkColumnOne).toHaveCount(data.linksCount); + await expect(await text.linkFormText).toContainText(data.linkText); + + expect(await webUtil.verifyAttributes(text.textLinkFarm, text.attProperties['text-Link-farm'])).toBeTruthy(); + expect(await webUtil.verifyCSS(text.linkFarmHeadline, text.cssProperties['heading-l'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(text.linkFarmcolumnheading, text.attProperties.headingprops)).toBeTruthy(); + }); + + await test.step('step-3: Verify analytics attributes', async () => { + await expect(await text.textLinkFarm).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('text', 1)); + }); + }); +}); diff --git a/nala/blocks/video/video.page.js b/nala/blocks/video/video.page.js new file mode 100644 index 0000000000..a68ae99911 --- /dev/null +++ b/nala/blocks/video/video.page.js @@ -0,0 +1,72 @@ +export default class Video { + constructor(page, nth = 0) { + this.page = page; + + // video locators + this.section = this.page.locator('.section').nth(nth); + this.content = this.page.locator('.content').nth(nth); + this.fragment = this.page.locator('.fragment'); + this.video = this.page.locator('.content video'); + this.videoSource = this.video.locator('source'); + this.miloVideo = this.page.locator('.milo-video'); + this.iframe = this.page.locator('iframe').first(); + this.mpcPlayerTitle = this.page.frameLocator('iframe').first().locator('.mpc-player__title'); + this.mpcPlayButton = this.page.frameLocator('iframe').first().locator('button .mpc-large-play.mpc-player__large-play'); + this.mpcMutedButton = this.page.frameLocator('iframe').first().locator('.mpc-player button[aria-label="Mute"]'); + this.mpcMutedLabel = this.page.frameLocator('iframe').first().locator('.mpc-player button[aria-label="Mute"] span'); + this.youtubePlayButton = this.page.locator('button.lty-playbtn'); + this.liteYoutube = this.page.locator('lite-youtube'); + this.modalVideo = this.fragment.locator('video'); + this.modalVideoSource = this.modalVideo.locator('source'); + this.consonantCardsGrid = this.page.locator('.consonant-CardsGrid'); + this.consonantCards = this.consonantCardsGrid.locator('.card.consonant-Card'); + this.video = this.page.locator('.content video'); + this.videoSource = this.video.locator('source'); + + // video block attributes + this.attributes = { + 'video.default': { + playsinline: '', + controls: '', + }, + 'video.source': { + type: 'video/mp4', + src: /.*.mp4/, + }, + 'video.autoplay': { + playsinline: '', + autoplay: '', + loop: '', + muted: '', + }, + 'video.autoplay.once': { + playsinline: '', + autoplay: '', + muted: '', + }, + 'video.hover.play': { + playsinline: '', + autoplay: '', + muted: '', + 'data-hoverplay': '', + 'data-mouseevent': 'true', + }, + 'iframe-mpc': { + class: 'adobetv', + scrolling: 'no', + allowfullscreen: '', + loading: 'lazy', + }, + 'iframe-youtube': { + class: 'youtube', + scrolling: 'no', + allowfullscreen: '', + allow: 'encrypted-media; accelerometer; gyroscope; picture-in-picture', + }, + analytics: { + 'section.daa-lh': { 'daa-lh': /s[1-9]/ }, + 'content.daa-lh': { 'daa-lh': /b[1-9]|content|default|default/ }, + }, + }; + } +} diff --git a/nala/blocks/video/video.spec.js b/nala/blocks/video/video.spec.js new file mode 100644 index 0000000000..80ca3350d8 --- /dev/null +++ b/nala/blocks/video/video.spec.js @@ -0,0 +1,84 @@ +module.exports = { + FeatureName: 'Video Block', + features: [ + { + tcid: '0', + name: '@Video Default', + path: '/drafts/nala/blocks/video/default-video', + data: { h2Text: 'Default video' }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '1', + name: '@Video autoplay loop', + path: '/drafts/nala/blocks/video/video-autoplay-loop', + data: { h2Text: 'Autoplay enabled video' }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '2', + name: '@Video autoplay loop once', + path: '/drafts/nala/blocks/video/autoplay-loop-once', + data: { h2Text: 'Autoplay once enabled video' }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '3', + name: '@Video hover play', + path: '/drafts/nala/blocks/video/video-hover-play', + data: { h2Text: 'Hover play enabled video (combined with #_autoplay1 for feature to work)' }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '4', + name: '@MPC Video', + path: '/drafts/nala/blocks/video/mpc-video', + data: { + h1Title: '1856730_Summit_2021_Marquee_1440x1028_v1.0.mp4', + iframeTitle: 'Adobe Video Publishing Cloud Player', + source: 'https://video.tv.adobe.com/v/332632', + }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '5', + name: '@MPC Video Autoplay Looping', + path: '/drafts/nala/blocks/video/mpc-video-autoplay-looping', + data: { + iframeTitle: 'Adobe Video Publishing Cloud Player', + source: 'https://video.tv.adobe.com/v/332632?autoplay=true&end=replay', + }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '6', + name: '@Youtube Video ', + path: '/drafts/nala/blocks/video/youtube-video', + data: { + h1Text: 'YouTube video', + playLabel: 'Adobe MAX Keynote 2022 | Adobe Creative Cloud', + source: 'https://www.youtube.com/embed/OfQKEzgPaBA?', + videoId: 'OfQKEzgPaBA', + }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '7', + name: '@Fragment Modal video inline', + path: '/drafts/nala/blocks/video/fragments-modal-video-autoplay', + data: + { source: 'https://main--milo--adobecom.hlx.live/libs/media_1e798d01c6ddc7e7eadc8f134d69e4f8d7193fdbb.mp4' }, + tags: '@video @smoke @regression @milo', + }, + { + tcid: '8', + name: '@Modal video with cards', + path: '/drafts/nala/blocks/video/modal-video-with-cards', + data: { + cardsCount: 3, + source: 'https://milo.adobe.com/libs/media_1e798d01c6ddc7e7eadc8f134d69e4f8d7193fdbb.mp4', + }, + tags: '@video @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/video/video.test.js b/nala/blocks/video/video.test.js new file mode 100644 index 0000000000..a3ab0e2e7d --- /dev/null +++ b/nala/blocks/video/video.test.js @@ -0,0 +1,212 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './video.spec.js'; +import VideoBlock from './video.page.js'; + +let webUtil; +let video; +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Video Block test suite', () => { + test.beforeEach(async ({ page }) => { + webUtil = new WebUtil(page); + video = new VideoBlock(page); + }); + + // Test 0 : Video default + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.video).toBeVisible(); + await expect(await video.content).toContainText(data.h2Text); + + await expect(await webUtil.verifyAttributes(video.video, video.attributes['video.default'])).toBeTruthy(); + await expect(await webUtil.verifyAttributes(video.videoSource, video.attributes['video.source'])).toBeTruthy(); + }); + }); + + // Test 1 : Video autoplay loop + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.video).toBeVisible(); + await expect(await video.content).toContainText(data.h2Text); + + expect(await webUtil.verifyAttributes(video.video, video.attributes['video.autoplay'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(video.videoSource, video.attributes['video.source'])).toBeTruthy(); + }); + }); + + // Test 2 : Video autoplay loop once + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.video).toBeVisible(); + await expect(await video.content).toContainText(data.h2Text); + + expect(await webUtil.verifyAttributes(video.video, video.attributes['video.autoplay.once'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(video.videoSource, video.attributes['video.source'])).toBeTruthy(); + }); + }); + + // Test 3 : Video hover play + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.video).toBeVisible(); + await expect(await video.content).toContainText(data.h2Text); + await new Promise((resolve) => { setTimeout(resolve, 5000); }); + await video.video.hover({ force: true }); + + expect(await webUtil.verifyAttributes(video.video, video.attributes['video.autoplay.once'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(video.videoSource, video.attributes['video.source'])).toBeTruthy(); + }); + }); + + // Test 4 : MPC Video + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[4].path}${miloLibs}`); + const { data } = features[4]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[4].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[4].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.miloVideo).toBeVisible(); + await expect(await video.iframe).toBeVisible(); + + await expect(await video.iframe).toHaveAttribute('title', data.iframeTitle); + await expect(await video.iframe).toHaveAttribute('src', data.source); + expect(await webUtil.verifyAttributes(video.iframe, video.attributes['iframe-mpc'])).toBeTruthy(); + }); + }); + + // Test 5 : MPC Video Autoplay Looping + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[5].path}${miloLibs}`); + const { data } = features[5]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[5].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[5].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.miloVideo).toBeVisible(); + await expect(await video.iframe).toHaveAttribute('title', data.iframeTitle); + await expect(await video.iframe).toHaveAttribute('src', data.source); + expect(await webUtil.verifyAttributes(video.iframe, video.attributes['iframe-mpc'])).toBeTruthy(); + }); + }); + + // Test 6 : Youtube Video + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[6].path}${miloLibs}`); + const { data } = features[6]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[6].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[6].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.miloVideo).toBeVisible(); + await expect(await video.youtubePlayButton).toBeVisible(); + await expect(await video.youtubePlayButton).toHaveAttribute('type', 'button'); + + await expect(await video.liteYoutube).toHaveAttribute('playlabel', data.playLabel); + await expect(await video.liteYoutube).toHaveAttribute('videoid', data.videoId); + }); + }); + + // Test 7 : Modal Video default + test(`${features[7].name},${features[7].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[7].path}${miloLibs}`); + // const { data } = features[7]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[7].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[7].path}${miloLibs}`); + }); + + await test.step('step-2: Verify video block content/specs', async () => { + await expect(await video.modalVideo).toBeVisible(); + + expect(await webUtil.verifyAttributes(video.modalVideo, video.attributes['video.autoplay'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(video.modalVideoSource, video.attributes['video.source'])).toBeTruthy(); + + const srcAttributeValue = await video.modalVideoSource.getAttribute('src'); + console.log('[video source]:', srcAttributeValue); + expect(srcAttributeValue).not.toBe(''); + }); + }); + + // Test 8 : Modal video with cards + test(`${features[8].name},${features[8].tags}`, async ({ page, baseURL }) => { + test.slow(); + console.info(`[Test Page]: ${baseURL}${features[8].path}${miloLibs}`); + const { data } = features[8]; + + await test.step('step-1: Go to video block test page', async () => { + await page.goto(`${baseURL}${features[8].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[8].path}${miloLibs}`); + }); + + await test.step('step-2: Verify consonant cards with modal video block content/specs', async () => { + await expect(await video.consonantCardsGrid).toBeVisible(); + await expect(await video.consonantCards.nth(0)).toBeVisible(); + await expect(await video.consonantCards).toHaveCount(data.cardsCount); + + await expect(await video.modalVideo).toBeVisible(); + expect(await webUtil.verifyAttributes(video.modalVideo, video.attributes['video.autoplay'])).toBeTruthy(); + expect(await webUtil.verifyAttributes(video.modalVideoSource, video.attributes['video.source'])).toBeTruthy(); + + const srcAttributeValue = await video.modalVideoSource.getAttribute('src'); + console.log('[video source]:', srcAttributeValue); + expect(srcAttributeValue).not.toBe(''); + }); + }); +}); diff --git a/nala/blocks/zpattern/zpattern.page.js b/nala/blocks/zpattern/zpattern.page.js new file mode 100644 index 0000000000..7ca8ddc2ae --- /dev/null +++ b/nala/blocks/zpattern/zpattern.page.js @@ -0,0 +1,39 @@ +export default class ZPattern { + constructor(page, nth = 0) { + this.page = page; + // z-pattern locators + this.zPattern = page.locator('.z-pattern').nth(nth); + + // zpatter header + this.zPatternHeader = this.zPattern.locator('.heading-row'); + this.zPatternPText = this.zPatternHeader.locator('p'); + + this.smallIntroHeadingText = this.zPattern.locator('#small-default-intro-text-optional'); + this.mediumIntroHeadingText = this.zPattern.locator('#medium-intro-text-optional'); + this.largeIntroHeadingText = this.zPattern.locator('#large-intro-text-optional'); + this.darkIntroHeadingText = this.zPattern.locator('#intuitive-block-authoring'); + + this.zPatternMediaBlocks = this.zPattern.locator('.media'); + this.mediaBlocks = this.zPattern.locator('.media'); + + // zpattern contents attributes + this.attProperties = { + 'z-pattern': { style: 'background: rgb(245, 245, 245);' }, + 'z-pattern-dark': { style: 'background: rgb(50, 50, 50);' }, + 'small-default-intro-text-optional': { class: 'heading-l headline' }, + 'medium-intro-text-optional': { class: 'heading-l headline' }, + 'large-intro-text-optional': { class: 'heading-xl headline' }, + 'dark-intro-text-optional': { class: 'heading-l headline' }, + 'media-medium': { class: 'media medium con-block' }, + 'small-media-reversed': { class: 'media small media-reversed con-block' }, + 'medium-media-reversed': { class: 'media medium media-reversed con-block' }, + 'medium-media-reverse-mobile': { class: 'media medium con-block media-reverse-mobile' }, + 'large-media-reversed': { class: 'media large media-reversed con-block' }, + 'media-image': { + width: '600', + height: '300', + }, + + }; + } +} diff --git a/nala/blocks/zpattern/zpattern.spec.js b/nala/blocks/zpattern/zpattern.spec.js new file mode 100644 index 0000000000..a531eb438a --- /dev/null +++ b/nala/blocks/zpattern/zpattern.spec.js @@ -0,0 +1,131 @@ +module.exports = { + BlockName: 'ZPattern', + features: [ + { + tcid: '0', + name: '@ZPattern', + path: '/drafts/nala/blocks/zpattern/z-pattern', + data: { + headingText: 'Medium Intro Text (optional)', + introText: 'Perspiciatis unde omnis iste natus error', + mediaBlockCount: 3, + mediaBlocks: [ + { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 z-pattern medium', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 z-pattern medium', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 z-pattern medium', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + ], + }, + tags: '@zpattern @smoke @regression @milo', + }, + { + tcid: '1', + name: '@ZPattern (small)', + path: '/drafts/nala/blocks/zpattern/z-pattern-small', + data: { + headingText: 'Small (default) Intro Text (optional)', + introText: 'Media blocks may use one of three background colors', + mediaBlockCount: 3, + mediaBlocks: [ + { + detailText: 'Detail M 12/15', + h2Text: 'Heading XS 18/22 z-pattern small', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + { + detailText: 'Detail M 12/15', + h2Text: 'Heading XS 18/22 z-pattern small', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + { + detailText: 'Detail M 12/15', + h2Text: 'Heading XS 18/22 z-pattern small', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + ], + }, + tags: '@zpattern @zpattern-small @smoke @regression @milo', + }, + + { + tcid: '2', + name: '@Zpattern (large)', + path: '/drafts/nala/blocks/zpattern/z-pattern-large', + data: { + headingText: 'Large Intro Text (optional)', + introText: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium', + mediaBlockCount: 3, + mediaBlocks: [ + { + detailText: 'Detail L 16/20', + h2Text: 'Heading XL 36/45 z-pattern large', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + { + detailText: 'Detail L 16/20', + h2Text: 'Heading XL 36/45 z-pattern large', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + { + detailText: 'Detail L 16/20', + h2Text: 'Heading XL 36/45 z-pattern large', + bodyText: 'Body M 18/27 Lorem ipsum dolor sit amet', + blueButtonText: 'learn more', + }, + ], + }, + tags: '@zpattern @zpattern-large @smoke @regression @milo', + }, + + { + tcid: '3', + name: '@Zpattern (dark)', + path: '/drafts/nala/blocks/zpattern/z-pattern-dark', + data: { + headingText: 'Intuitive block authoring', + introText: 'Supports alternating or inline authoring preferences', + mediaBlockCount: 3, + mediaBlocks: [ + { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 z-pattern medium', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'Learn More', + }, + { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 z-pattern medium', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'Learn More', + }, + { + detailText: 'Detail M 12/15', + h2Text: 'Heading M 24/30 z-pattern medium', + bodyText: 'Body S 16/24 Lorem ipsum dolor sit amet', + blueButtonText: 'Learn More', + }, + ], + }, + tags: '@zpattern @zpattern-dark @smoke @regression @milo', + }, + ], +}; diff --git a/nala/blocks/zpattern/zpattern.test.js b/nala/blocks/zpattern/zpattern.test.js new file mode 100644 index 0000000000..a4e298677d --- /dev/null +++ b/nala/blocks/zpattern/zpattern.test.js @@ -0,0 +1,168 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './zpattern.spec.js'; +import ZPatternBlock from './zpattern.page.js'; + +let webUtil; +let zpattern; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Milo Z Pattern Block test suite', () => { + test.beforeEach(async ({ page }) => { + webUtil = new WebUtil(page); + zpattern = new ZPatternBlock(page); + }); + + // Test 0 : ZPattern default block + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}${miloLibs}`); + const { data } = features[0]; + + await test.step('step-1: Go to Z Pattern block test page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('step-2: Verify Z Pattern block specs', async () => { + await expect(await zpattern.zPatternHeader).toContainText(data.headingText); + await expect(await zpattern.zPatternPText).toContainText(data.introText); + await expect(await zpattern.mediaBlocks).toHaveCount(data.mediaBlockCount); + + const mediaBlocks = await zpattern.mediaBlocks.all(); + const mediaBlocksArray = await Promise.all(mediaBlocks.map(async (block) => block)); + + for (let i = 0; i < mediaBlocksArray.length; i++) { + const mediaBlock = mediaBlocksArray[i]; + await expect(await mediaBlock.locator('.detail-m')).toContainText(data.mediaBlocks[i].detailText); + await expect(await mediaBlock.locator('.heading-m')).toContainText(data.mediaBlocks[i].h2Text); + await expect(await mediaBlock.locator('.body-s').nth(0)).toContainText(data.mediaBlocks[i].bodyText); + await expect(await mediaBlock.locator('.blue')).toContainText(data.mediaBlocks[i].blueButtonText); + + const image = await mediaBlock.locator('.image img').nth(0); + const classAttribute = await mediaBlock.getAttribute('class'); + const hasReversedClass = classAttribute.includes('media-reversed'); + + if (hasReversedClass) { + expect(await webUtil.verifyAttributes(mediaBlock, zpattern.attProperties['medium-media-reversed'])).toBeTruthy(); + } + expect(await webUtil.verifyAttributes(image, zpattern.attProperties['media-image'])).toBeTruthy(); + } + }); + }); + + // Test 1 :ZPattern (small) block + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}${miloLibs}`); + const { data } = features[1]; + + await test.step('step-1: Go to z-pattern (small) block test page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('step-2: Verify z-pattern (small) block specs', async () => { + await expect(await zpattern.zPatternHeader).toContainText(data.headingText); + await expect(await zpattern.zPatternPText).toContainText(data.introText); + await expect(await zpattern.mediaBlocks).toHaveCount(data.mediaBlockCount); + + const mediaBlocks = await zpattern.mediaBlocks.all(); + const mediaBlocksArray = await Promise.all(mediaBlocks.map(async (block) => block)); + + for (let i = 0; i < mediaBlocksArray.length; i++) { + const mediaBlock = mediaBlocksArray[i]; + await expect(await mediaBlock.locator('.detail-m')).toContainText(data.mediaBlocks[i].detailText); + await expect(await mediaBlock.locator('.heading-xs')).toContainText(data.mediaBlocks[i].h2Text); + await expect(await mediaBlock.locator('.body-s').nth(0)).toContainText(data.mediaBlocks[i].bodyText); + await expect(await mediaBlock.locator('.blue')).toContainText(data.mediaBlocks[i].blueButtonText); + + const image = await mediaBlock.locator('.image img').nth(0); + const classAttribute = await mediaBlock.getAttribute('class'); + const hasReversedClass = classAttribute.includes('media-reversed'); + + if (hasReversedClass) { + expect(await webUtil.verifyAttributes(mediaBlock, zpattern.attProperties['small-media-reversed'])).toBeTruthy(); + } + expect(await webUtil.verifyAttributes(image, zpattern.attProperties['media-image'])).toBeTruthy(); + } + }); + }); + + // Test 2 :Zpattern (large) block + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}${miloLibs}`); + const { data } = features[2]; + + await test.step('step-1: Go to z-pattern (large) block test page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('step-2: Verify z-pattern (large) block specs', async () => { + await expect(await zpattern.zPatternHeader).toContainText(data.headingText); + await expect(await zpattern.zPatternPText).toContainText(data.introText); + await expect(await zpattern.mediaBlocks).toHaveCount(data.mediaBlockCount); + + const mediaBlocks = await zpattern.mediaBlocks.all(); + const mediaBlocksArray = await Promise.all(mediaBlocks.map(async (block) => block)); + + for (let i = 0; i < mediaBlocksArray.length; i++) { + const mediaBlock = mediaBlocksArray[i]; + await expect(await mediaBlock.locator('.detail-l')).toContainText(data.mediaBlocks[i].detailText); + await expect(await mediaBlock.locator('.heading-xl')).toContainText(data.mediaBlocks[i].h2Text); + await expect(await mediaBlock.locator('.body-m').nth(0)).toContainText(data.mediaBlocks[i].bodyText); + await expect(await mediaBlock.locator('.blue')).toContainText(data.mediaBlocks[i].blueButtonText); + + const image = await mediaBlock.locator('.image img').nth(0); + const classAttribute = await mediaBlock.getAttribute('class'); + const hasReversedClass = classAttribute.includes('media-reversed'); + + if (hasReversedClass) { + expect(await webUtil.verifyAttributes(mediaBlock, zpattern.attProperties['large-media-reversed'])).toBeTruthy(); + } + expect(await webUtil.verifyAttributes(image, zpattern.attProperties['media-image'])).toBeTruthy(); + } + }); + }); + + // Test 3 :Zpattern (dark) block + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`); + const { data } = features[3]; + + await test.step('step-1: Go to z-pattern (large) block test page', async () => { + await page.goto(`${baseURL}${features[3].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}${miloLibs}`); + }); + + await test.step('step-2: Verify z-pattern (dark) block specs', async () => { + await expect(await zpattern.zPatternHeader).toContainText(data.headingText); + await expect(await zpattern.zPatternPText).toContainText(data.introText); + await expect(await zpattern.mediaBlocks).toHaveCount(data.mediaBlockCount); + + const mediaBlocks = await zpattern.mediaBlocks.all(); + const mediaBlocksArray = await Promise.all(mediaBlocks.map(async (block) => block)); + + for (let i = 0; i < mediaBlocksArray.length; i++) { + const mediaBlock = mediaBlocksArray[i]; + await expect(await mediaBlock.locator('.detail-m')).toContainText(data.mediaBlocks[i].detailText); + await expect(await mediaBlock.locator('.heading-m')).toContainText(data.mediaBlocks[i].h2Text); + await expect(await mediaBlock.locator('.body-s').nth(0)).toContainText(data.mediaBlocks[i].bodyText); + await expect(await mediaBlock.locator('.blue')).toContainText(data.mediaBlocks[i].blueButtonText); + + const image = await mediaBlock.locator('.image img').nth(0); + const classAttribute = await mediaBlock.getAttribute('class'); + const hasReversedClass = classAttribute.includes('media-reversed'); + + if (hasReversedClass) { + expect(await webUtil.verifyAttributes(mediaBlock, zpattern.attProperties['dark-media-reversed'])).toBeTruthy(); + } + expect(await webUtil.verifyAttributes(image, zpattern.attProperties['media-image'])).toBeTruthy(); + } + }); + }); +}); diff --git a/nala/features/commerce/commerce.page.js b/nala/features/commerce/commerce.page.js new file mode 100644 index 0000000000..60edc95c9e --- /dev/null +++ b/nala/features/commerce/commerce.page.js @@ -0,0 +1,19 @@ +export default class CommercePage { + constructor(page) { + this.page = page; + + this.price = page.locator('//span[@data-template="price"]'); + this.priceOptical = page.locator('//span[@data-template="optical"]'); + this.priceStrikethrough = page.locator('//span[@data-template="strikethrough"]'); + this.buyNowCta = page.locator('//a[contains(@daa-ll, "Buy now")]'); + this.freeTrialCta = page.locator('//a[contains(@daa-ll, "Free trial")]'); + this.merchCard = page.locator('merch-card'); + // universal nav login account type + this.loginType = page.locator('div.feds-profile > div > div > ul > li:nth-child(5) > button'); + // entitlement block locators + this.ccAllAppsCTA = page.locator('//*[contains(@daa-ll,"CC All Apps")]'); + this.photoshopBuyCTA = page.locator('//*[contains(@daa-ll,"Buy now-1--Photoshop")]'); + this.photoshopFreeCTA = page.locator('//*[contains(@daa-ll,"Free trial-2--Photoshop")]'); + this.switchModalIframe = page.locator('#switch-modal > div > iframe'); + } +} diff --git a/nala/features/commerce/commerce.spec.js b/nala/features/commerce/commerce.spec.js new file mode 100644 index 0000000000..51b3a27dd9 --- /dev/null +++ b/nala/features/commerce/commerce.spec.js @@ -0,0 +1,89 @@ +module.exports = { + name: 'Commerce', + features: [ + { + tcid: '0', + name: '@Commerce-Price-Term', + path: '/drafts/nala/features/commerce/prices-with-term', + tags: '@commerce @smoke @regression', + }, + { + tcid: '1', + name: '@Commerce-Price-Unit-Term', + path: '/drafts/nala/features/commerce/prices-with-term-unit', + tags: '@commerce @smoke @regression', + + }, + { + tcid: '2', + name: '@Commerce-Price-Taxlabel-Unit-Term', + path: '/drafts/nala/features/commerce/prices-with-term-unit-taxlabel', + tags: '@commerce @smoke @regression', + }, + { + tcid: '3', + name: '@Commerce-Promo', + path: '/drafts/nala/features/commerce/promo-placeholders', + data: { + promo: 'UMRM2MUSPr501YOC', + workflow: 'recommendation', + }, + tags: '@commerce @smoke @regression', + }, + { + tcid: '4', + name: '@Commerce-Upgrade-Entitlement', + path: '/drafts/nala/features/commerce/checkout-links', + data: { UpgradeCTATitle: 'Upgrade now' }, + tags: '@commerce @entitlement @smoke @regression @nopr', + }, + { + tcid: '5', + name: '@Commerce-Download-Entitlement', + path: '/drafts/nala/features/commerce/checkout-links', + data: { + DownloadCTATitle: 'Download', + TrialCTATitle: 'Free trial', + DownloadUrl: 'download/photoshop', + }, + tags: '@commerce @entitlement @smoke @regression @nopr', + }, + { + tcid: '6', + name: '@Commerce-KitchenSink-Smoke', + path: '/docs/library/kitchen-sink/merch-card', + tags: '@commerce @kitchensink @smoke @regression', + }, + { + tcid: '7', + name: '@Commerce-DE', + path: '/de/drafts/nala/features/commerce/promo-placeholders', + data: { + promo: 'PEMAP50AASTE2', + CO: 'co=DE', + lang: 'lang=de', + workflow: 'recommendation', + }, + tags: '@commerce @smoke @regression', + }, + { + tcid: '8', + name: '@Commerce-Old-Promo', + path: '/drafts/nala/features/commerce/promo-old-price', + data: { promo: 'UMRM2MUSPr501YOC' }, + tags: '@commerce @smoke @regression', + }, + { + tcid: '9', + name: '@Commerce-GB', + path: '/uk/drafts/nala/features/commerce/promo-placeholders', + data: { + promo: 'PEMAP50AASTE2', + CO: 'co=GB', + lang: 'lang=en', + workflow: 'recommendation', + }, + tags: '@commerce @smoke @regression', + }, + ], +}; diff --git a/nala/features/commerce/commerce.test.js b/nala/features/commerce/commerce.test.js new file mode 100644 index 0000000000..6f2f404524 --- /dev/null +++ b/nala/features/commerce/commerce.test.js @@ -0,0 +1,474 @@ +import { expect, test } from '@playwright/test'; +import WebUtil from '../../libs/webutil.js'; +import { features } from './commerce.spec.js'; +import CommercePage from './commerce.page.js'; +import FedsLogin from '../feds/login/login.page.js'; +import FedsHeader from '../feds/header/header.page.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +let COMM; +test.beforeEach(async ({ page, baseURL, browserName }) => { + COMM = new CommercePage(page); + if (browserName === 'chromium') { + await page.setExtraHTTPHeaders({ 'sec-ch-ua': '"Chromium";v="123", "Not:A-Brand";v="8"' }); + } + + const skipOn = ['bacom', 'business']; + skipOn.some((skip) => { + if (baseURL.includes(skip)) test.skip(true, `Skipping the commerce tests for ${baseURL}`); + return null; + }); +}); + +test.describe('Commerce feature test suite', () => { + // @Commerce-Price-Term - Validate price with term display + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[0].path}${miloLibs}`; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate regular price display', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.price.innerText()).toContain('US$263.88/yr'); + expect(await COMM.price.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.price.locator('.price-tax-inclusivity').innerText()).toBe(''); + }); + + await test.step('Validate optical price display', async () => { + await COMM.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceOptical.innerText()).toContain('US$21.99/mo'); + expect(await COMM.priceOptical.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.priceOptical.locator('.price-tax-inclusivity').innerText()).toBe(''); + }); + + await test.step('Validate strikethrough price display', async () => { + await COMM.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceStrikethrough.innerText()).toContain('US$263.88/yr'); + expect(await COMM.priceStrikethrough.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-tax-inclusivity').innerText()).toBe(''); + const priceStyle = await COMM.priceStrikethrough.evaluate( + (e) => window.getComputedStyle(e).getPropertyValue('text-decoration'), + ); + expect(await priceStyle).toContain('line-through'); + }); + }); + + // @Commerce-Price-Unit-Term - Validate price with term and unit display + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[1].path}${miloLibs}`; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate regular price display', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.price.innerText()).toContain('US$'); + expect(await COMM.price.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-unit-type').innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-tax-inclusivity').innerText()).toBe(''); + }); + + await test.step('Validate optical price display', async () => { + await COMM.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceOptical.innerText()).toContain('US$'); + expect(await COMM.priceOptical.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-unit-type').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-tax-inclusivity').innerText()).toBe(''); + }); + + await test.step('Validate strikethrough price display', async () => { + await COMM.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceStrikethrough.innerText()).toContain('US$'); + expect(await COMM.priceStrikethrough.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-unit-type').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-tax-inclusivity').innerText()).toBe(''); + const priceStyle = await COMM.priceStrikethrough.evaluate( + (e) => window.getComputedStyle(e).getPropertyValue('text-decoration'), + ); + expect(await priceStyle).toContain('line-through'); + }); + }); + + // @Commerce-Price-Taxlabel-Unit-Term - Validate price with term, unit and tax label display + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[2].path}${miloLibs}`; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate regular price display', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.price.innerText()).toContain('US$'); + expect(await COMM.price.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-unit-type').innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-tax-inclusivity').innerText()).not.toBe(''); + }); + + await test.step('Validate optical price display', async () => { + await COMM.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceOptical.innerText()).toContain('US$'); + expect(await COMM.priceOptical.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-unit-type').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-tax-inclusivity').innerText()).not.toBe(''); + }); + + await test.step('Validate strikethrough price display', async () => { + await COMM.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceStrikethrough.innerText()).toContain('US$'); + expect(await COMM.priceStrikethrough.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-unit-type').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-tax-inclusivity').innerText()).not.toBe(''); + const priceStyle = await COMM.priceStrikethrough.evaluate( + (e) => window.getComputedStyle(e).getPropertyValue('text-decoration'), + ); + expect(await priceStyle).toContain('line-through'); + }); + }); + + // @Commerce-Promo - Validate price and CTAs have promo code applied + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[3].path}${miloLibs}`; + const { data } = features[3]; + + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate regular price has promo', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.price).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.price).toHaveAttribute('data-display-old-price', 'true'); + await COMM.price.locator('.price').first().waitFor({ state: 'visible', timeout: 10000 }); + await COMM.price.locator('.price-strikethrough').waitFor({ state: 'visible', timeout: 10000 }); + }); + + await test.step('Validate optical price has promo', async () => { + await COMM.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.priceOptical).toHaveAttribute('data-promotion-code', data.promo); + }); + + await test.step('Validate strikethrough price has promo', async () => { + await COMM.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.priceStrikethrough).toHaveAttribute('data-promotion-code', data.promo); + }); + + await test.step('Validate Buy now CTA has promo', async () => { + await COMM.buyNowCta.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.buyNowCta).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.promo}`)); + }); + + await test.step('Validate Free Trial CTA has promo', async () => { + await COMM.freeTrialCta.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.freeTrialCta).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.promo}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.workflow}`)); + }); + }); + + // @Commerce-Upgrade-Entitlement - Validate Upgrade commerce flow + test(`${features[4].name}, ${features[4].tags}`, async ({ page, baseURL }) => { + test.skip(); // Skipping due to missing login + + const testPage = `${baseURL}${features[4].path}${miloLibs}`; + console.info('[Test Page]: ', testPage); + + const { data } = features[4]; + const Login = new FedsLogin(page); + const Header = new FedsHeader(page); + + // Go to test example + await test.step('Go to test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + // Login with Adobe test account: + await test.step('Login with a valid Adobe account', async () => { + await Header.signInButton.click(); + if (COMM.loginType.isVisible()) { + await COMM.loginType.click(); + } + await Login.loginOnAppForm(process.env.IMS_EMAIL_PAID_PS, process.env.IMS_PASS_PAID_PS); + }); + + // Validate Upgrade eligibility check w.r.t Buy CTA + await test.step('Verify cc all apps card cta title', async () => { + await page.waitForLoadState('domcontentloaded'); + await COMM.ccAllAppsCTA.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.ccAllAppsCTA).toHaveText(data.UpgradeCTATitle); + }); + + // Validate Upgrade eligibility check w.r.t Switch modal + await test.step('Verify Switch modal launch for Upgrade', async () => { + await COMM.ccAllAppsCTA.click(); + await COMM.switchModalIframe.waitFor({ state: 'visible', timeout: 45000 }); + await expect(COMM.switchModalIframe).toBeVisible(); + }); + }); + + // @Commerce-Download-Entitlement - Validate Download commerce flow + test(`${features[5].name}, ${features[5].tags}`, async ({ page, baseURL }) => { + test.skip(); // Skipping due to missing login + + const testPage = `${baseURL}${features[5].path}${miloLibs}`; + console.info('[Test Page]: ', testPage); + const { data } = features[5]; + const Login = new FedsLogin(page); + const Header = new FedsHeader(page); + + // Go to test example + await test.step('Go to test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + // Login with Adobe test account: + await test.step('Login with a valid Adobe account', async () => { + await Header.signInButton.click(); + if (COMM.loginType.isVisible()) { + await COMM.loginType.click(); + } + await Login.loginOnAppForm(process.env.IMS_EMAIL_PAID_PS, process.env.IMS_PASS_PAID_PS); + }); + + // Validate Download eligibility check w.r.t Buy CTA + await test.step('Verify photoshop card cta title', async () => { + await page.waitForLoadState('domcontentloaded'); + await COMM.photoshopBuyCTA.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.photoshopBuyCTA).toHaveText(data.DownloadCTATitle); + await expect(COMM.photoshopFreeCTA).toHaveText(data.TrialCTATitle); + }); + + // Validate Download eligibility check w.r.t download link + await test.step('Verify download link for download', async () => { + await COMM.photoshopBuyCTA.click(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.url()).toContain(data.DownloadUrl); + }); + }); + + // @Commerce-KitchenSink-Smoke - Validate commerce CTA and checkout placeholders + test(`${features[6].name}, ${features[6].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[6].path}${miloLibs}`; + const webUtil = new WebUtil(page); + + console.info('[Test Page]: ', testPage); + + // Go to test example + await test.step('Go to test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + // Validate there are no unresolved commerce placeholders + await test.step('Validate wcs placeholders', async () => { + await COMM.merchCard.first().waitFor({ state: 'visible', timeout: 45000 }); + await webUtil.scrollPage('down', 'slow'); + const unresolvedPlaceholders = await page.evaluate( + () => [...document.querySelectorAll('[data-wcs-osi]')].filter( + (el) => !el.classList.contains('placeholder-resolved'), + ), + ); + expect(unresolvedPlaceholders.length).toBe(0); + }); + + // Validate commerce checkout links are indeed commerce + await test.step('Validate checkout links', async () => { + const invalidCheckoutLinks = await page.evaluate( + () => [...document.querySelectorAll('[data-wcs-osi][is="checkout-link"]')].filter( + (el) => !el.getAttribute('href').includes('commerce'), + ), + ); + expect(invalidCheckoutLinks.length).toBe(0); + }); + }); + + // @Commerce-DE - Validate commerce CTA and checkout placeholders in DE locale + test(`${features[7].name}, ${features[7].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[7].path}${miloLibs}`; + const { data } = features[7]; + + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + // Validate there are no unresolved commerce placeholders + await test.step('Validate wcs placeholders', async () => { + const unresolvedPlaceholders = await page.evaluate( + () => [...document.querySelectorAll('[data-wcs-osi]')].filter( + (el) => !el.classList.contains('placeholder-resolved'), + ), + ); + expect(unresolvedPlaceholders.length).toBe(0); + }); + + await test.step('Validate Buy now CTA', async () => { + await COMM.buyNowCta.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.buyNowCta).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.promo}`)); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.CO}`)); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.lang}`)); + }); + + await test.step('Validate Free Trial CTA', async () => { + await COMM.freeTrialCta.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.freeTrialCta).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.promo}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.CO}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.lang}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.workflow}`)); + }); + + await test.step('Validate regular price display', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.price.innerText()).toContain('€/Jahr'); + expect(await COMM.price.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.price.locator('.price-tax-inclusivity').innerText()).toBe(''); + await expect(COMM.price).toHaveAttribute('data-promotion-code', data.promo); + }); + + await test.step('Validate optical price display', async () => { + await COMM.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceOptical.innerText()).toContain('€/Monat'); + expect(await COMM.priceOptical.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.priceOptical.locator('.price-tax-inclusivity').innerText()).toBe(''); + await expect(COMM.priceOptical).toHaveAttribute('data-promotion-code', data.promo); + }); + + await test.step('Validate strikethrough price display', async () => { + await COMM.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceStrikethrough.innerText()).toContain('€/Jahr'); + expect(await COMM.priceStrikethrough.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-tax-inclusivity').innerText()).toBe(''); + const priceStyle = await COMM.priceStrikethrough.evaluate( + (e) => window.getComputedStyle(e).getPropertyValue('text-decoration'), + ); + expect(await priceStyle).toContain('line-through'); + await expect(COMM.priceStrikethrough).toHaveAttribute('data-promotion-code', data.promo); + }); + }); + + // @Commerce-Old-Promo - Validate promo price WITHOUT old price + test(`${features[8].name},${features[8].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[8].path}${miloLibs}`; + const { data } = features[8]; + + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate promo price does not show old price', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.price).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.price).not.toHaveAttribute('data-display-old-price', 'true'); + // expect(await COMM.price.innerText()).toContain('US$17.24'); + // expect(await COMM.price.innerText()).not.toContain('US$34.49'); + await expect(await COMM.price.locator('.price').first()).toBeVisible(); + await expect(await COMM.price.locator('.price-strikethrough')).not.toBeVisible(); + }); + }); + + // @Commerce-GB - Validate commerce CTA and checkout placeholders in UK locale + test(`${features[9].name}, ${features[9].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[9].path}${miloLibs}`; + const { data } = features[9]; + + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + // Validate there are no unresolved commerce placeholders + await test.step('Validate wcs placeholders', async () => { + const unresolvedPlaceholders = await page.evaluate( + () => [...document.querySelectorAll('[data-wcs-osi]')].filter( + (el) => !el.classList.contains('placeholder-resolved'), + ), + ); + expect(unresolvedPlaceholders.length).toBe(0); + }); + + await test.step('Validate Buy now CTA', async () => { + await COMM.buyNowCta.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.buyNowCta).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.promo}`)); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.CO}`)); + await expect(COMM.buyNowCta).toHaveAttribute('href', new RegExp(`${data.lang}`)); + }); + + await test.step('Validate Free Trial CTA', async () => { + await COMM.freeTrialCta.waitFor({ state: 'visible', timeout: 10000 }); + await expect(COMM.freeTrialCta).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.promo}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.CO}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.lang}`)); + await expect(COMM.freeTrialCta).toHaveAttribute('href', new RegExp(`${data.workflow}`)); + }); + + await test.step('Validate regular price display', async () => { + await COMM.price.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.price.innerText()).toContain('£'); + expect(await COMM.price.innerText()).toContain('/yr'); + expect(await COMM.price.locator('.price-recurrence').first().innerText()).not.toBe(''); + expect(await COMM.price.locator('.price-unit-type').first().innerText()).toBe(''); + expect(await COMM.price.locator('.price-tax-inclusivity').first().innerText()).toBe(''); + await expect(COMM.price).toHaveAttribute('data-promotion-code', data.promo); + await expect(COMM.price).toHaveAttribute('data-display-old-price', 'true'); + await COMM.price.locator('.price').first().waitFor({ state: 'visible', timeout: 10000 }); + await COMM.price.locator('.price-strikethrough').waitFor({ state: 'visible', timeout: 10000 }); + }); + + await test.step('Validate optical price display', async () => { + await COMM.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceOptical.innerText()).toContain('£'); + expect(await COMM.priceOptical.innerText()).toContain('/mo'); + expect(await COMM.priceOptical.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceOptical.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.priceOptical.locator('.price-tax-inclusivity').innerText()).toBe(''); + await expect(COMM.priceOptical).toHaveAttribute('data-promotion-code', data.promo); + }); + + await test.step('Validate strikethrough price display', async () => { + await COMM.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + expect(await COMM.priceStrikethrough.innerText()).toContain('£'); + expect(await COMM.priceStrikethrough.innerText()).toContain('/yr'); + expect(await COMM.priceStrikethrough.locator('.price-recurrence').innerText()).not.toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-unit-type').innerText()).toBe(''); + expect(await COMM.priceStrikethrough.locator('.price-tax-inclusivity').innerText()).toBe(''); + const priceStyle = await COMM.priceStrikethrough.evaluate( + (e) => window.getComputedStyle(e).getPropertyValue('text-decoration'), + ); + expect(await priceStyle).toContain('line-through'); + await expect(COMM.priceStrikethrough).toHaveAttribute('data-promotion-code', data.promo); + }); + }); +}); diff --git a/nala/features/feds/footer/footer.page.js b/nala/features/feds/footer/footer.page.js new file mode 100644 index 0000000000..cb19320cb9 --- /dev/null +++ b/nala/features/feds/footer/footer.page.js @@ -0,0 +1,77 @@ +/* eslint-disable import/no-import-module-exports */ + +export default class FedsFooter { + constructor(page) { + this.page = page; + + // Container Selectors: + this.footerContainer = page.locator('footer.global-footer'); + this.footerSections = page.locator('footer div.feds-menu-section'); + this.footerColumns = page.locator('footer div.feds-menu-column'); + this.footerHeadings = page.locator('footer div.feds-menu-headline'); + + // Change Region Selectors: + this.changeRegionContainer = page.locator('div.feds-regionPicker-wrapper'); + this.changeRegionButton = page.locator('div.feds-regionPicker-wrapper a.feds-regionPicker'); + this.changeRegionModal = page.locator('div#langnav'); + this.changeRegionDropDown = page.locator('div.region-selector'); + this.changeRegionCloseButton = page.locator('button.dialog-close'); + + // Legal Selectors: + this.legalContainer = page.locator('div.feds-footer-legalWrapper'); + this.legalSections = page.locator('p.feds-footer-privacySection'); + this.legalLinks = page.locator('div.feds-footer-legalWrapper a'); + this.legalCopyright = page.locator('span.feds-footer-copyright'); + this.privacyLink = page.locator('a[href*="privacy.html"]'); + this.termsOfUseLink = page.locator('a[href*="terms.html"]'); + this.cookiePreferencesLink = page.locator('a[href*="#openPrivacy"]'); + this.doNotSellInformationLink = page.locator('a[href*="ca-rights.html"]'); + this.adChoicesLink = page.locator('a[href*="opt-out.html"]'); + this.adChoicesLogo = page.locator('svg.feds-adChoices-icon'); + + // Adobe Socials Selectors: + this.twitterIcon = page.locator('ul.feds-social a[aria-label="twitter"]'); + this.linkedInIcon = page.locator('ul.feds-social a[aria-label="linkedin"]'); + this.facebookIcon = page.locator('ul.feds-social a[aria-label="facebook"]'); + this.instagramIcon = page.locator('ul.feds-social a[aria-label="instagram"]'); + this.socialContainer = page.locator('ul.feds-social'); + this.socialIcons = page.locator('ul.feds-social li'); + + // Featured Products Selectors: + this.featuredProductsContainer = page.locator('div.feds-featuredProducts'); + this.featuredProducts = page.locator('div.feds-featuredProducts a'); + this.downloadAdobeExpress = page.locator('footer a[daa-ll="Adobe_Express"]'); + this.downloadAdobePhotoshop = page.locator('footer a[daa-ll="Photoshop"]'); + this.downloadAdobeIllustrator = page.locator('footer a[daa-ll="Illustrator"]'); + + // Footer Section Selectors: + this.footerCreativeCloud = page.locator(".feds-footer-wrapper a[href*='creativecloud.html']"); + this.footerViewAllProducts = page.locator(".feds-navLink[href*='/products/catalog.html?']"); + this.footerCreativeCloudForBusiness = page.locator(".feds-footer-wrapper [href$='cloud/business.html']").nth(0); + this.footerAcrobatForBusiness = page.locator(".feds-footer-wrapper a[href$='acrobat/business.html']"); + this.footerDiscountsForStudentsAndTeachers = page.locator(".feds-footer-wrapper a[href$='buy/students.html']"); + this.footerDigitalLearningSolutions = page.locator("a[href$='/elearning.html']"); + this.footerAppsforiOS = page.locator("a[href*='id852473028']"); + this.footerAppsforAndroid = page.locator("a[href*='id=com.adobe.cc']"); + this.footerWhatIsExperienceCloud = page.locator('.feds-footer-wrapper a[href*="business"]').nth(4); + this.footerTermsOfUse = page.locator('a[href*="experiencecloudterms"]'); + this.footerDownloadAndInstall = page.locator('.feds-footer-wrapper a[href*="download-install.html"]'); + this.footerGenuineSoftware = page.locator('a[href*="genuine.html"]'); + this.footerAdobeBlog = page.locator('.feds-navLink[href*="blog"]').nth(1); + this.footerAdobeDeveloper = page.locator('a[href*="developer"]'); + this.footerLogInToYourAccount = page.locator('.feds-footer-wrapper a[href*="account.adobe"]').nth(0); + this.footerAbout = page.locator('.feds-footer-wrapper [href*="about-adobe.html"]').nth(0); + this.footerIntegrity = page.locator('a[href*="integrity.html"]'); + this.footerAdobeBlogSecond = page.locator('.feds-navLink[href*="blog"]').nth(0); + this.protectMyPersonalData = page.locator('.feds-footer-legalWrapper a:nth-of-type(4)'); + this.termsOfUseLinkTwo = page.locator('a[href*="terms.html"]').nth(1); + + // Featured Product Selectors: + this.footerAdobeAcrobatReaderlogo = page.locator('a[href$="reader/"]'); + this.footerAdobeExpresslogo = page.locator('a[href$="Z2G1FSYV&mv=other"]:nth-of-type(2)'); + this.footerPhotoshoplogo = page.locator('a[href$="photoshop/free-trial-download.html"]'); + this.footerIllustratorlogo = page.locator('a[href$="illustrator/free-trial-download.html"]'); + } + + // >> FEDS Footer methods declared here << +} diff --git a/nala/features/feds/footer/footer.spec.js b/nala/features/feds/footer/footer.spec.js new file mode 100644 index 0000000000..47db482828 --- /dev/null +++ b/nala/features/feds/footer/footer.spec.js @@ -0,0 +1,26 @@ +module.exports = { + name: 'Footer Block', + features: [ + { + name: '@FEDS-Default-Footer', + path: [ + '/drafts/nala/blocks/footer/feds-default-footer', + ], + tags: '@milo @feds @footer @smoke @regression', + }, + { + name: '@FEDS-Skinny-Footer', + path: [ + '/drafts/nala/blocks/footer/feds-skinny-footer', + ], + tags: '@milo @feds @footer @smoke @regression', + }, + { + name: '@FEDS-Privacy-Footer', + path: [ + '/drafts/nala/blocks/footer/feds-privacy-footer', + ], + tags: '@milo @feds @footer @smoke @regression', + }, + ], +}; diff --git a/nala/features/feds/footer/footer.test.js b/nala/features/feds/footer/footer.test.js new file mode 100644 index 0000000000..ae67478fc3 --- /dev/null +++ b/nala/features/feds/footer/footer.test.js @@ -0,0 +1,164 @@ +/* eslint-disable no-await-in-loop, import/extensions */ +import { expect, test } from '@playwright/test'; +import { features } from './footer.spec.js'; +import FedsFooter from './footer.page.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Footer Block Test Suite', () => { + // FEDS Default Footer Checks: + test(`${features[0].name}, ${features[0].tags}`, async ({ page, baseURL }) => { + const Footer = new FedsFooter(page); + console.info(`[FEDSInfo] Checking page: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('Navigate to FEDS Default Footer page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('Check FEDS Default Footer critical elements', async () => { + // Scroll FEDS Footer into viewport: + await Footer.legalContainer.scrollIntoViewIfNeeded(); + // Wait for FEDS Footer to be visible: + await Footer.footerContainer.waitFor({ state: 'visible', timeout: 5000 }); + // Check FEDS Footer critical elements: + await expect(Footer.legalContainer).toBeVisible(); + await expect(Footer.socialContainer).toBeVisible(); + await expect(Footer.footerContainer).toBeVisible(); + await expect(Footer.changeRegionContainer).toBeVisible(); + // !Note: Footer featuredProducts not appearing in NALA. Possible BUG! + // await expect(Footer.featuredProductsContainer).toBeVisible(); + await expect(Footer.footerColumns).toHaveCount(5); + + // updated the footer section and heading content as per consuming sites + // milo=6, cc=9 and so on + await expect([4, 6, 9].includes(await Footer.footerSections.count())).toBeTruthy(); + await expect([4, 6, 9].includes(await Footer.footerHeadings.count())).toBeTruthy(); + + await expect(Footer.socialIcons).toHaveCount(4); + await expect(Footer.legalLinks).toHaveCount(5); + }); + + await test.step('Check ChangeRegion functionality', async () => { + await Footer.changeRegionButton.click(); + await expect(Footer.changeRegionModal).toBeVisible(); + await Footer.changeRegionCloseButton.click(); + await expect(Footer.changeRegionModal).not.toBeVisible(); + }); + }); + + // FEDS Skinny Footer Checks: + test(`${features[1].name}, ${features[1].tags}`, async ({ page, baseURL }) => { + const Footer = new FedsFooter(page); + console.info(`[FEDSInfo] Checking page: ${baseURL}${features[1].path}${miloLibs}`); + + await test.step('Navigate to FEDS Skinny Footer page', async () => { + await page.goto(`${baseURL}${features[1].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}${miloLibs}`); + }); + + await test.step('Check FEDS Skinny Footer critical elements', async () => { + // Scroll FEDS Footer into viewport: + await Footer.legalContainer.scrollIntoViewIfNeeded(); + // Wait for FEDS Footer to be visible: + await Footer.footerContainer.waitFor({ state: 'visible', timeout: 5000 }); + // Check FEDS Footer critical elements: + await expect(Footer.legalContainer).toBeVisible(); + await expect(Footer.socialContainer).toBeVisible(); + await expect(Footer.footerContainer).toBeVisible(); + await expect(Footer.changeRegionContainer).toBeVisible(); + + // await expect(Footer.featuredProducts).toHaveCount(0); + // updated the featuredProducts count as per consuming sites + // milo=0, cc=4 and so on + expect([0, 4].includes(await Footer.featuredProducts.count())).toBeTruthy(); + + const featuredProductsCount = await Footer.featuredProducts.count(); + + if (featuredProductsCount === 0) { + await expect(Footer.featuredProductsContainer).not.toBeVisible(); + } else { + await expect(Footer.featuredProductsContainer).toBeVisible(); + } + + await expect(Footer.legalLinks).toHaveCount(5); + await expect(Footer.socialIcons).toHaveCount(4); + + // await expect(Footer.footerColumns).toHaveCount(0); + // await expect(Footer.footerSections).toHaveCount(0); + // await expect(Footer.footerHeadings).toHaveCount(0); + + const footerSectionsCount = await Footer.featuredProducts.count(); + + if (footerSectionsCount === 0) { + await expect(Footer.footerColumns).not.toBeVisible(); + await expect(Footer.footerSections).not.toBeVisible(); + await expect(Footer.footerHeadings).not.toBeVisible(); + } else { + expect([0, 5].includes(await Footer.footerColumns.count())).toBeTruthy(); + expect([4, 6, 9].includes(await Footer.footerSections.count())).toBeTruthy(); + expect([4, 6, 9].includes(await Footer.footerHeadings.count())).toBeTruthy(); + } + }); + + await test.step('Check ChangeRegion functionality', async () => { + await Footer.changeRegionButton.click(); + await expect(Footer.changeRegionModal).toBeVisible(); + await Footer.changeRegionCloseButton.click(); + await expect(Footer.changeRegionModal).not.toBeVisible(); + }); + }); + + // FEDS Privacy Footer Checks: + test(`${features[2].name}, ${features[2].tags}`, async ({ page, baseURL }) => { + const Footer = new FedsFooter(page); + console.info(`[FEDSInfo] Checking page: ${baseURL}${features[2].path}${miloLibs}`); + + await test.step('Navigate to FEDS Privacy Footer page', async () => { + await page.goto(`${baseURL}${features[2].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}${miloLibs}`); + }); + + await test.step('Check FEDS Privacy Footer critical elements', async () => { + // Scroll FEDS Footer into viewport: + await Footer.legalContainer.scrollIntoViewIfNeeded(); + // Wait for FEDS Footer to be visible: + await Footer.footerContainer.waitFor({ state: 'visible', timeout: 5000 }); + // Check FEDS Footer critical elements: + await expect(Footer.legalContainer).toBeVisible(); + await expect(Footer.socialContainer).toBeVisible(); + await expect(Footer.footerContainer).toBeVisible(); + await expect(Footer.changeRegionContainer).toBeVisible(); + await expect(Footer.featuredProductsContainer).toBeVisible(); + + await expect(Footer.footerColumns).toHaveCount(5); + + // await expect(Footer.footerSections).toHaveCount(9) + // await expect(Footer.footerHeadings).toHaveCount(9) + // await expect(Footer.featuredProducts).toHaveCount(3); + // await expect(Footer.legalSections).toHaveCount(2); + await expect(Footer.socialIcons).toHaveCount(4); + await expect(Footer.legalLinks).toHaveCount(5); + + // updated the footer section and heading content equal or greater + // than 6, to pass tests on cc pages. + expect([4, 6, 9].includes(await Footer.footerSections.count())).toBeTruthy(); + expect([4, 6, 9].includes(await Footer.footerHeadings.count())).toBeTruthy(); + expect([3, 4].includes(await Footer.featuredProducts.count())).toBeTruthy(); + expect([1, 2].includes(await Footer.legalSections.count())).toBeTruthy(); + expect([4].includes(await Footer.socialIcons.count())).toBeTruthy(); + expect([5].includes(await Footer.legalLinks.count())).toBeTruthy(); + }); + + await test.step('Check ChangeRegion functionality', async () => { + await Footer.changeRegionButton.click(); + await expect(Footer.changeRegionDropDown).toBeVisible(); + await expect(Footer.changeRegionModal).not.toBeVisible(); + await Footer.changeRegionButton.click(); + await expect(Footer.changeRegionDropDown).not.toBeVisible(); + }); + }); +}); diff --git a/nala/features/feds/header/header.page.js b/nala/features/feds/header/header.page.js new file mode 100644 index 0000000000..df43a70215 --- /dev/null +++ b/nala/features/feds/header/header.page.js @@ -0,0 +1,110 @@ +/* eslint-disable import/no-import-module-exports */ +import { expect } from '@playwright/test'; + +export default class FedsHeader { + constructor(page) { + this.page = page; + + // GNAV selectors: + this.gnavLogo = page.locator('a.gnav-logo'); + this.headerContainer = page.locator('header.global-navigation'); + this.mainNavLogo = page.locator('a.feds-brand, a.gnav-brand'); + this.mainNavContainer = page.locator('nav.feds-topnav, .gnav-wrapper'); + this.megaMenuToggle = page.locator('button.feds-navLink.feds-navLink--hoverCaret, .section-menu').first(); + this.megaMenuContainer = page.locator('section.feds-navItem--megaMenu div.feds-popup, .section-menu .gnav-menu-container'); + this.megaMenuColumn = page.locator('section.feds-navItem--megaMenu div.feds-menu-section'); + + // GNAV action selectors: + this.signInButton = page.locator('button.feds-signIn').first(); + this.searchIcon = page.locator('button.feds-search-trigger, button.gnav-search-button'); + this.searchInput = page.locator('input.feds-search-input, input.gnav-search-input'); + this.closeSearch = page.locator('span.feds-search-close, button.gnav-search-button[daa-lh="header|Close"]'); + this.searchResults = page.locator('#feds-search-results, .gnav-search-results'); + this.advancedSearchLink = page.locator('#feds-search-results li a, .gnav-search-results li a'); + + this.profileIcon = page.locator('button.feds-profile-button'); + this.profileModal = page.locator('div#feds-profile-menu'); + this.profileName = page.locator('p.feds-profile-name'); + this.profileEmail = page.locator('p.feds-profile-email'); + this.profileAccountLink = page.locator('p.feds-profile-account'); + this.profileDetails = page.locator('div.feds-profile-details'); + this.profileSignOut = page.locator('a.feds-profile-action'); + + // GNAV breadcrumb selectors: + this.breadcrumbList = page.locator('nav.feds-breadcrumbs ul'); + this.breadcrumbElems = page.locator('nav.feds-breadcrumbs li'); + this.breadcrumbContainer = page.locator('nav.feds-breadcrumbs'); + + // Promo-bar selectors: + this.promoBarContainer = page.locator('div.aside.promobar'); + this.promoBarBackground = this.promoBarContainer.locator('div.background'); + this.promoBarForeground = this.promoBarContainer.locator('div.foreground'); + this.promoBarContent = this.promoBarContainer.locator('div.desktop-up'); + this.promoBarText = this.promoBarContainer.locator('div.desktop-up p.content-area'); + this.promoBarBtn = this.promoBarContainer.locator('div.desktop-up p.action-area a'); + this.promoBarMobileContent = this.promoBarContainer.locator('div.mobile-up'); + this.promoBarMobileText = this.promoBarContainer.locator('div.mobile-up p.content-area'); + this.promoBarMobileBtn = this.promoBarContainer.locator('div.mobile-up p.action-area a'); + this.promoBarTabletContent = this.promoBarContainer.locator('div.tablet-up'); + this.promoBarTabletText = this.promoBarContainer.locator('div.tablet-up p.content-area'); + this.promoBarTabletBtn = this.promoBarContainer.locator('div.tablet-up p.action-area a'); + } + + /** + * Opens the User Profile via click on GNAV profile icon. + * !Note: Only use after user was logged in! + * @param {none} + * @return {Promise} PlayWright promise + */ + async openUserProfile() { + await this.profileIcon.waitFor({ state: 'visible', timeout: 10000 }); + await this.profileIcon.click(); + await expect(this.profileModal).toBeVisible(); + } + + /** + * Closes the User Profile via click on GNAV profile icon. + * !Note: Only use after user was logged in! + * @param {none} + * @return {Promise} PlayWright promise + */ + async closeUserProfile() { + await this.profileIcon.waitFor({ state: 'visible', timeout: 10000 }); + await this.profileIcon.click(); + await expect(this.profileModal).not.toBeVisible(); + } + + /** + * Checks the elements of the User Profile component. + * @param {none} + * @return {Promise} PlayWright promise + */ + async checkUserProfile() { + await expect(this.profileName).toBeVisible(); + await expect(this.profileEmail).toBeVisible(); + await expect(this.profileSignOut).toBeVisible(); + await expect(this.profileAccountLink).toBeVisible(); + } + + /** + * Opens the search bar via click fron GNAV search icon. + * @param {none} + * @return {Promise} PlayWright promise + */ + async openSearchBar() { + await this.searchIcon.waitFor({ state: 'visible', timeout: 10000 }); + await this.searchIcon.click(); + await expect(this.searchInput).toBeVisible(); + } + + /** + * Closes the search bar via click fron GNAV search icon. + * @param {none} + * @return {Promise} PlayWright promise + */ + async closeSearchBar() { + await this.closeSearch.waitFor({ state: 'visible', timeout: 10000 }); + await this.closeSearch.click(); + await expect(this.searchInput).not.toBeVisible(); + } +} diff --git a/nala/features/feds/header/header.spec.js b/nala/features/feds/header/header.spec.js new file mode 100644 index 0000000000..d0e754d39f --- /dev/null +++ b/nala/features/feds/header/header.spec.js @@ -0,0 +1,12 @@ +module.exports = { + name: 'Header Block', + features: [ + { + name: '@FEDS-Header-Checks', + path: [ + '/drafts/nala/blocks/header/feds-header-page', + ], + tags: '@milo @feds @header @nopr @smoke @regression', + }, + ], +}; diff --git a/nala/features/feds/header/header.test.js b/nala/features/feds/header/header.test.js new file mode 100644 index 0000000000..9969a551fc --- /dev/null +++ b/nala/features/feds/header/header.test.js @@ -0,0 +1,52 @@ +/* eslint-disable no-await-in-loop, import/extensions */ +import { expect, test } from '@playwright/test'; +import { features } from './header.spec.js'; +import FedsHeader from './header.page.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +test.describe('Header Block Test Suite', () => { + // FEDS Default Header Checks: + test(`${features[0].name}, ${features[0].tags}`, async ({ page, baseURL }) => { + const Header = new FedsHeader(page); + console.info(`[FEDSInfo] Checking page: ${baseURL}${features[0].path}${miloLibs}`); + + await test.step('Navigate to FEDS HEADER page', async () => { + await page.goto(`${baseURL}${features[0].path}${miloLibs}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}${miloLibs}`); + }); + + await test.step('Check HEADER block content', async () => { + // Wait for FEDS GNAV to be visible: + await Header.mainNavContainer.waitFor({ state: 'visible', timeout: 5000 }); + // Check HEADER block content: + await expect(Header.mainNavLogo).toBeVisible(); + + // skipping the step for PR branch runs + // working on better workaround soloution + // await expect(Header.signInButton).toBeVisible(); + }); + + await test.step('Check HEADER search component', async () => { + // adding the below check to accommodate testing on consuming sites + const isSearchIconVisible = await Header.searchIcon.isVisible(); + if (isSearchIconVisible) { + await test.step('Check HEADER search component', async () => { + await Header.openSearchBar(); + await Header.closeSearchBar(); + }); + } else { + console.info('Search icon is not visible, skipping the search component test.'); + } + }); + + await test.step('Check HEADER block mega menu component', async () => { + await Header.megaMenuToggle.waitFor({ state: 'visible', timeout: 5000 }); + await Header.megaMenuToggle.click(); + await expect(Header.megaMenuContainer).toBeVisible(); + await Header.megaMenuToggle.click(); + await expect(Header.megaMenuContainer).not.toBeVisible(); + }); + }); +}); diff --git a/nala/features/feds/login/login.page.js b/nala/features/feds/login/login.page.js new file mode 100644 index 0000000000..e2baaee3e4 --- /dev/null +++ b/nala/features/feds/login/login.page.js @@ -0,0 +1,118 @@ +// eslint-disable-next-line import/no-import-module-exports +import { expect } from '@playwright/test'; + +export default class FedsLogin { + constructor(page) { + this.page = page; + + this.loginButton = page.locator('button#sign_in'); + this.loginForm = page.locator('form#adobeid_signin'); + this.emailField = page.locator('input#adobeid_username'); + this.passwordField = page.locator('input#adobeid_password'); + + this.loggedInState = page.locator('img.feds-profile-img'); + this.loginWithEnterpriseId = page.locator('a#enterprise_signin_link'); + this.forgotPasswordLink = page.locator('a#adobeid_trouble_signing_in'); + + this.loginWithFacebook = page.locator('a.mod-facebook'); + this.loginWithGoogle = page.locator('a.mod-google'); + this.loginWithApple = page.locator('a.mod-apple'); + + this.appEmailForm = page.locator('form#EmailForm'); + this.appPasswordForm = page.locator('form#PasswordForm'); + this.appEmailField = page.locator('input#EmailPage-EmailField'); + this.appPasswordField = page.locator('input#PasswordPage-PasswordField'); + this.appVisibilityToggle = page.locator('button.PasswordField-VisibilityToggle'); + this.appPasswordContinue = page.locator('button[data-id^="EmailPage"]'); + this.appLoginContinue = page.locator('button[data-id^="PasswordPage"]'); + this.personalAccountLogo = page.locator('img[alt="Personal Account"]'); + this.selectAccountForm = page.locator('div[data-id="Profile"]'); + + this.appEmailFieldSelector = page.locator('input#EmailPage-EmailField'); + this.appPasswordFieldSelector = page.locator('input#PasswordPage-PasswordField'); + this.codePadChallenge = page.locator('div[data-id="ChallengeCodePage"]'); + } + + /** + * Login on the IMS APP login form with email & password. + * @param {string} email + * @param {string} password + * @return {Promise} PlayWright promise + */ + async loginOnAppForm(email, password) { + console.info('[EuroLogin] APP login form identified!'); + // Check EMAIL & PASSWWORD status: + expect(process.env.IMS_EMAIL, 'ERROR: No environment variable found for IMS_EMAIL').toBeTruthy(); + expect(process.env.IMS_PASS, 'ERROR: No environment variable found for IMS_PASS.').toBeTruthy(); + console.info(`[EuroLogin] Logging in with '${email}' account ...`); + // Wait for page to load & stabilize: + await this.page.waitForLoadState('domcontentloaded'); + // Wait for the SUSI login form to load: + await this.appEmailForm.waitFor({ state: 'visible', timeout: 15000 }); + // Insert account email & click 'Continue': + await this.appEmailField.waitFor({ state: 'visible', timeout: 15000 }); + await this.appEmailField.fill(email); + await this.appPasswordContinue.waitFor({ state: 'visible', timeout: 15000 }); + await expect(this.appPasswordContinue).toHaveText('Continue'); + await this.appPasswordContinue.click(); + // Wait for page to load & stabilize: + await this.page.waitForTimeout(5000); + // Insert account password & click 'Continue': + // await this.appPasswordForm.waitFor({state: 'visible', timeout: 15000}); + await this.appPasswordField.waitFor({ state: 'visible', timeout: 15000 }); + await this.appPasswordField.fill(password); + await this.appLoginContinue.waitFor({ state: 'visible', timeout: 15000 }); + await expect(this.appLoginContinue).toHaveText('Continue'); + await this.appLoginContinue.click(); + // Check if login process was successful: + await this.loggedInState.waitFor({ state: 'visible', timeout: 20000 }); + console.info(`[EuroLogin] Successfully logged-in as '${email}' (via APP login form).`); + } + + /** + * Login on the IMS SUSI login form with email & password. + * @param {string} email + * @param {string} password + * @return {Promise} PlayWright promise + */ + async loginOnSusiForm(email, password) { + console.info('[EuroLogin] SUSI login form identified!'); + // Check EMAIL & PASSWWORD status: + expect(process.env.IMS_EMAIL, 'ERROR: No environment variable found for IMS_EMAIL').toBeTruthy(); + expect(process.env.IMS_PASS, 'ERROR: No environment variable found for IMS_PASS.').toBeTruthy(); + console.info(`[EuroLogin] Logging in with '${email}' account ...`); + // Wait for page to load & stabilize: + await this.page.waitForLoadState('networkidle'); + // Wait for the SUSI login form to load: + await this.loginForm.waitFor({ state: 'visible', timeout: 15000 }); + await this.emailField.fill(email); + // !Note: Email field has short client-side validation (load). + // Password field is not interactable during that time. + await this.page.keyboard.press('Tab'); + // Wait for page to load & stabilize: + await this.page.waitForLoadState('domcontentloaded'); + // Set password & click 'Continue': + await this.appPasswordForm.waitFor({ state: 'visible', timeout: 15000 }); + await this.passwordField.waitFor({ state: 'visible', timeout: 15000 }); + await this.passwordField.fill(password); + // Complete the login flow: + await this.loginButton.waitFor({ state: 'visible', timeout: 15000 }); + await this.loginButton.click(); + // Check if login process was successful: + await this.loggedInState.waitFor({ state: 'visible', timeout: 20000 }); + console.info(`[EuroLogin] Successfully logged-in as '${email}' (via SUSI login form).`); + } + + /** + * Toggles the visibility of the IMS password field. + * @param {string} password + * @return {Promise} PlayWright promise + */ + async TogglePasswordVisibility(password) { + await this.appVisibilityToggle.waitFor({ state: 'visible', timeout: 15000 }); + await this.appVisibilityToggle.click(); + await expect(this.appPasswordField).toContain(password); + await this.appVisibilityToggle.click(); + await this.appVisibilityToggle.waitFor({ state: 'visible', timeout: 15000 }); + } +} diff --git a/nala/features/georouting/georouting.page.js b/nala/features/georouting/georouting.page.js index 8388b6bd71..0df953c8eb 100644 --- a/nala/features/georouting/georouting.page.js +++ b/nala/features/georouting/georouting.page.js @@ -66,7 +66,7 @@ export default class Georouting { await expect(this.geoModal.locator(`//a[text()="${data[tab].button}"]`)).toBeVisible({ timeout: 1000 }); await expect(this.geoModal.locator(`//a[text()="${data[tab].link}"]`).nth(index)).toBeVisible({ timeout: 1000 }); await expect(this.geoModal.locator(`//img[@alt="${data[tab].flag}"]`)).toBeVisible({ timeout: 1000 }); - index = +1; + index += 1; } return true; diff --git a/nala/features/georouting/georouting.test.js b/nala/features/georouting/georouting.test.js index 53b9cd1c61..b3524815ed 100644 --- a/nala/features/georouting/georouting.test.js +++ b/nala/features/georouting/georouting.test.js @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies, max-len, no-console */ import { expect, test } from '@playwright/test'; import { features } from './georouting.spec.js'; import Georouting from './georouting.page.js'; diff --git a/nala/features/imslogin/imslogin.page.js b/nala/features/imslogin/imslogin.page.js new file mode 100644 index 0000000000..c5a4f2d9a0 --- /dev/null +++ b/nala/features/imslogin/imslogin.page.js @@ -0,0 +1,23 @@ +module.exports = { + '@gnav-signin': '.gnav-signin', + '@gnav-profile-button': '.gnav-profile-button', + '@gnav-signout': 'text=Sign Out', + '@gnav-viewaccount': '.gnav-profile-header', + '@gnav-manageTeam': 'text=Manage Team', + '@email': '#EmailPage-EmailField', + '@password': '#PasswordPage-PasswordField', + '@email-continue-btn': '[data-id=EmailPage-ContinueButton]', + '@verify-continue-btn': '[data-id=Page-PrimaryButton]', + '@password-reset': 'text=Reset your password', + '@password-continue-btn': '[data-id=PasswordPage-ContinueButton]', + '@apple-signin': '[data-id=EmailPage-AppleSignInButton]', + '@google-signin': '[data-id=EmailPage-GoogleSignInButton]', + '@facebook-signin': '[data-id=EmailPage-FacebookSignInButton]', + '@page-heading': '.spectrum-Heading1', + '@gnav-ec-signin': '[daa-ll=Experience_Cloud-1]', + '@gnav-comm-signin': '[daa-ll=Commerce__Magento-2]', + '@gnav-multi-signin': '[daa-ll=Adobe_Account-5]', + '@gnav-app-launcher': '.gnav-applications-button', + '@cc-app-launcher': '#navmenu-apps >> ul >> li:nth-child(1) >> a', + '@app-launcher-list': '.apps >> li', +}; diff --git a/nala/features/osttools/ost.page.js b/nala/features/osttools/ost.page.js new file mode 100644 index 0000000000..68d7586370 --- /dev/null +++ b/nala/features/osttools/ost.page.js @@ -0,0 +1,31 @@ +export default class OSTPage { + constructor(page) { + this.page = page; + + this.searchField = page.locator('//input[contains(@data-testid,"search")]'); + this.productList = page.locator('//span[contains(@class,"productName")]'); + this.planType = page.locator( + '//button/span[contains(@class, "spectrum-Dropdown-label") and (.//ancestor::div/span[contains(text(),"plan type")])]', + ); + this.offerType = page.locator( + '//button/span[contains(@class, "spectrum-Dropdown-label") and (.//ancestor::div/span[contains(text(),"offer type")])]', + ); + this.nextButton = page.locator('//button[contains(@data-testid, "nextButton")]/span'); + this.price = page.locator('//div[@data-type="price"]/span'); + this.priceOptical = page.locator('//div[contains(@data-type, "priceOptical")]/span'); + this.priceStrikethrough = page.locator('//div[contains(@data-type, "priceStrikethrough")]/span'); + this.termCheckbox = page.locator('//input[@value="displayRecurrence"]'); + this.unitCheckbox = page.locator('//input[@value="displayPerUnit"]'); + this.taxlabelCheckbox = page.locator('//input[@value="displayTax"]'); + this.taxInlcusivityCheckbox = page.locator('//input[@value="forceTaxExclusive"]'); + this.oldPrice = page.locator('//input[@value="displayOldPrice"]'); + this.priceUse = page.locator('button:near(h4:text("Price"))').first(); + this.priceOpticalUse = page.locator('button:near(:text("Optical price"))').first(); + this.priceStrikethroughUse = page.locator('button:near(:text("Strikethrough price"))').first(); + this.checkoutTab = page.locator('//div[@data-key="checkout"]'); + this.checkoutLink = page.locator('//a[@data-type="checkoutUrl"]'); + this.workflowMenu = page.locator('button:near(label:text("Workflow"))').first(); + this.promoField = page.locator('//input[contains(@class, "spectrum-Textfield-input")]'); + this.cancelPromo = page.locator('button:right-of(span:text("Promotion:"))').first(); + } +} diff --git a/nala/features/osttools/ost.spec.js b/nala/features/osttools/ost.spec.js new file mode 100644 index 0000000000..ec5e68f9c1 --- /dev/null +++ b/nala/features/osttools/ost.spec.js @@ -0,0 +1,128 @@ +module.exports = { + name: 'Offer Selector Tool', + features: [ + { + tcid: '0', + name: '@OST-Search-OfferID', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + productName: 'Photoshop', + productNameShort: 'phsp', + planType: 'PUF', + offerType: 'TRIAL', + price: 'US$263.88', + opticalPrice: 'US$21.99', + term: '/yr', + opticalTerm: '/mo', + unit: 'per license', + taxLabel: 'excl. tax', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '1', + name: '@OST-Offer-Entitlements', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + planType: 'PUF', + offerType: 'TRIAL', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + + }, + { + tcid: '2', + name: '@OST-Offer-Price', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + price: 'US$263.88', + opticalPrice: 'US$21.99', + term: '/yr', + opticalTerm: '/mo', + unit: 'per license', + taxLabel: 'excl. tax', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '3', + name: '@OST-Term', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + term: '/yr', + opticalTerm: '/mo', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '4', + name: '@OST-Unit', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + unit: 'per license', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '5', + name: '@OST-TaxLabel', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + taxLabel: 'excl. tax', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '6', + name: '@OST-TaxInclusivity', + path: '/tools/ost', + data: { offerID: '0ADF92A6C8514F2800BE9E87DB641D2A' }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '7', + name: '@OST-Price-Promo', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + promo: 'testpromo', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '8', + name: '@OST-Checkout-Link', + path: '/tools/ost', + data: { + offerID: '0ADF92A6C8514F2800BE9E87DB641D2A', + workflowStep_1: 'email', + workflowStep_2: 'recommendation', + promo: 'testpromo', + }, + browserParams: '?token=', + tags: '@ost @commerce @regression @nopr', + }, + { + tcid: '9', + name: '@OST-OldPrice', + path: '/tools/ost', + data: { offerID: '0ADF92A6C8514F2800BE9E87DB641D2A' }, + browserParams: '?token=', + tags: '@ost @commerce @f1 S@regression @nopr', + }, + ], +}; diff --git a/nala/features/osttools/ost.test.js b/nala/features/osttools/ost.test.js new file mode 100644 index 0000000000..dc87a39aac --- /dev/null +++ b/nala/features/osttools/ost.test.js @@ -0,0 +1,695 @@ +import { expect, test } from '@playwright/test'; +import { features } from './ost.spec.js'; +import OSTPage from './ost.page.js'; +import ims from '../../libs/imslogin.js'; + +let authToken; +let adobeIMS; +let OST; + +test.beforeAll(async ({ browser }) => { + test.slow(); + // Skip tests on github actions and PRs, run only on Jenkins + if (process.env.GITHUB_ACTIONS) test.skip(); + + const page = await browser.newPage(); + await page.goto('https://www.adobe.com/creativecloud/plans.html?mboxDisable=1&adobe_authoring_enabled=true'); + const signinBtn = page.locator('#universal-nav button.profile-comp').first(); + await expect(signinBtn).toBeVisible(); + await signinBtn.click(); + await page.waitForURL('**/auth.services.adobe.com/en_US/index.html**/'); + features[0].url = 'https://www.adobe.com/creativecloud/plans.html?mboxDisable=1&adobe_authoring_enabled=true'; + await ims.fillOutSignInForm(features[0], page); + await expect(async () => { + const response = await page.request.get(features[0].url); + expect(response.status()).toBe(200); + }).toPass(); + authToken = await page.evaluate(() => adobeIMS.getAccessToken().token); +}); + +test.beforeEach(async ({ page }) => { + OST = new OSTPage(page); +}); + +test.describe('OST page test suite', () => { + // Verify OST search by offer ID + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}`); + + const testPage = `${baseURL}${features[0].path}${features[0].browserParams}${authToken}`; + const { data } = features[0]; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + await page.waitForTimeout(2000); + }); + + await test.step('Validate search results', async () => { + await OST.productList.first().waitFor({ state: 'visible', timeout: 10000 }); + const skus = OST.productList; + expect(await skus.count()).toBeLessThanOrEqual(2); + expect(await skus.nth(0).innerText()).toContain(data.productName); + expect(await skus.nth(1).innerText()).toContain(data.productNameShort); + }); + }); + + // Verify OST offer entitlements + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}`); + + const testPage = `${baseURL}${features[1].path}${features[1].browserParams}${authToken}`; + const { data } = features[1]; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + await page.waitForTimeout(2000); + }); + + await test.step('Validate entitlements', async () => { + await OST.planType.waitFor({ state: 'visible', timeout: 10000 }); + await OST.offerType.waitFor({ state: 'visible', timeout: 10000 }); + expect(await OST.planType.innerText()).toContain(data.planType); + expect(await OST.offerType.innerText()).toContain(data.offerType); + }); + }); + + // Verify OST offer price options display + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}`); + + const testPage = `${baseURL}${features[2].path}${features[2].browserParams}${authToken}`; + const { data } = features[2]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate Offer regular price option', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + + expect(await OST.price.innerText()).toContain(data.price); + expect(await OST.price.innerText()).toContain(data.term); + expect(await OST.price.innerText()).toContain(data.unit); + expect(await OST.price.innerText()).not.toContain(data.taxLabel); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('milo.adobe.com/tools/ost'); + expect(await clipboardText).toContain('type=price'); + }); + + await test.step('Validate Offer optical price option', async () => { + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + + expect(await OST.priceOptical.innerText()).toContain(data.opticalPrice); + expect(await OST.priceOptical.innerText()).toContain(data.opticalTerm); + expect(await OST.priceOptical.innerText()).toContain(data.unit); + expect(await OST.priceOptical.innerText()).not.toContain(data.taxLabel); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('milo.adobe.com/tools/ost'); + expect(await clipboardText).toContain('type=priceOptical'); + }); + + await test.step('Validate Offer strikethrough price option', async () => { + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + + expect(await OST.priceStrikethrough.innerText()).toContain(data.price); + expect(await OST.priceStrikethrough.innerText()).toContain(data.term); + expect(await OST.priceStrikethrough.innerText()).toContain(data.unit); + expect(await OST.priceStrikethrough.innerText()).not.toContain(data.taxLabel); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('milo.adobe.com/tools/ost'); + expect(await clipboardText).toContain('type=priceStrikethrough'); + }); + }); + + // Verify OST enebalement for price term text + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}`); + + const testPage = `${baseURL}${features[3].path}${features[3].browserParams}${authToken}`; + const { data } = features[3]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate term enablement', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + + expect(await OST.price.innerText()).toContain(data.term); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=price'); + expect(await clipboardText).not.toContain('term='); + + expect(await OST.priceOptical.innerText()).toContain(data.opticalTerm); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceOptical'); + expect(await clipboardText).not.toContain('term='); + + expect(await OST.priceStrikethrough.innerText()).toContain(data.term); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceStrikethrough'); + expect(await clipboardText).not.toContain('term='); + + // Check term checkbox + await OST.termCheckbox.click(); + + expect(await OST.price.innerText()).not.toContain(data.term); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('term=false'); + + expect(await OST.priceOptical.innerText()).not.toContain(data.opticalTerm); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('term=false'); + + expect(await OST.priceStrikethrough.innerText()).not.toContain(data.term); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('term=false'); + + // Uncheck term checkbox + await OST.termCheckbox.click(); + + expect(await OST.price.innerText()).toContain(data.term); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('term='); + + expect(await OST.priceOptical.innerText()).toContain(data.opticalTerm); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('term='); + + expect(await OST.priceStrikethrough.innerText()).toContain(data.term); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('term='); + }); + }); + + // Verify OST enebalement for price unit text + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[4].path}`); + + const testPage = `${baseURL}${features[4].path}${features[4].browserParams}${authToken}`; + const { data } = features[4]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate unit enablement', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + + expect(await OST.price.innerText()).toContain(data.unit); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=price'); + expect(await clipboardText).not.toContain('seat='); + + expect(await OST.priceOptical.innerText()).toContain(data.unit); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceOptical'); + expect(await clipboardText).not.toContain('seat='); + + expect(await OST.priceStrikethrough.innerText()).toContain(data.unit); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceStrikethrough'); + expect(await clipboardText).not.toContain('seat='); + + // Check unit checkbox + await OST.unitCheckbox.click(); + + expect(await OST.price.innerText()).not.toContain(data.unit); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('seat=false'); + + expect(await OST.priceOptical.innerText()).not.toContain(data.unit); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('seat=false'); + + expect(await OST.priceStrikethrough.innerText()).not.toContain(data.unit); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('seat=false'); + + // Uncheck unit checkbox + await OST.unitCheckbox.click(); + + expect(await OST.price.innerText()).toContain(data.unit); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('seat='); + + expect(await OST.priceOptical.innerText()).toContain(data.unit); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('seat='); + + expect(await OST.priceStrikethrough.innerText()).toContain(data.unit); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('seat='); + }); + }); + + // Verify OST enebalement for price tax label + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[5].path}`); + + const testPage = `${baseURL}${features[5].path}${features[5].browserParams}${authToken}`; + const { data } = features[5]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate tax label enablement', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + + expect(await OST.price.innerText()).not.toContain(data.taxLabel); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=price'); + expect(await clipboardText).not.toContain('tax='); + + expect(await OST.priceOptical.innerText()).not.toContain(data.taxLabel); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceOptical'); + expect(await clipboardText).not.toContain('tax='); + + expect(await OST.priceStrikethrough.innerText()).not.toContain(data.taxLabel); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceStrikethrough'); + expect(await clipboardText).not.toContain('tax='); + + // Check tax label checkbox + await OST.taxlabelCheckbox.click(); + + expect(await OST.price.innerText()).toContain(data.taxLabel); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('tax=true'); + + expect(await OST.priceOptical.innerText()).toContain(data.taxLabel); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('tax=true'); + + expect(await OST.priceStrikethrough.innerText()).toContain(data.taxLabel); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('tax=true'); + + // Uncheck tax label checkbox + await OST.taxlabelCheckbox.click(); + + expect(await OST.price.innerText()).not.toContain(data.taxLabel); + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('tax='); + + expect(await OST.priceOptical.innerText()).not.toContain(data.taxLabel); + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('tax='); + + expect(await OST.priceStrikethrough.innerText()).not.toContain(data.taxLabel); + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('tax='); + }); + }); + + // Verify OST enebalement for tax inclusivity in the price + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[6].path}`); + + const testPage = `${baseURL}${features[6].path}${features[6].browserParams}${authToken}`; + const { data } = features[6]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate tax inclusivity enablement', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=price'); + expect(await clipboardText).not.toContain('exclusive='); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceOptical'); + expect(await clipboardText).not.toContain('exclusive='); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceStrikethrough'); + expect(await clipboardText).not.toContain('exclusive='); + + // Check tax label checkbox + await OST.taxInlcusivityCheckbox.click(); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('exclusive=true'); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('exclusive=true'); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('exclusive=true'); + + // Uncheck tax label checkbox + await OST.taxInlcusivityCheckbox.click(); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('exclusive='); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('exclusive='); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('exclusive='); + }); + }); + + // Verify OST offer price promo + test(`${features[7].name},${features[7].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[7].path}`); + + const testPage = `${baseURL}${features[7].path}${features[7].browserParams}${authToken}`; + const { data } = features[7]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate price with promo option', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.promoField.waitFor({ state: 'visible', timeout: 10000 }); + await OST.cancelPromo.waitFor({ state: 'visible', timeout: 10000 }); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('promo='); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('promo='); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('promo='); + + // Add promo + await OST.promoField.fill(data.promo); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain(`promo=${data.promo}`); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain(`promo=${data.promo}`); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain(`promo=${data.promo}`); + + // Cancel promo + await OST.cancelPromo.click(); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('promo='); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('promo='); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('promo='); + }); + }); + + // Verify OST checkout link generation + test(`${features[8].name},${features[8].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[8].path}`); + + const testPage = `${baseURL}${features[7].path}${features[8].browserParams}${authToken}`; + const { data } = features[8]; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Go to Checkout link tab', async () => { + await OST.checkoutTab.waitFor({ state: 'visible', timeout: 10000 }); + await OST.checkoutTab.click(); + }); + + await test.step('Validate Checkout Link', async () => { + await OST.checkoutLink.waitFor({ state: 'visible', timeout: 10000 }); + await OST.promoField.waitFor({ state: 'visible', timeout: 10000 }); + await OST.workflowMenu.waitFor({ state: 'visible', timeout: 10000 }); + + await expect(OST.checkoutLink).toHaveAttribute('href', new RegExp(`${data.offerID}`)); + await expect(OST.checkoutLink).toHaveAttribute('href', new RegExp(`${data.workflowStep_1}`)); + await expect(OST.checkoutLink).not.toHaveAttribute('href', /apc=/); + + // Add promo + await OST.promoField.fill(data.promo); + await expect(OST.checkoutLink).toHaveAttribute('href', new RegExp(`${data.promo}`)); + + // Change Forkflow step + await OST.workflowMenu.click(); + await page.locator(`div[data-key="${data.workflowStep_2}"]`).waitFor({ state: 'visible', timeout: 10000 }); + await page.locator(`div[data-key="${data.workflowStep_2}"]`).click(); + await expect(OST.checkoutLink).toHaveAttribute('href', new RegExp(`${data.workflowStep_2}`)); + }); + }); + + // Verify OST enebalement for old price in the promo price + test(`${features[9].name},${features[9].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[9].path}`); + + const testPage = `${baseURL}${features[9].path}${features[9].browserParams}${authToken}`; + const { data } = features[9]; + + let clipboardText; + + await test.step('Open Offer Selector Tool', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Enter Offer ID in the search field', async () => { + await OST.searchField.fill(data.offerID); + }); + + await test.step('Click Next button in OST', async () => { + await OST.nextButton.waitFor({ state: 'visible', timeout: 10000 }); + await OST.nextButton.click(); + }); + + await test.step('Validate tax inclusivity enablement', async () => { + await OST.price.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOptical.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethrough.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceOpticalUse.waitFor({ state: 'visible', timeout: 10000 }); + await OST.priceStrikethroughUse.waitFor({ state: 'visible', timeout: 10000 }); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=price'); + expect(await clipboardText).not.toContain('old='); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceOptical'); + expect(await clipboardText).not.toContain('old='); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('type=priceStrikethrough'); + expect(await clipboardText).not.toContain('old='); + + // Check tax label checkbox + await OST.oldPrice.click(); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('old=true'); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('old=true'); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).toContain('old=true'); + + // Uncheck tax label checkbox + await OST.oldPrice.click(); + + await OST.priceUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('old='); + + await OST.priceOpticalUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('old='); + + await OST.priceStrikethroughUse.click(); + clipboardText = await page.evaluate('navigator.clipboard.readText()'); + expect(await clipboardText).not.toContain('old='); + }); + }); +}); diff --git a/nala/features/promotions/promotions.page.js b/nala/features/promotions/promotions.page.js new file mode 100644 index 0000000000..484c813e6f --- /dev/null +++ b/nala/features/promotions/promotions.page.js @@ -0,0 +1,25 @@ +export default class CommercePage { + constructor(page) { + this.page = page; + + this.marqueeDefault = page.locator('.marquee #promo-test-page'); + this.marqueeReplace = page.locator('.marquee #marquee-promo-replace'); + this.marqueeFragment = page.locator('.marquee #fragment-marquee'); + this.textBlock = page.locator('.text-block'); + this.textDefault = page.locator('.text #default-text'); + this.textReplace = page.locator('.text #promo-text-replace'); + this.textInsertAfterMarquee = page.locator('.text #marquee-promo-text-insert'); + this.textInsertBeforeText = page.locator('.text #text-promo-text-insert'); + this.textInsertFuture = page.locator('.text #future-promo-text-insert'); + this.textInsertBeforeCommon = page.locator('.text #common-promo'); + this.textInsertBeforeCommonDE = page.locator('.text #german-promo'); + this.textInsertBeforeCommonFR = page.locator('.text #french-promo'); + this.mepMenuOpen = page.locator('.mep-open'); + this.mepPreviewButton = page.locator('//a[contains(text(),"Preview")]'); + this.mepManifestList = page.locator('.mep-manifest-list'); + this.mepInsertDefault = page.locator('//input[contains(@name,"promo-insert") and @value="default"]'); + this.mepInsertAll = page.locator('//input[contains(@name,"promo-insert") and @value="all"]'); + this.mepReplaceDefault = page.locator('//input[contains(@name,"promo-replace") and @value="default"]'); + this.mepReplaceAll = page.locator('//input[contains(@name,"promo-replace") and @value="all"]'); + } +} diff --git a/nala/features/promotions/promotions.spec.js b/nala/features/promotions/promotions.spec.js new file mode 100644 index 0000000000..44a30af081 --- /dev/null +++ b/nala/features/promotions/promotions.spec.js @@ -0,0 +1,163 @@ +module.exports = { + name: 'Promotions', + features: [ + { + tcid: '0', + name: '@Promo-insert', + path: '/drafts/nala/features/promotions/promo-insert', + data: { + textMarquee: 'Promo test page', + textAfterMarquee: 'Marquee promo text insert', + textBeforeText: 'Text promo text insert', + textDefault: 'Default text', + }, + tags: '@promo @commerce @regression', + }, + { + tcid: '1', + name: '@Promo-replace', + path: '/drafts/nala/features/promotions/promo-replace', + data: { + textReplaceMarquee: 'Marquee promo replace', + textReplace: 'Promo text replace', + }, + tags: '@promo @commerce @regression', + }, + { + tcid: '2', + name: '@Promo-remove', + path: '/drafts/nala/features/promotions/promo-remove', + data: { textMarquee: 'Promo test page' }, + tags: '@promo @commerce @regression', + }, + { + tcid: '3', + name: '@Promo-two-manifests', + path: '/drafts/nala/features/promotions/promo-default', + data: { + textReplaceMarquee: 'Marquee promo replace', + textReplace: 'Promo text replace', + textAfterMarquee: 'Marquee promo text insert', + textBeforeText: 'Text promo text insert', + }, + tags: '@promo @commerce @smoke @regression', + }, + { + tcid: '4', + name: '@Promo-replace-fragment', + path: '/drafts/nala/features/promotions/promo-with-fragments', + data: { textReplaceMarquee: 'Marquee promo replace' }, + tags: '@promo @commerce @regression', + }, + { + tcid: '5', + name: '@Promo-future', + path: '/drafts/nala/features/promotions/promo-future', + data: { + mepPath: '/drafts/nala/features/promotions/manifests/promo-insert-future.json--all', + textMarquee: 'Promo test page', + textDefault: 'Default text', + textFuture: 'Future promo text insert', + status: 'Scheduled - inactive', + manifestFile: 'promo-insert-future.json', + }, + tags: '@promo @commerce @regression', + }, + { + tcid: '6', + name: '@Promo-with-personalization', + path: '/drafts/nala/features/promotions/promo-with-personalization', + data: { + textMarquee: 'Promo test page', + textAfterMarquee: 'Marquee promo text insert', + textBeforeText: 'Text promo text insert', + }, + tags: '@promo @commerce @smoke @regression', + }, + { + tcid: '7', + name: '@Promo-with-personalization-and-target', + path: '/drafts/nala/features/promotions/promo-with-personalization-and-target', + data: { + textMarquee: 'Promo test page', + textAfterMarquee: 'Marquee promo text insert', + textBeforeText: 'Text promo text insert', + }, + tags: '@promo @commerce @smoke @regression', + }, + { + tcid: '8', + name: '@Promo-preview', + path: '/drafts/nala/features/promotions/promo-default', + data: { + mepInsertOn: '/drafts/nala/features/promotions/manifests/promo-insert.json--all', + mepReplaceOn: '/drafts/nala/features/promotions/manifests/promo-replace.json--all', + mepInsertOff: '/drafts/nala/features/promotions/manifests/promo-insert.json--default', + mepReplaceOff: '/drafts/nala/features/promotions/manifests/promo-replace.json--default', + textMarquee: 'Promo test page', + textDefault: 'Default text', + textAfterMarquee: 'Marquee promo text insert', + textBeforeText: 'Text promo text insert', + textReplaceMarquee: 'Marquee promo replace', + textReplace: 'Promo text replace', + inactiveStatus: 'Scheduled - inactive', + manifestInsertFile: 'promo-insert.json', + manifestReplaceFile: 'promo-replace.json', + }, + tags: '@promo @commerce @smoke @regression', + }, + { + tcid: '9', + name: '@Promo-page-filter-insert', + path: '/drafts/nala/features/promotions/promo-page-filter-insert', + data: { + textMarquee: 'Promo test page', + textDefault: 'Default text', + textAfterMarquee: 'Marquee promo text insert', + }, + tags: '@promo @commerce @regression', + }, + { + tcid: '10', + name: '@Promo-page-filter-replace', + path: '/drafts/nala/features/promotions/promo-page-filter-replace', + data: { + textReplaceMarquee: 'Marquee promo replace', + textDefault: 'Default text', + }, + tags: '@promo @commerce @regression', + }, + { + tcid: '11', + name: '@Promo-page-filter-geo', + path: '/drafts/nala/features/promotions/promo-page-filter', + data: { + textMarquee: 'Promo test page', + textDefault: 'Default text', + textBeforeText: 'Common Promo', + CO_DE: '/de', + textBeforeTextDE: 'German Promo', + CO_FR: '/fr', + textBeforeTextFR: 'French Promo', + }, + tags: '@promo @commerce @smoke @regression', + }, + { + tcid: '12', + name: '@Promo-remove-fragment', + path: '/drafts/nala/features/promotions/promo-with-fragments-remove', + tags: '@promo @commerce @regression', + }, + { + tcid: '13', + name: '@Promo-fragment-insert', + path: '/drafts/nala/features/promotions/promo-with-fragments-insert', + data: { + textMarquee: 'Fragment marquee', + textBeforeMarquee: 'Text promo text insert', + textAfterMarquee: 'Marquee promo text insert', + }, + tags: '@promo @commerce @regression', + }, + ], +}; diff --git a/nala/features/promotions/promotions.test.js b/nala/features/promotions/promotions.test.js new file mode 100644 index 0000000000..880704d762 --- /dev/null +++ b/nala/features/promotions/promotions.test.js @@ -0,0 +1,534 @@ +import { expect, test } from '@playwright/test'; +import { features } from './promotions.spec.js'; +import PromoPage from './promotions.page.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +let PROMO; +test.beforeEach(async ({ page, baseURL }) => { + PROMO = new PromoPage(page); + const skipOn = ['bacom', 'business']; + + skipOn.some((skip) => { + if (baseURL.includes(skip)) test.skip(true, `Skipping the promo tests for ${baseURL}`); + return null; + }); +}); + +test.describe('Promotions feature test suite', () => { + // @Promo-insert - Validate promo insert text after marquee and before text component + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[0].path}${miloLibs}`; + const { data } = features[0]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + }); + + await test.step('Validate content insert after marquee', async () => { + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + }); + + await test.step('Validate content insert before text component', async () => { + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeText); + }); + }); + + // @Promo-replace - Validate promo replaces marquee and text component + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[1].path}${miloLibs}`; + const { data } = features[1]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content is not visible', async () => { + await expect(await PROMO.marqueeDefault).not.toBeVisible(); + await expect(await PROMO.textDefault).not.toBeVisible(); + }); + + await test.step('Validate marque replace', async () => { + await expect(await PROMO.marqueeReplace).toBeVisible(); + await expect(await PROMO.marqueeReplace).toContainText(data.textReplaceMarquee); + }); + + await test.step('Validate text component replace', async () => { + await expect(await PROMO.textReplace).toBeVisible(); + await expect(await PROMO.textReplace).toContainText(data.textReplace); + }); + }); + + // @Promo-remove - Validate promo removes text component + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[2].path}${miloLibs}`; + const { data } = features[2]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify only default test page marquee is visible', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + await expect(await PROMO.textDefault).not.toBeVisible(); + }); + + await test.step('Validate text component removed', async () => { + await expect(await PROMO.textBlock).not.toBeVisible(); + }); + }); + + // @Promo-two-manifests - Validate 2 active manifests on the page + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[3].path}${miloLibs}`; + const { data } = features[3]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content is not visible', async () => { + await expect(await PROMO.marqueeDefault).not.toBeVisible(); + await expect(await PROMO.textDefault).not.toBeVisible(); + }); + + await test.step('Validate marque replace', async () => { + await expect(await PROMO.marqueeReplace).toBeVisible(); + await expect(await PROMO.marqueeReplace).toContainText(data.textReplaceMarquee); + }); + + await test.step('Validate text component replace', async () => { + await expect(await PROMO.textReplace).toBeVisible(); + await expect(await PROMO.textReplace).toContainText(data.textReplace); + }); + + await test.step('Validate content insert after marquee', async () => { + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + }); + + await test.step('Validate content insert before text component', async () => { + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeText); + }); + }); + + // @Promo-replace-fragment - Validate fragment marquee replace + test(`${features[4].name},${features[4].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[4].path}${miloLibs}`; + const { data } = features[4]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content is not visible', async () => { + await expect(await PROMO.marqueeFragment).not.toBeVisible(); + }); + + await test.step('Validate marque promo replace', async () => { + await expect(await PROMO.marqueeReplace).toBeVisible(); + await expect(await PROMO.marqueeReplace).toContainText(data.textReplaceMarquee); + }); + }); + + // @Promo-future - Validate active promo scheduled in the future + test(`${features[5].name},${features[5].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[5].path}${miloLibs}`; + const { data } = features[5]; + const previewPage = `${baseURL}${features[5].path}${'?mep='}${data.mepPath}&${miloLibs}`; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate manifest is on served on the page but inactive', async () => { + await PROMO.mepMenuOpen.click(); + await expect(await PROMO.mepManifestList).toBeVisible(); + await expect(await PROMO.mepManifestList).toContainText(data.status); + await expect(await PROMO.mepManifestList).toContainText(data.manifestFile); + }); + + await test.step('Verify default test page content', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + }); + + await test.step('Validate no future insert on the page', async () => { + await expect(await PROMO.textInsertFuture).not.toBeVisible(); + }); + + await test.step('Navigate to the page with applied future promo and validate content', async () => { + await page.goto(previewPage); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(previewPage); + console.info(`[Promo preview Page]: ${previewPage}`); + + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + + await expect(await PROMO.textInsertFuture).toBeVisible(); + await expect(await PROMO.textInsertFuture).toContainText(data.textFuture); + }); + }); + + // @Promo-with-personalization - Validate promo together with personalization and target OFF + test(`${features[6].name},${features[6].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[6].path}${miloLibs}`; + const { data } = features[6]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify only default test page marquee is visible', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + await expect(await PROMO.textDefault).not.toBeVisible(); + }); + + await test.step('Validate content insert after marquee', async () => { + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + }); + + await test.step('Validate content insert before text component', async () => { + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeText); + }); + }); + + // @Promo-with-personalization-and-target - Validate promo together with personalization and target ON + test(`${features[7].name},${features[7].tags}`, async ({ page, baseURL, browserName }) => { + test.skip(browserName === 'chromium', 'Skipping test for Chromium browser'); + + const testPage = `${baseURL}${features[7].path}${miloLibs}`; + const { data } = features[7]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify only default test page marquee is visible', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + await expect(await PROMO.textDefault).not.toBeVisible(); + }); + + await test.step('Validate content insert after marquee', async () => { + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + }); + + await test.step('Validate content insert before text component', async () => { + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeText); + }); + }); + + // @Promo-preview - Validate preview functionality + test(`${features[8].name},${features[8].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[8].path}${miloLibs}`; + const { data } = features[8]; + let previewPage; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Validate all manifests are served and active on the page', async () => { + await PROMO.mepMenuOpen.click(); + await expect(await PROMO.mepManifestList).toBeVisible(); + await expect(await PROMO.mepManifestList).not.toContainText(data.inactiveStatus); + await expect(await PROMO.mepManifestList).toContainText(data.manifestInsertFile); + await expect(await PROMO.mepManifestList).toContainText(data.manifestReplaceFile); + }); + + await test.step('Verify promo page content', async () => { + await expect(await PROMO.marqueeDefault).not.toBeVisible(); + await expect(await PROMO.textDefault).not.toBeVisible(); + + await expect(await PROMO.marqueeReplace).toBeVisible(); + await expect(await PROMO.marqueeReplace).toContainText(data.textReplaceMarquee); + + await expect(await PROMO.textReplace).toBeVisible(); + await expect(await PROMO.textReplace).toContainText(data.textReplace); + + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeText); + }); + + await test.step('Disable insert manifest and preview', async () => { + await PROMO.mepInsertDefault.click(); + await PROMO.mepPreviewButton.click(); + + await page.waitForLoadState('domcontentloaded'); + previewPage = decodeURIComponent(page.url()); + console.info(`[Preview Page]: ${previewPage}`); + expect(previewPage).toContain(data.mepInsertOff); + expect(previewPage).toContain(data.mepReplaceOn); + + await expect(await PROMO.marqueeDefault).not.toBeVisible(); + await expect(await PROMO.textDefault).not.toBeVisible(); + + await expect(await PROMO.textInsertAfterMarquee).not.toBeVisible(); + await expect(await PROMO.textInsertBeforeText).not.toBeVisible(); + + await expect(await PROMO.marqueeReplace).toBeVisible(); + await expect(await PROMO.marqueeReplace).toContainText(data.textReplaceMarquee); + + await expect(await PROMO.textReplace).toBeVisible(); + await expect(await PROMO.textReplace).toContainText(data.textReplace); + }); + + await test.step('Enable insert and disable replace manifest and preview', async () => { + await PROMO.mepMenuOpen.click(); + await PROMO.mepInsertAll.click(); + await PROMO.mepReplaceDefault.click(); + await PROMO.mepPreviewButton.click(); + + await page.waitForLoadState('domcontentloaded'); + previewPage = decodeURIComponent(page.url()); + console.info(`[Preview Page]: ${previewPage}`); + expect(previewPage).toContain(data.mepInsertOn); + expect(previewPage).toContain(data.mepReplaceOff); + + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeText); + + await expect(await PROMO.marqueeReplace).not.toBeVisible(); + await expect(await PROMO.textReplace).not.toBeVisible(); + }); + + await test.step('Desable all manifests and preview', async () => { + await PROMO.mepMenuOpen.click(); + await PROMO.mepInsertDefault.click(); + await PROMO.mepReplaceDefault.click(); + await PROMO.mepPreviewButton.click(); + + await page.waitForLoadState('domcontentloaded'); + previewPage = decodeURIComponent(page.url()); + console.info(`[Preview Page]: ${previewPage}`); + expect(previewPage).toContain(data.mepInsertOff); + expect(previewPage).toContain(data.mepReplaceOff); + + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + + await expect(await PROMO.textInsertAfterMarquee).not.toBeVisible(); + await expect(await PROMO.textInsertBeforeText).not.toBeVisible(); + await expect(await PROMO.marqueeReplace).not.toBeVisible(); + await expect(await PROMO.textReplace).not.toBeVisible(); + }); + }); + + // @Promo-page-filter-insert - Validate promo page filter with insert action + test(`${features[9].name},${features[9].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[9].path}${miloLibs}`; + const { data } = features[9]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + }); + + await test.step('Validate content insert after marquee', async () => { + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + }); + + await test.step('Validate other promo filter actions are not applied', async () => { + await expect(await PROMO.textInsertBeforeCommon).not.toBeVisible(); + await expect(await PROMO.marqueeReplace).not.toBeVisible(); + }); + }); + + // @Promo-page-filter-replace - Validate promo page filter with replace action + test(`${features[10].name},${features[10].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[10].path}${miloLibs}`; + const { data } = features[10]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default content', async () => { + await expect(await PROMO.marqueeDefault).not.toBeVisible(); + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + }); + + await test.step('Validate marque replace', async () => { + await expect(await PROMO.marqueeReplace).toBeVisible(); + await expect(await PROMO.marqueeReplace).toContainText(data.textReplaceMarquee); + }); + + await test.step('Validate other promo filter actions are not applied', async () => { + await expect(await PROMO.textInsertBeforeCommon).not.toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).not.toBeVisible(); + }); + }); + + // @Promo-page-filter-geo - Validate promo page filter in default, de and fr locales + test(`${features[11].name},${features[11].tags}`, async ({ page, baseURL }) => { + let testPage = `${baseURL}${features[11].path}${miloLibs}`; + const { data } = features[11]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.marqueeDefault).toContainText(data.textMarquee); + + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textDefault).toContainText(data.textDefault); + }); + + await test.step('Validate content insert before text', async () => { + await expect(await PROMO.textInsertBeforeCommon).toBeVisible(); + await expect(await PROMO.textInsertBeforeCommon).toContainText(data.textBeforeText); + }); + + await test.step('Validate other promo filter actions are not applied', async () => { + await expect(await PROMO.textInsertAfterMarquee).not.toBeVisible(); + await expect(await PROMO.marqueeReplace).not.toBeVisible(); + }); + + await test.step('Go to the test page in DE locale', async () => { + testPage = `${baseURL}${data.CO_DE}${features[11].path}${miloLibs}`; + console.info('[Test Page][DE]: ', testPage); + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify page filter on DE page', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textInsertBeforeCommonDE).toBeVisible(); + await expect(await PROMO.textInsertBeforeCommonDE).toContainText(data.textBeforeTextDE); + await expect(await PROMO.textInsertAfterMarquee).not.toBeVisible(); + await expect(await PROMO.marqueeReplace).not.toBeVisible(); + }); + + await test.step('Go to the test page in FR locale', async () => { + testPage = `${baseURL}${data.CO_FR}${features[11].path}${miloLibs}`; + console.info('[Test Page][FR]: ', testPage); + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify page filter on FR page', async () => { + await expect(await PROMO.marqueeDefault).toBeVisible(); + await expect(await PROMO.textDefault).toBeVisible(); + await expect(await PROMO.textInsertBeforeCommonFR).toBeVisible(); + await expect(await PROMO.textInsertBeforeCommonFR).toContainText(data.textBeforeTextFR); + await expect(await PROMO.textInsertAfterMarquee).not.toBeVisible(); + await expect(await PROMO.marqueeReplace).not.toBeVisible(); + }); + }); + + // @Promo-remove-fragment - Validate fragment marquee remove + test(`${features[12].name},${features[12].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[12].path}${miloLibs}`; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content is not visible', async () => { + await expect(await PROMO.marqueeFragment).not.toBeVisible(); + }); + }); + + // @Promo-fragment-insert - Validate promo insert text after and before fragment + test(`${features[13].name},${features[13].tags}`, async ({ page, baseURL }) => { + const testPage = `${baseURL}${features[13].path}${miloLibs}`; + const { data } = features[13]; + console.info('[Test Page]: ', testPage); + + await test.step('Go to the test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('Verify default test page content', async () => { + await expect(await PROMO.marqueeFragment).toBeVisible(); + await expect(await PROMO.marqueeFragment).toContainText(data.textMarquee); + }); + + await test.step('Validate content insert after marquee', async () => { + await expect(await PROMO.textInsertAfterMarquee).toBeVisible(); + await expect(await PROMO.textInsertAfterMarquee).toContainText(data.textAfterMarquee); + }); + + await test.step('Validate content insert before text component', async () => { + await expect(await PROMO.textInsertBeforeText).toBeVisible(); + await expect(await PROMO.textInsertBeforeText).toContainText(data.textBeforeMarquee); + }); + }); +}); diff --git a/nala/libs/imslogin.js b/nala/libs/imslogin.js new file mode 100644 index 0000000000..7ccb781f77 --- /dev/null +++ b/nala/libs/imslogin.js @@ -0,0 +1,37 @@ +/* eslint-disable import/no-import-module-exports, import/no-extraneous-dependencies, max-len, no-console */ +import { expect } from '@playwright/test'; +import selectors from '../features/imslogin/imslogin.page.js'; + +async function clickSignin(page) { + const signinBtn = page.locator(selectors['@gnav-signin']); + await expect(signinBtn).toBeVisible(); + await signinBtn.click(); +} + +async function fillOutSignInForm(props, page) { + expect(process.env.IMS_EMAIL, 'ERROR: No environment variable for email provided for IMS Test.').toBeTruthy(); + expect(process.env.IMS_PASS, 'ERROR: No environment variable for password provided for IMS Test.').toBeTruthy(); + + await expect(page).toHaveTitle(/Adobe ID/); + let heading = await page.locator(selectors['@page-heading']).first().innerText(); + expect(heading).toBe('Sign in'); + + // Fill out Sign-in Form + await expect(async () => { + await page.locator(selectors['@email']).fill(process.env.IMS_EMAIL); + await page.locator(selectors['@email-continue-btn']).click(); + await expect(page.locator(selectors['@password-reset'])).toBeVisible({ timeout: 45000 }); // Timeout accounting for how long IMS Login page takes to switch form + }).toPass({ + intervals: [1_000], + timeout: 10_000, + }); + + heading = await page.locator(selectors['@page-heading'], { hasText: 'Enter your password' }).first().innerText(); + expect(heading).toBe('Enter your password'); + await page.locator(selectors['@password']).fill(process.env.IMS_PASS); + await page.locator(selectors['@password-continue-btn']).click(); + await page.waitForURL(`${props.url}#`); + await expect(page).toHaveURL(`${props.url}#`); +} + +module.exports = { clickSignin, fillOutSignInForm }; diff --git a/nala/utils/pr.run.sh b/nala/utils/pr.run.sh new file mode 100755 index 0000000000..0d72a8d66a --- /dev/null +++ b/nala/utils/pr.run.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +TAGS="" +REPORTER="" +EXCLUDE_TAGS="--grep-invert nopr" +EXIT_STATUS=0 +PR_NUMBER=$(echo "$GITHUB_REF" | awk -F'/' '{print $3}') +echo "PR Number: $PR_NUMBER" + +# Extract feature branch name from GITHUB_HEAD_REF +FEATURE_BRANCH="$GITHUB_HEAD_REF" +# Replace "/" characters in the feature branch name with "-" +FEATURE_BRANCH=$(echo "$FEATURE_BRANCH" | sed 's/\//-/g') +echo "Feature Branch Name: $FEATURE_BRANCH" + +PR_BRANCH_LIVE_URL_GH="https://$FEATURE_BRANCH--$prRepo--$prOrg.hlx.live" +# set pr branch url as env +export PR_BRANCH_LIVE_URL_GH +export PR_NUMBER + +echo "PR Branch live URL: $PR_BRANCH_LIVE_URL_GH" +echo "*******************************" + +# Convert GitHub Tag(@) labels that can be grepped +for label in ${labels}; do + if [[ "$label" = \@* ]]; then + label="${label:1}" + TAGS+="|$label" + fi +done + +# Remove the first pipe from tags if tags are not empty +[[ ! -z "$TAGS" ]] && TAGS="${TAGS:1}" && TAGS="-g $TAGS" + +# Retrieve GitHub reporter parameter if not empty +# Otherwise, use reporter settings in playwright.config.js +REPORTER=$reporter +[[ ! -z "$REPORTER" ]] && REPORTER="--reporter $REPORTER" + +echo "*** Running Nala on $FEATURE_BRANCH ***" +echo "Tags : $TAGS" +echo "npx playwright test ${TAGS} ${EXCLUDE_TAGS} ${REPORTER}" + +# Navigate to the GitHub Action path and install dependencies +cd "$GITHUB_ACTION_PATH" || exit +npm ci +npx playwright install --with-deps + +# Run Playwright tests on the specific projects using root-level playwright.config.js +# This will be changed later +echo "*** Running tests on specific projects ***" +npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --project=milo-live-chromium --project=milo-live-firefox ${REPORTER} || EXIT_STATUS=$? + +# Check if tests passed or failed +if [ $EXIT_STATUS -ne 0 ]; then + echo "Some tests failed. Exiting with error." + exit $EXIT_STATUS +else + echo "All tests passed successfully." +fi From cb05c67577ac0e50816e0c866f30c0680768e7ec Mon Sep 17 00:00:00 2001 From: Robert Bogos <146744221+robert-bogos@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:55:26 +0300 Subject: [PATCH 16/22] [MWPW-157973] Run Nala checks on all PRs by default (#2877) removed run nala label check Co-authored-by: milo-pr-merge[bot] <169241390+milo-pr-merge[bot]@users.noreply.github.com> --- .github/workflows/run-nala.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/run-nala.yml b/.github/workflows/run-nala.yml index 7b701eb8e5..55120b8caa 100644 --- a/.github/workflows/run-nala.yml +++ b/.github/workflows/run-nala.yml @@ -7,7 +7,6 @@ on: jobs: action: name: Running E2E & IT - if: contains(github.event.pull_request.labels.*.name, 'run-nala') runs-on: ubuntu-latest steps: From 840e037c88e9369f16a56f8f8ac7088d2f51f994 Mon Sep 17 00:00:00 2001 From: Ryan Clayton Date: Mon, 16 Sep 2024 10:17:35 -0600 Subject: [PATCH 17/22] Revert "MWPW-154980 - Milo advanced page publishing" (#2883) Revert "MWPW-154980 - Milo advanced page publishing (#2846)" This reverts commit 8fd5925aa2bbf6b9bb757c3305727291ecec82b5. --- .../components/bulk-publisher.js | 17 ++---- libs/blocks/bulk-publish-v2/services.js | 25 -------- libs/tools/utils/publish.js | 32 ---------- libs/utils/sidekick-decorate.js | 61 +++---------------- .../bulk-publish-v2/bulk-publish-v2.test.js | 33 ++++------ .../bulk-publish-v2/mocks/authentication.js | 2 +- test/blocks/bulk-publish-v2/mocks/fetch.js | 3 +- .../mocks/response/permissions.json | 26 -------- tools/utils/utils.js | 14 +---- 9 files changed, 30 insertions(+), 183 deletions(-) delete mode 100644 libs/tools/utils/publish.js delete mode 100644 test/blocks/bulk-publish-v2/mocks/response/permissions.json diff --git a/libs/blocks/bulk-publish-v2/components/bulk-publisher.js b/libs/blocks/bulk-publish-v2/components/bulk-publisher.js index 712f663fa0..c0c88ae5db 100644 --- a/libs/blocks/bulk-publish-v2/components/bulk-publisher.js +++ b/libs/blocks/bulk-publish-v2/components/bulk-publisher.js @@ -1,7 +1,7 @@ import './job-process.js'; import { LitElement, html } from '../../../deps/lit-all.min.js'; import { getSheet } from '../../../../tools/utils/utils.js'; -import { authenticate, getPublishable, startJob } from '../services.js'; +import { authenticate, startJob } from '../services.js'; import { getConfig } from '../../../utils/utils.js'; import { delay, @@ -95,8 +95,7 @@ class BulkPublish2 extends LitElement { this.validateUrls(); } - setJobErrors(jobErrors, authErrors) { - const errors = [...jobErrors, ...authErrors]; + setJobErrors(errors) { const urls = []; errors.forEach((error) => { const matched = this.urls.filter((url) => { @@ -324,8 +323,7 @@ class BulkPublish2 extends LitElement { class="panel-title" @click=${handleToggle}> - ${this.jobs.length ? html`${this.jobs.length}` : ''} - Job Result${this.jobs.length > 1 ? 's' : ''} + Job Results
{ - /* c8 ignore next 4 */ const loader = this.renderRoot.querySelector('.load-indicator'); const message = this.renderRoot.querySelector('.message'); loader?.classList.add('hide'); @@ -431,7 +427,6 @@ class BulkPublish2 extends LitElement { const canUse = Object.values(this.user.permissions).filter((perms) => perms.canUse); if (canUse.length) return html``; message = 'Current user is not authorized to use Bulk Publishing Tool'; - /* c8 ignore next 3 */ } else { message = 'Please sign in to AEM sidekick to continue'; } diff --git a/libs/blocks/bulk-publish-v2/services.js b/libs/blocks/bulk-publish-v2/services.js index 76af892bf2..2889aa5660 100644 --- a/libs/blocks/bulk-publish-v2/services.js +++ b/libs/blocks/bulk-publish-v2/services.js @@ -1,4 +1,3 @@ -import userCanPublishPage from '../../tools/utils/publish.js'; import { PROCESS_TYPES, getErrorText, @@ -247,32 +246,8 @@ const updateRetry = async ({ queue, urls, process }) => { return newQueue; }; -// publish authentication service -const getPublishable = async ({ urls, process, user }) => { - let publishable = { authorized: [], unauthorized: [] }; - if (!isLive(process)) { - publishable.authorized = urls; - } else { - const { permissions, profile } = user; - const live = { permissions: ['read'] }; - if (permissions?.publish?.canUse) { - live.permissions.push('write'); - } - publishable = await urls.reduce(async (init, url) => { - const result = await init; - const detail = { webPath: new URL(url).pathname, live, profile }; - const { canPublish, message } = await userCanPublishPage(detail); - if (canPublish) result.authorized.push(url); - else result.unauthorized.push({ href: url, message }); - return result; - }, Promise.resolve(publishable)); - } - return publishable; -}; - export { authenticate, - getPublishable, pollJobStatus, startJob, updateRetry, diff --git a/libs/tools/utils/publish.js b/libs/tools/utils/publish.js deleted file mode 100644 index 49acf0928d..0000000000 --- a/libs/tools/utils/publish.js +++ /dev/null @@ -1,32 +0,0 @@ -import { getCustomConfig } from '../../../tools/utils/utils.js'; - -const userCanPublishPage = async (detail, isBulk = true) => { - if (!detail) return false; - const { live, profile, webPath } = detail; - let canPublish = isBulk ? live?.permissions?.includes('write') : true; - let message = 'Publishing is currently disabled for this page'; - const config = await getCustomConfig('/.milo/publish-permissions-config.json'); - const item = config?.urls?.data?.find(({ url }) => ( - url.endsWith('**') ? webPath.includes(url.slice(0, -2)) : url === webPath - )); - if (item) { - canPublish = false; - if (item.message) message = item.message; - const group = config[item.group]; - if (group && profile?.email) { - let isDeny; - const user = group.data?.find(({ allow, deny }) => { - if (deny) { - /* c8 ignore next 3 */ - isDeny = true; - return deny === profile.email; - } - return allow === profile.email; - }); - canPublish = isDeny ? !user : !!user; - } - } - return { canPublish, message }; -}; - -export default userCanPublishPage; diff --git a/libs/utils/sidekick-decorate.js b/libs/utils/sidekick-decorate.js index acaa428716..ec125ef650 100644 --- a/libs/utils/sidekick-decorate.js +++ b/libs/utils/sidekick-decorate.js @@ -1,22 +1,4 @@ -import userCanPublishPage from '../tools/utils/publish.js'; - -const PUBLISH_BTN = '.publish.plugin button'; -const BACKUP_PROFILE = '.profile-email'; -const CONFIRM_MESSAGE = 'Are you sure? This will publish to production.'; - export default function stylePublish(sk) { - const setupPublishBtn = async (page, btn) => { - const { canPublish, message } = await userCanPublishPage(page, false); - btn.setAttribute('disabled', !canPublish); - const messageText = btn.querySelector('span'); - const text = canPublish ? CONFIRM_MESSAGE : message; - if (messageText) { - messageText.innerText = text; - } else { - btn.insertAdjacentHTML('beforeend', `${text}`); - } - }; - const style = new CSSStyleSheet(); style.replaceSync(` :host { @@ -28,21 +10,19 @@ export default function stylePublish(sk) { order: 100; } .publish.plugin button { - position: relative; - } - .publish.plugin button:not([disabled=true]) { background: var(--bg-color); border-color: #b46157; color: var(--text-color); + position: relative; } - .publish.plugin button:not([disabled=true]):hover { + .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: #666; + background: var(--bg-color); border-radius: 4px; line-height: 1.2rem; padding: 8px 12px; @@ -53,9 +33,6 @@ export default function stylePublish(sk) { width: 150px; white-space: pre-wrap; } - .publish.plugin button:not([disabled=true]) > span { - background: var(--bg-color); - } .publish.plugin button:hover > span { display: block; color: var(--text-color); @@ -64,39 +41,19 @@ export default function stylePublish(sk) { content: ''; border-left: 6px solid transparent; border-right: 6px solid transparent; - border-bottom: 6px solid #666; + border-bottom: 6px solid var(--bg-color); position: absolute; text-align: center; top: -6px; left: 50%; transform: translateX(-50%); } - .publish.plugin button:not([disabled=true]) > span:before { - border-bottom: 6px solid var(--bg-color); - } `); - sk.shadowRoot.adoptedStyleSheets = [style]; - - sk.addEventListener('statusfetched', async (event) => { - const page = event?.detail?.data; - const btn = event?.target?.shadowRoot?.querySelector(PUBLISH_BTN); - if (page && btn) { - setupPublishBtn(page, btn); - } - }); - - setTimeout(async () => { - const btn = sk.shadowRoot.querySelector(PUBLISH_BTN); - btn?.setAttribute('disabled', true); - const message = btn?.querySelector('span'); - if (btn && !message) { - const page = { - webPath: window.location.pathname, - // added for edge case where the statusfetched event isnt fired on refresh - profile: { email: sk.shadowRoot.querySelector(BACKUP_PROFILE)?.innerText }, - }; - setupPublishBtn(page, btn); - } + setTimeout(() => { + const btn = sk.shadowRoot.querySelector('.publish.plugin button'); + btn?.insertAdjacentHTML('beforeend', ` + Are you sure? This will publish to production. + `); }, 500); } diff --git a/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js b/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js index 7cd56109b4..3b634681d2 100644 --- a/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js +++ b/test/blocks/bulk-publish-v2/bulk-publish-v2.test.js @@ -114,16 +114,6 @@ describe('Bulk Publish Tool', () => { await mouseEvent(rootEl.querySelector('.fix-btn')); }); - it('can trigger cannot publish config', async () => { - await clock.runAllAsync(); - await setProcess(rootEl, 'publish'); - await setTextArea(rootEl, 'https://error--milo--adobecom.hlx.page/not/a/valid/path'); - await mouseEvent(rootEl.querySelector('#RunProcess')); - const errors = rootEl.querySelector('.errors'); - expect(errors.querySelector('strong').innerText).to.equal('Publishing disabled until the test is over'); - await mouseEvent(rootEl.querySelector('.fix-btn')); - }); - it('can validate milo urls and enable form', async () => { await clock.runAllAsync(); await setProcess(rootEl, 'publish'); @@ -142,17 +132,6 @@ describe('Bulk Publish Tool', () => { await mouseEvent(rootEl.querySelector('.switch.half')); }); - it('can toggle job timing flyout', async () => { - await clock.runAllAsync(); - const doneJobProcess = rootEl.querySelector('job-process'); - const jobInfo = doneJobProcess?.shadowRoot.querySelector('job-info'); - const timerDetail = jobInfo?.shadowRoot.querySelector('.timer'); - await mouseEvent(timerDetail); - await clock.runAllAsync(); - await mouseEvent(timerDetail); - expect(timerDetail.classList.contains('show-times')).to.be.false; - }); - it('can submit valid bulk preview job', async () => { await clock.runAllAsync(); await setProcess(rootEl, 'preview'); @@ -195,6 +174,17 @@ describe('Bulk Publish Tool', () => { expect(rootEl.querySelectorAll('job-process')).to.have.lengthOf(4); }); + it('can toggle job timing flyout', async () => { + await clock.runAllAsync(); + const doneJobProcess = rootEl.querySelector('job-process'); + const jobInfo = doneJobProcess?.shadowRoot.querySelector('job-info'); + const timerDetail = jobInfo?.shadowRoot.querySelector('.timer'); + await mouseEvent(timerDetail); + await clock.runAllAsync(); + await mouseEvent(timerDetail); + expect(timerDetail.classList.contains('show-times')).to.be.false; + }); + it('can toggle view mode', async () => { await mouseEvent(rootEl.querySelector('.switch.full')); await clock.runAllAsync(); @@ -228,6 +218,7 @@ describe('Bulk Publish Tool', () => { it('can clear bulk jobs', async () => { await clock.runAllAsync(); await mouseEvent(rootEl.querySelector('.clear-jobs')); + await clock.runAllAsync(); expect(rootEl.querySelectorAll('job-process')).to.have.lengthOf(0); }); }); diff --git a/test/blocks/bulk-publish-v2/mocks/authentication.js b/test/blocks/bulk-publish-v2/mocks/authentication.js index ca826fa82d..22d01045ad 100644 --- a/test/blocks/bulk-publish-v2/mocks/authentication.js +++ b/test/blocks/bulk-publish-v2/mocks/authentication.js @@ -23,7 +23,7 @@ class MockAuth extends HTMLElement { bubbles: true, detail: { data: { - profile: { name: 'Unit Test', email: 'tester@adobe.com' }, + profile: { name: 'Unit Test' }, preview: { permissions }, live: { permissions }, }, diff --git a/test/blocks/bulk-publish-v2/mocks/fetch.js b/test/blocks/bulk-publish-v2/mocks/fetch.js index 48d7637f54..8ab62d86d3 100644 --- a/test/blocks/bulk-publish-v2/mocks/fetch.js +++ b/test/blocks/bulk-publish-v2/mocks/fetch.js @@ -11,7 +11,6 @@ const requests = { delstatus: 'https://admin.hlx.page/job/adobecom/milo/main/preview-remove/job-2024-01-24t23-16-20-377z/details', retry: 'https://admin.hlx.page/preview/adobecom/milo/main/tools/bulk-publish-v2-test', index: 'https://admin.hlx.page/index/adobecom/milo/main/tools/bulk-publish-v2-test', - permissions: '/.milo/publish-permissions-config.json', }; const getMocks = async () => { @@ -26,7 +25,7 @@ const getMocks = async () => { export async function mockFetch() { const mocks = await getMocks(); stub(window, 'fetch').callsFake((...args) => { - const headers = args[1]?.body ?? null; + const headers = args[1].body ?? null; const body = headers ? JSON.parse(headers) : false; const [resource] = args; const response = mocks.find((mock) => (body.delete ? mock.request === 'delete' : mock.url === resource)); diff --git a/test/blocks/bulk-publish-v2/mocks/response/permissions.json b/test/blocks/bulk-publish-v2/mocks/response/permissions.json deleted file mode 100644 index 37fe5eaec9..0000000000 --- a/test/blocks/bulk-publish-v2/mocks/response/permissions.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "urls": { - "total": 4, - "offset": 0, - "limit": 4, - "data": [ - { - "url": "/not/a/valid/path", - "group": "gwp-test", - "message": "Publishing disabled until the test is over" - } - ] - }, - "gwp-test": { - "total": 2, - "offset": 0, - "limit": 2, - "data": [ - { "allow": "testuser@adobe.com" }, - { "allow": "testuser1@adobe.com" } - ] - }, - ":version": 3, - ":names": ["urls", "gwp-US", "no-publish", "gwp-FR"], - ":type": "multi-sheet" -} diff --git a/tools/utils/utils.js b/tools/utils/utils.js index 8193d21fdf..29d68397d8 100644 --- a/tools/utils/utils.js +++ b/tools/utils/utils.js @@ -1,7 +1,6 @@ const IMS_CLIENT_ID = 'milo_ims'; const IMS_PROD_URL = 'https://auth.services.adobe.com/imslib/imslib.min.js'; const STYLE_SHEETS = {}; -const CONFIGS = {}; const getImsToken = async (loadScript) => { window.adobeid = { @@ -26,15 +25,4 @@ const getSheet = async (url) => { return sheet; }; -const getCustomConfig = async (path) => { - /* c8 ignore next 3 */ - if (CONFIGS[path] !== undefined) { - return CONFIGS[path]; - } - const resp = await fetch(path); - const config = resp.ok ? await resp.json() : null; - CONFIGS[path] = config; - return config; -}; - -export { getImsToken, getSheet, getCustomConfig }; +export { getImsToken, getSheet }; From d4134c85979cfbb88a01d331f8d946eb67251de5 Mon Sep 17 00:00:00 2001 From: Okan Sahin <39759830+mokimo@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:15:35 +0200 Subject: [PATCH 18/22] MWPW-156749: Fix video CLS (#2849) * MWPW-155412: Fix video CLS netting +10 lighthouse points (#2724) * fix cls by adding the videoEl without a source * adapt the video hover and in view port play * Remove no-lazy and return earlier * Fix linting issue * Consolidate duplicated logic * Move functions into the init function * Move root margin and use optional chaining * Only query for the videoEl once * Add default hash at in the right spot * Fix the figure/video interaction * Remove unused export statement * Keep the previous defaults * Re-add adding autoplay * Retain decorateBlockBg logic * Move autoplay logic to the right location * Fix linting issues * Adapt figure tests * PR Feedback * Safeguard decorateAnchorVideo * Remove anchor if neccessary * Add video test --- libs/blocks/adobetv/adobetv.js | 34 ++++----------- libs/blocks/figure/figure.js | 36 ++++++++-------- libs/blocks/video/video.js | 43 ++++++------------- libs/utils/decorate.js | 29 ++++++++++--- test/blocks/adobetv/adobetv.test.js | 11 ----- test/blocks/adobetv/mocks/body.html | 2 +- test/blocks/figure/figure.test.js | 5 ++- test/blocks/figure/mocks/body.html | 16 +------- test/blocks/marquee/marquee.test.js | 21 ++++++++-- test/blocks/video/mocks/body.html | 2 +- test/blocks/video/video.test.js | 64 +++++++++++++---------------- 11 files changed, 113 insertions(+), 150 deletions(-) diff --git a/libs/blocks/adobetv/adobetv.js b/libs/blocks/adobetv/adobetv.js index 31cfc7a368..85928d1c03 100644 --- a/libs/blocks/adobetv/adobetv.js +++ b/libs/blocks/adobetv/adobetv.js @@ -1,22 +1,15 @@ -import { createIntersectionObserver } from '../../utils/utils.js'; -import { applyHoverPlay, getVideoAttrs } from '../../utils/decorate.js'; +import { decorateAnchorVideo } from '../../utils/decorate.js'; -const ROOT_MARGIN = 1000; - -const loadAdobeTv = (a) => { +export default function init(a) { + a.classList.add('hide-video'); const bgBlocks = ['aside', 'marquee', 'hero-marquee']; if (a.href.includes('.mp4') && bgBlocks.some((b) => a.closest(`.${b}`))) { a.classList.add('hide'); - const { href, hash, dataset } = a; - const attrs = getVideoAttrs(hash || 'autoplay', dataset); - const video = ``; if (!a.parentNode) return; - a.insertAdjacentHTML('afterend', video); - const videoElem = document.body.querySelector(`source[src="${href}"]`)?.parentElement; - applyHoverPlay(videoElem); - a.remove(); + decorateAnchorVideo({ + src: a.href, + anchorTag: a, + }); } else { const embed = `