diff --git a/creativecloud/blocks/prm-yt-gallery/prm-yt-gallery.js b/creativecloud/blocks/prm-yt-gallery/prm-yt-gallery.js index 482414a1a..13e65dd8a 100644 --- a/creativecloud/blocks/prm-yt-gallery/prm-yt-gallery.js +++ b/creativecloud/blocks/prm-yt-gallery/prm-yt-gallery.js @@ -15,6 +15,7 @@ const DEFAULT_PROPS = { collectionId: 'GClrnGPOMA39cFtEAAeNWoykuhUmaUw0', buttonText: 'Edit this template', freeTagText: 'Free', + branchLinkTestId: 'gRUEA7oVAYb', }; // CSS Class Names @@ -57,7 +58,7 @@ const ICONS = { const cleanUrl = (url) => (url ? url.replace(/\\\//g, '/') : ''); // Generates template deep link URL. -const createTemplateDeepLink = (templateId) => `https://premierepro.app.link/gRUEA7oVAYb?template_id=${templateId}`; +const createTemplateDeepLink = (templateId, branchLinkTestId) => `https://premierepro.app.link/${branchLinkTestId}?template_id=${templateId}`; // Detects if the user is on an iOS device. const isIOSDevice = () => { @@ -95,10 +96,10 @@ const setAriaHidden = (elementOrSelector, hidden, parent = document) => { }; // Normalizes API item to consistent internal structure. -const normalizeItem = (apiItem) => ({ +const normalizeItem = (apiItem, branchLinkTestId) => ({ image: cleanUrl(apiItem.thumbnail_url), altText: apiItem.title || 'premiere youtube card', - deepLinkUrl: createTemplateDeepLink(apiItem.id), + deepLinkUrl: createTemplateDeepLink(apiItem.id, branchLinkTestId), video: cleanUrl(apiItem.video_preview_url), isFree: apiItem.is_free || false, ID: apiItem.id, @@ -151,6 +152,7 @@ const PROPERTY_MAP = { 'collection-id': 'collectionId', button: 'buttonText', 'free-tag-text': 'freeTagText', + 'branch-link-test-id': 'branchLinkTestId', }; // Parses block properties from the authoring table. @@ -538,14 +540,14 @@ const addFreeTagToCard = (card, freeTagText) => { }; // Updates cards with fetched data from API. -const updateCardsWithData = (container, data, cardLimit, freeTagText) => { +const updateCardsWithData = (container, data, cardLimit, freeTagText, branchLinkTestId) => { const cards = container.querySelectorAll(`.${CLASSES.CARD}`); const items = data?.files?.slice(0, cardLimit) || []; items.forEach((rawItem, index) => { const card = cards[index]; if (!card) return; - const item = normalizeItem(rawItem); + const item = normalizeItem(rawItem, branchLinkTestId); const eager = index < CONFIG.EAGER_LOAD_COUNT; updateCardWithData(card, item, eager); @@ -592,6 +594,6 @@ export default async function init(el) { limit: 96, }); if (data) { - updateCardsWithData(grid, data, cardLimit, blockProps.freeTagText); + updateCardsWithData(grid, data, cardLimit, blockProps.freeTagText, blockProps.branchLinkTestId); } } diff --git a/creativecloud/blocks/showcase-marquee/showcase-marquee.css b/creativecloud/blocks/showcase-marquee/showcase-marquee.css index 2be601ed7..9c62f9de3 100644 --- a/creativecloud/blocks/showcase-marquee/showcase-marquee.css +++ b/creativecloud/blocks/showcase-marquee/showcase-marquee.css @@ -1,3 +1,7 @@ +:root { + --offset-filler-size: 1.5rem; +} + .showcase-marquee { position: relative; color: var(--color-white); @@ -81,6 +85,49 @@ /* background-color: red; */ } +.showcase-marquee .animation-controls { + display: flex; + padding: 0 0.5rem 0.5rem; + width: auto; + align-items: center; + justify-content: end; + position: absolute; + inset-block-end: 5%; + inset-inline-end: 1rem; +} + +.showcase-marquee .animation-controls .pause-play-wrapper { + display: flex; + justify-content: center; + align-items: center; + width: fit-content; + border-radius: 50%; + z-index: 3; + padding: 2px; + cursor: pointer; + background-color: var(--color-white); + margin: 0; +} + +.showcase-marquee .animation-controls .pause-play-wrapper .offset-filler { + display: flex; + justify-content: center; + align-items: center; + width: var(--offset-filler-size); + height: var(--offset-filler-size); + border-radius: inherit; + background: var(--color-gray-800); +} + +.showcase-marquee .animation-controls .pause-play-wrapper:focus-visible { + outline: var(--color-accent-focus-ring) solid 2px; +} + +.showcase-marquee .pause-play-wrapper .offset-filler.is-playing .play-icon, +.showcase-marquee .pause-play-wrapper .offset-filler:not(.is-playing) .pause-icon { + display: none; +} + .showcase-marquee .logo-row { opacity: .3; position: absolute; @@ -102,6 +149,10 @@ animation-name: scroll-right; } +.showcase-marquee .logo-row .logo-container.paused { + animation-play-state: paused; +} + .showcase-marquee.unity-enabled .interactive-area { background: none; } @@ -190,6 +241,10 @@ padding: 0; } + .showcase-marquee .animation-controls .pause-play-wrapper .offset-filler{ + --offset-filler-size: 2rem; + } + .showcase-marquee.unity-enabled .interactive-area { width: auto; } @@ -252,6 +307,10 @@ margin-bottom: 20px; } + .showcase-marquee .animation-controls .pause-play-wrapper .offset-filler { + --offset-filler-size: 2.5rem; + } + .showcase-marquee.unity-enabled .interactive-area { width: auto; } @@ -303,4 +362,23 @@ .showcase-marquee .logo-row svg { height: 50px; } + + .showcase-marquee .animation-controls { + padding: 0; + width: fit-content; + inset-block-end: 8%; + inset-inline-end: 2.5rem; + justify-content: center; + border-radius: 50%; + } +} + +@media (prefers-reduced-motion: reduce) { + .showcase-marquee .logo-row .logo-container { + animation: none !important; + } + + .showcase-marquee .animation-controls { + display: none; + } } diff --git a/creativecloud/blocks/showcase-marquee/showcase-marquee.js b/creativecloud/blocks/showcase-marquee/showcase-marquee.js index 99d95459b..25824ef75 100644 --- a/creativecloud/blocks/showcase-marquee/showcase-marquee.js +++ b/creativecloud/blocks/showcase-marquee/showcase-marquee.js @@ -3,6 +3,21 @@ import { getLibs } from '../../scripts/utils.js'; const miloLibs = getLibs('/libs'); const { createTag } = await import(`${miloLibs}/utils/utils.js`); +const LANA_OPTIONS = { tags: 'showcase-marquee', errorType: 'i' }; + +const PLACEHOLDER_LABELS = ['pause-motion', 'play-motion', 'pause-icon', 'play-icon']; + +let animationLabels = { + playMotion: 'Play', + pauseMotion: 'Pause', + pauseIcon: 'Pause icon', + playIcon: 'Play icon', +}; + +function logError(message, error) { + window.lana?.log(`Animation slot text ${message}: ${error}`, LANA_OPTIONS); +} + export function createRollingLogos(logos) { const copies = Array.from({ length: 3 }, () => { const copy = createTag('div', { class: 'logos-duplicate' }); @@ -67,9 +82,110 @@ export function createRollingLogos(logos) { return { addScrolling, logoContainer }; } +async function fetchAnimationLabels(getFedsPlaceholderConfig, replaceKeyArray) { + try { + const [pauseMotion, playMotion, pauseIcon, playIcon] = await replaceKeyArray( + PLACEHOLDER_LABELS, + getFedsPlaceholderConfig(), + ); + return { playMotion, pauseMotion, pauseIcon, playIcon, hasFetched: true }; + } catch (err) { + logError('Failed to fetch animation labels', err); + return animationLabels; + } +} + +function initAnimationControls({ button, iconWrapper, logoContainer }) { + let isPlaying = true; + if (!button || !iconWrapper || !logoContainer) return; + + const updateControlState = (playing) => { + isPlaying = playing; + iconWrapper.classList.toggle('is-playing', playing); + button.setAttribute('aria-label', playing ? animationLabels.pauseMotion : animationLabels.playMotion); + button.setAttribute('title', playing ? animationLabels.pauseMotion : animationLabels.playMotion); + button.setAttribute('aria-pressed', String(playing)); + }; + + const pauseAnimation = () => { + updateControlState(false); + const { transform } = getComputedStyle(logoContainer); + logoContainer.style.animation = 'none'; + logoContainer.style.transform = transform; + logoContainer.classList.add('paused'); + }; + + const playAnimation = () => { + updateControlState(true); + logoContainer.style.animation = ''; + logoContainer.style.transform = ''; + logoContainer.classList.remove('paused'); + }; + + const toggleAnimation = () => (isPlaying ? pauseAnimation() : playAnimation()); + + const handleKeydown = (event) => { + if (event.code === 'Enter' || event.code === 'Space') { + event.preventDefault(); + toggleAnimation(); + } + }; + + button.addEventListener('click', (event) => { + event.preventDefault(); + toggleAnimation(); + }); + + button.addEventListener('keydown', handleKeydown); +} + +function createAnimationControls({ container, getFederatedContentRoot, logoContainer }) { + const fedRoot = getFederatedContentRoot(); + if (!container || !fedRoot) return; + const controlsWrapper = createTag('div', { class: 'animation-controls' }); + + const button = createTag('button', { + class: 'pause-play-wrapper', + title: animationLabels.pauseMotion, + 'aria-label': animationLabels.pauseMotion, + 'aria-pressed': true, + }); + + const iconWrapper = createTag('div', { class: 'offset-filler is-playing' }); + + const playIcon = createTag('img', { + class: 'accessibility-control play-icon', + alt: animationLabels.playIcon, + src: `${fedRoot}/federal/assets/svgs/accessibility-play.svg`, + }); + + const pauseIcon = createTag('img', { + class: 'accessibility-control pause-icon', + alt: animationLabels.pauseIcon, + src: `${fedRoot}/federal/assets/svgs/accessibility-pause.svg`, + }); + + iconWrapper.append(playIcon, pauseIcon); + button.appendChild(iconWrapper); + controlsWrapper.appendChild(button); + + container.appendChild(controlsWrapper); + + initAnimationControls({ + button, + iconWrapper, + logoContainer, + }); +} + export default async function init(el) { const { decorateBlockBg } = await import(`${miloLibs}/utils/decorate.js`); - + const { getFederatedContentRoot, getFedsPlaceholderConfig } = await import(`${miloLibs}/utils/utils.js`); + const { replaceKeyArray } = await import(`${miloLibs}/features/placeholders.js`); + animationLabels = await fetchAnimationLabels( + getFedsPlaceholderConfig, + replaceKeyArray, + ); const children = el.querySelectorAll(':scope > div'); const foreground = children[children.length - 2]; @@ -95,10 +211,13 @@ export default async function init(el) { logoRowContent.innerHTML = ''; // TODO: cut down 1 level of DOM nesting const { logoContainer, addScrolling } = createRollingLogos(logos); + createAnimationControls({ container: logoRowContent, getFederatedContentRoot, logoContainer }); logoRowContent.append(logoContainer); new IntersectionObserver(([{ isIntersecting }], ob) => { if (!isIntersecting) return; ob.disconnect(); - addScrolling(); + if (!logoContainer.classList.contains('paused')) { + addScrolling(); + } }).observe(el); } diff --git a/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.page.js b/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.page.js new file mode 100644 index 000000000..6cbb596ce --- /dev/null +++ b/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.page.js @@ -0,0 +1,26 @@ +export default class mediaroundedcorners { + constructor(page) { + this.page = page; + // Media rounded corners styles for both images and madia block UI locators + this.mediaRoundedCornerImageGroup = page.locator('.section.three-up.s-spacing'); + this.firstimagerow = this.mediaRoundedCornerImageGroup.locator('.media-row').nth(0); + this.secondimagerow = this.mediaRoundedCornerImageGroup.locator('.media-row').nth(1); + this.thirdimagerow = this.mediaRoundedCornerImageGroup.locator('.media-row').nth(2); + this.image_SmallRoundedCorners = this.mediaRoundedCornerImageGroup.locator('.media.s-rounded-corners-image.con-block.has-bg').first(); + this.image_MediumRoundedCorners = this.mediaRoundedCornerImageGroup.locator('media.m-rounded-corners-image.con-block.has-bg'); + this.image_LargeRoundedCorners = this.mediaRoundedCornerImageGroup.locator('media.l-rounded-corners-image.con-block.has-bg'); + this.mediaRoundedBlockGroup = page.locator('.section.two-up.s-spacing').first(); + this.firstMediaBlock = this.mediaRoundedBlockGroup.locator('.media.l-rounded-corners.con-block.has-bg'); + this.secondMediaBlock = this.mediaRoundedBlockGroup.locator('.media.large.dark.m-rounded-corners.s-rounded-corners-image.con-block.has-bg.media-reverse-mobile'); + this.imageWithInBlock = this.mediaRoundedBlockGroup.locator('//img[@src="./media_15701a692530b54cb77445d12a98ec8d5b498e188.jpeg?width=750&format=jpeg&optimize=medium"]'); + this.fullRoundedCornersBlock = page.locator('.media.l-rounded-corners.full-rounded-corners-image.con-block'); + this.fullRoundedcornerimage = this.fullRoundedCornersBlock.locator('//img[@src="./media_1f320de06e9c6a05dc899e773a62d93cf1149bb40.jpeg?width=750&format=jpeg&optimize=medium"]'); + // image & block rounded corner css property + this.cssProperties = { + smallRoundedCorners: { 'border-radius': '4px' }, + mediumRoundedCorners: { 'border-radius': '8px' }, + largeRoundedCorners: { 'border-radius': '16px' }, + fullRoundedCorners: { 'border-radius': '50%' }, + }; + } +} diff --git a/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.spec.js b/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.spec.js new file mode 100644 index 000000000..5c7ce6403 --- /dev/null +++ b/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.spec.js @@ -0,0 +1,23 @@ +module.exports = { + name: 'mediaroundedcorners', + features: [ + { + tcid: '0', + name: 'media-roundedsmall', + path: '/drafts/Automation-PW/blocks/round-media-block?georouting=off', + tags: '@cc @cc-mediarounded @cc-imageroundedcornersmall', + }, + { + tcid: '1', + name: 'media-roundedmedium', + path: '/drafts/Automation-PW/blocks/round-media-block?georouting=off', + tags: '@cc @cc-mediarounded @cc-imageroundedcornermedium', + }, + { + tcid: '2', + name: 'media-roundedlarge', + path: '/drafts/Automation-PW/blocks/round-media-block?georouting=off', + tags: '@cc @cc-mediarounded @cc-imageroundedcornerlarge', + }, + ], +}; diff --git a/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.test.js b/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.test.js new file mode 100644 index 000000000..6dff6258d --- /dev/null +++ b/nala/blocks/mediablockroundedcorners/mediablockroundedcorners.test.js @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { features } from './mediablockroundedcorners.spec.js'; +import Mediaroundcorners from './mediablockroundedcorners.page.js'; + +let roundcorners; +test.describe('verify media rounder corners features for media block and its images', () => { + test.beforeEach(async ({ page }) => { + roundcorners = new Mediaroundcorners(page); + }); + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}`); + await test.step('media block image with small 4x rounded corners', async () => { + await page.goto(`${baseURL}${features[0].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}`); + }); + await test.step('media block image with small 4x rounded corners style', async () => { + await page.waitForLoadState(); + await expect(await roundcorners.mediaRoundedCornerImageGroup).toBeTruthy(); + await expect(await roundcorners.image_SmallRoundedCorners).toBeTruthy(); + }); + }); + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}`); + await test.step('media block image with medium 8x rounded corners', async () => { + await page.goto(`${baseURL}${features[1].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}`); + }); + await test.step('media block image with Medium 8x rounded corners style', async () => { + await page.waitForLoadState(); + await expect(await roundcorners.mediaRoundedCornerImageGroup).toBeTruthy(); + await expect(await roundcorners.image_MediumRoundedCorners).toBeTruthy(); + }); + }); + + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}`); + await test.step('media block image with large 16x rounded corners', async () => { + await page.goto(`${baseURL}${features[2].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}`); + }); + await test.step('media block image with large 16x rounded corners style', async () => { + await page.waitForLoadState(); + await expect(await roundcorners.mediaRoundedCornerImageGroup).toBeTruthy(); + await expect(await roundcorners.image_LargeRoundedCorners).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/merchtable/merchtable.page.js b/nala/blocks/merchtable/merchtable.page.js new file mode 100644 index 000000000..1ee86ed82 --- /dev/null +++ b/nala/blocks/merchtable/merchtable.page.js @@ -0,0 +1,30 @@ +export default class merchtable { + constructor(page) { + this.page = page; + // merch table page UI selectors to check 3 column layout + this.merchTableSection = page.locator('.section.table-merch-section'); + this.firstColumn = this.merchTableSection.locator('.col.col-1.col-heading'); + this.secondColumn = this.merchTableSection.locator('.col.col-2.col-heading.light'); + this.thirdColumn = this.merchTableSection.locator('.col.col-3.col-heading.dark'); + this.productMnemonics = page.locator('//img[@src="./media_139dede8055046fb80034053bbe235aaadc823b7a.png?width=750&format=png&optimize=medium"]').first(); + // @merchtable-headrowitems + this.FirstRowHeading = page.locator('.row.row-1.row-heading'); + this.firstColumnHeading = this.FirstRowHeading.locator('.col.col-1.col-heading'); + this.secondColumnHeading = this.FirstRowHeading.locator('.col.col-2.col-heading.light'); + this.thirdColumnHeading = this.FirstRowHeading.locator('.col.col-3.col-heading.dark'); + this.fixedColumnheading = page.locator('//div[@class="section-head-title col col-1"]//strong[text()="Includes:"]'); + // @merchtable-rowheadprices + this.ccIndividaulProductPrice = page.locator('.price.price-optical').nth(0); + this.ccStudentProductPrice = page.locator('.price.price-optical').nth(1); + this.ccTeamsProductPrice = page.locator('.price').nth(2); + this.freeTrialBtn = page.locator('.con-button.outline.button-l').first(); + this.buyNowBtn = page.locator('.con-button.blue.button-l').first(); + // @merchtable-appdetails + this.listOfAppsInRows1 = page.locator('.col-merch-content').nth(0); + this.appPhotoshop = this.listOfAppsInRows1.locator('//p[text()="Photoshop"]').nth(0); + this.listOfAppsInRows2 = page.locator('.col-merch-content').nth(4); + this.appFresco = this.listOfAppsInRows1.locator('//p[text()="fresco"]').nth(1); + this.listOfAppsInRows3 = page.locator('.col-merch-content').nth(8); + this.appPhotoshopExpress = this.listOfAppsInRows1.locator('//p[text()="Photoshop Express"]').nth(2); + } +} diff --git a/nala/blocks/merchtable/merchtable.spec.js b/nala/blocks/merchtable/merchtable.spec.js new file mode 100644 index 000000000..b77e8e493 --- /dev/null +++ b/nala/blocks/merchtable/merchtable.spec.js @@ -0,0 +1,35 @@ +module.exports = { + name: 'merchtable', + features: [ + { + tcid: '0', + name: '@merchtable-3column-layout-block', + path: '/drafts/Automation-PW/merch-table?georouting=off', + tags: '@cc @cc-merchtable @cc-merchtableui', + }, + { + tcid: '1', + name: '@merchtable-headrowitems', + path: '/drafts/Automation-PW/merch-table?georouting=off', + tags: '@cc @cc-merchtable @cc-merchtableheadrow', + }, + { + tcid: '2', + name: '@merchtable-rowheadprices', + path: '/drafts/Automation-PW/merch-table?georouting=off', + tags: '@cc @cc-merchtable @cc-merchtableprices', + data: { + ccIndividualPrice: 'US$54.99/mo', + ccStudentPrice: 'US$9.99/mo', + ccTeamsprice: 'US$22.99/mo', + ccTeamsStrikeThrough: 'Regularly at US$54.99 per month', + }, + }, + { + tcid: '3', + name: '@merchtable-appdetails', + path: '/drafts/Automation-PW/merch-table?georouting=off', + tags: '@cc @cc-merchtable @cc-appdeatilsinrows', + }, + ], +}; diff --git a/nala/blocks/merchtable/merchtable.test.js b/nala/blocks/merchtable/merchtable.test.js new file mode 100644 index 000000000..0fbdd2342 --- /dev/null +++ b/nala/blocks/merchtable/merchtable.test.js @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test'; +import { features } from './merchtable.spec.js'; +import Merchtable from './merchtable.page.js'; + +let merchtable; +test.describe('verify merch table colum layout, headings, CSS styles, images rendering, price CTAs', () => { + test.beforeEach(async ({ page }) => { + merchtable = new Merchtable(page); + }); + // Test merch table block to be shown 3 column layout when placed on page + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}`); + await test.step('merch table should have 3 column layout when authored', 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: 3 columns should showup with memonics', async () => { + await page.waitForLoadState(); + expect(await merchtable.merchTableSection).toBeTruthy(); + expect(await merchtable.firstColumn).toBeTruthy(); + expect(await merchtable.secondColumn).toBeTruthy(); + expect(await merchtable.thirdColumn).toBeTruthy(); + expect(await merchtable.productMnemonics).toBeTruthy(); + }); + }); + + // Test merch table first row in the 3 column has heading + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}`); + await test.step('merch table first row in the 3 column has product headings', async () => { + await page.goto(`${baseURL}${features[1].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}`); + }); + await test.step('step-2: 3 columns first row is head column', async () => { + await page.waitForLoadState(); + expect(await merchtable.fixedColumnheading).toBeTruthy(); + expect(await merchtable.firstColumnHeading).toBeTruthy(); + expect(await merchtable.secondColumnHeading).toBeTruthy(); + expect(await merchtable.thirdColumnHeading).toBeTruthy(); + }); + }); + + // Test merch table column heading showing valid prodcut price with CTAs + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}`); + const { data } = features[2]; + await test.step('merch table column heading has valid product prices and CTAs', async () => { + await page.goto(`${baseURL}${features[2].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}`); + }); + await test.step('step-2: product price checks with CTAs', async () => { + await page.waitForLoadState(); + await expect(merchtable.freeTrialBtn).toBeVisible(); + await expect(merchtable.buyNowBtn).toBeVisible(); + await expect(merchtable.ccIndividaulProductPrice).toContainText(data.ccIndividualPrice); + await expect(merchtable.ccStudentProductPrice).toContainText(data.ccStudentPrice); + await expect(merchtable.ccTeamsProductPrice).toContainText(data.ccTeamsprice); + }); + }); + + // Test merch table app details rows in each product column + test(`${features[3].name},${features[3].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[3].path}`); + await test.step('merch table had product details in each column', async () => { + await page.goto(`${baseURL}${features[3].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[3].path}`); + }); + await test.step('step-2: apps display in the colums as list', async () => { + await page.waitForLoadState(); + expect(await merchtable.appPhotoshop).toBeTruthy(); + expect(await merchtable.appFresco).toBeTruthy(); + expect(await merchtable.appPhotoshopExpress).toBeTruthy(); + }); + }); +}); diff --git a/nala/blocks/photoshop-unity/photoshop-unity.page.js b/nala/blocks/photoshop-unity/photoshop-unity.page.js new file mode 100644 index 000000000..ec2bb1ab1 --- /dev/null +++ b/nala/blocks/photoshop-unity/photoshop-unity.page.js @@ -0,0 +1,21 @@ +export default class CCPhotoshopUnity { + constructor(page) { + this.page = page; + + this.unityBlock = page.locator('.upload-block.unity-enabled'); + this.uploadButton = this.unityBlock.locator("div.upload-grid.desktop-up button[type='button']"); + this.fileInput = this.unityBlock.locator('input[type="file"][id="file-upload"]').first(); + this.dropZone = this.unityBlock.locator('div.upload-grid.desktop-up >> div.drop-zone'); + this.alertContent = this.unityBlock.locator('div.upload-grid.desktop-up div.alert-content'); + this.dragAndDropText = this.unityBlock.locator('div.drop-zone p:has-text("Drag and drop an image")'); + this.videoElement = this.unityBlock.locator('div.upload-grid.desktop-up video'); + this.OnlineVideoElement = this.unityBlock.locator('div.media-container video'); + this.OnlinedropZoneText = this.unityBlock.locator('div.upload-grid.desktop-up div.drop-zone p:nth-child(1)'); + this.OnlineAgreementText = this.unityBlock.locator("//div[@class='upload-grid desktop-up']//p[contains(text(),'By uploading your')]"); + this.dropZoneParagraph = this.unityBlock.locator("div[class='upload-grid desktop-up'] div[class='drop-zone'] p").first(); + this.uploadDisclaimer = this.unityBlock.locator('div.upload-grid.desktop-up p', { hasText: 'By uploading your image or video' }); + this.removeBackgroundButton = page.locator('sp-action-button:has-text("Remove background")'); + this.progressHolder = page.locator('div.progress-holder'); + this.photoshopPreviewHeading = page.locator('h2#one-moment-as-we-take-you-to-photoshop-preview:has-text("One moment")').nth(0); + } +} diff --git a/nala/blocks/photoshop-unity/photoshop-unity.spec.js b/nala/blocks/photoshop-unity/photoshop-unity.spec.js new file mode 100644 index 000000000..a0f8374be --- /dev/null +++ b/nala/blocks/photoshop-unity/photoshop-unity.spec.js @@ -0,0 +1,11 @@ +module.exports = { + name: 'CC-Photoshop-Unity-Widget', + features: [ + { + tcid: '0', + name: '@CC-PhotoshopUnity-UI', + path: '/drafts/automation-pw/online?georouting=off', + tags: '@cc @cc-PSUnitywidget @cc-removeBGCheckUI', + }, + ], +}; diff --git a/nala/blocks/photoshop-unity/photoshop-unity.test.js b/nala/blocks/photoshop-unity/photoshop-unity.test.js new file mode 100644 index 000000000..f5d07a6f1 --- /dev/null +++ b/nala/blocks/photoshop-unity/photoshop-unity.test.js @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; +import { features } from './photoshop-unity.spec.js'; +import CCPhotoshopUnity from './photoshop-unity.page.js'; + +let ccPhotoshopUnity; + +test.describe('Verify Photoshop Unity Widget functionality on Stage', () => { + test.beforeEach(async ({ page }) => { + ccPhotoshopUnity = new CCPhotoshopUnity(page); + }); + // close the page after each test + test.afterEach(async ({ page }) => { + await page.close(); + }); + + // check the page load and UI elements + test(`${features[0].name}, ${features[0].tags}`, async ({ page, baseURL }) => { + console.log(`Running test: ${features[0].name}`); + await test.step('Check page load and UI elements', async () => { + await page.goto(`${baseURL}${features[0].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}`); + await expect(ccPhotoshopUnity.uploadButton).toBeTruthy(); + await expect(ccPhotoshopUnity.dropZone).toBeVisible(); + await expect(ccPhotoshopUnity.dragAndDropText).toBeVisible(); + await expect(ccPhotoshopUnity.videoElement).toBeVisible(); + await expect(ccPhotoshopUnity.dropZoneParagraph).toBeVisible(); + await expect(ccPhotoshopUnity.uploadDisclaimer).toBeVisible(); + }); + }); +}); diff --git a/nala/blocks/promobar/promobar.page.js b/nala/blocks/promobar/promobar.page.js new file mode 100644 index 000000000..9410f5b01 --- /dev/null +++ b/nala/blocks/promobar/promobar.page.js @@ -0,0 +1,15 @@ +export default class promocloseaction { + constructor(page) { + this.page = page; + // promobar with close action UI elements + this.promoBar = page.locator('.foreground.container').last(); + this.promoBarMobile = this.promoBar.locator('.mobile-up.promo-text'); + this.promoBarTablet = this.promoBar.locator('.tablet-up.promo-text'); + this.promoBarDesktop = this.promoBar.locator('.desktop-up.promo-text'); + this.PromoText = this.promoBarDesktop.locator('.text-area').nth(0); + this.promosticky = page.locator('.section.promo-sticky-section.popup.static-links.sticky-bottom'); + this.promoNotSticky = page.locator('.section.promo-sticky-section.popup.static-links.sticky-bottom.close-sticky-section'); + this.promoCTA = page.locator('.body-s.action-area').last(); + this.promoClose = page.locator('.promo-close'); + } +} diff --git a/nala/blocks/promobar/promobar.spec.js b/nala/blocks/promobar/promobar.spec.js new file mode 100644 index 000000000..0cd13ae14 --- /dev/null +++ b/nala/blocks/promobar/promobar.spec.js @@ -0,0 +1,24 @@ +module.exports = { + name: 'promocloseaction', + features: [ + { + tcid: '0', + name: '@promocloseaction-page', + path: '/drafts/Automation-PW/blocks/promocloseaction-pageload?georouting=off', + tags: '@cc @cc-promocloseaction @cc-promoui', + }, + { + tcid: '1', + name: '@promocloseaction-closebuttonfunction', + path: '/drafts/Automation-PW/blocks/promocloseaction-pageload?georouting=off', + tags: '@cc @cc-promocloseaction @cc-promoclosefeature', + }, + { + tcid: '2', + name: '@promocloseaction-getstart-CTA', + path: '/drafts/Automation-PW/blocks/promocloseaction-pageload?georouting=off', + tags: '@cc @cc-promocloseaction @cc-promogetctaaction', + url: 'https://www.adobe.com/creativecloud/plan-recommender/quiz.html', + }, + ], +}; diff --git a/nala/blocks/promobar/promobar.test.js b/nala/blocks/promobar/promobar.test.js new file mode 100644 index 000000000..5800df2fb --- /dev/null +++ b/nala/blocks/promobar/promobar.test.js @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { features } from './promobar.spec.js'; +import Promocloseaction from './promobar.page.js'; + +let promoaction; +test.describe('verify promo action bar with CTAs, close, stickiness features in page', () => { + test.beforeEach(async ({ page }) => { + promoaction = new Promocloseaction(page); + }); + // Test sticky promo bar shows up in page bottom when page loads + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}`); + await test.step('promo bar with close action shows up in page bottom when it loads', async () => { + await page.goto(`${baseURL}${features[0].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}`); + }); + await test.step('promo action with close at page botton with stickiness', async () => { + await page.waitForLoadState(); + expect(await promoaction.promoBar).toBeTruthy(); + expect(await promoaction.promoBarMobile).toBeTruthy(); + expect(await promoaction.promoBarTablet).toBeTruthy(); + expect(await promoaction.promoBarDesktop).toBeTruthy(); + expect(await promoaction.PromoText).toBeTruthy(); + }); + }); + // Test promo bar closed when hit cross icon on it and promo disappear + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}`); + await test.step('Test promo bar closed when hit cross icon on it', async () => { + await page.goto(`${baseURL}${features[1].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}`); + }); + await test.step('Test promo bar closed when hit cross icon on it and disappear in page', async () => { + await page.waitForLoadState(); + await promoaction.promoClose.click(); + await page.waitForTimeout(2000); // wait for 2 second to ensure the promo bar is closed + expect(await promoaction.promoNotSticky).toBeTruthy(); + }); + }); + // Test promo bar CTAs navigates valid urls + test(`${features[2].name},${features[2].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[2].path}`); + await test.step('sticky promo bar CTAs are working', async () => { + await page.goto(`${baseURL}${features[2].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[2].path}`); + }); + await test.step('promo bar CTAs naviagate to valid destination', async () => { + await page.waitForLoadState(); + await promoaction.promoCTA.click(); + }); + }); +}); diff --git a/nala/blocks/seocanonical/seocanonical.page.js b/nala/blocks/seocanonical/seocanonical.page.js new file mode 100644 index 000000000..fceed62f2 --- /dev/null +++ b/nala/blocks/seocanonical/seocanonical.page.js @@ -0,0 +1,12 @@ +import { expect } from '@playwright/test'; + +export default class seocanonical { + constructor(page) { + this.page = page; + this.seoCanonicalLink = page.locator('//link[@rel="canonical"]'); + } + + async checkPage() { + expect(await this.seoCanonicalLink).toBeTruthy(); + } +} diff --git a/nala/blocks/seocanonical/seocanonical.spec.js b/nala/blocks/seocanonical/seocanonical.spec.js new file mode 100644 index 000000000..0bce4063f --- /dev/null +++ b/nala/blocks/seocanonical/seocanonical.spec.js @@ -0,0 +1,17 @@ +module.exports = { + name: 'seocanonical', + features: [ + { + tcid: '0', + name: '@seocanonical-presence-cc', + path: '/creativecloud?georouting=off', + tags: '@cc @cc-canonical @cc-canonical-presence-creativecloud', + }, + { + tcid: '1', + name: '@seocanonical-presence-ps', + path: '/products/photoshop?georouting=off', + tags: '@cc @cc-canonical @cc-canonical-presence-photoshop', + }, + ], +}; diff --git a/nala/blocks/seocanonical/seocanonical.test.js b/nala/blocks/seocanonical/seocanonical.test.js new file mode 100644 index 000000000..ec9125211 --- /dev/null +++ b/nala/blocks/seocanonical/seocanonical.test.js @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { features } from './seocanonical.spec.js'; +import Seo from './seocanonical.page.js'; + +let seo; +test.describe('SEO Canonical Tests', () => { + test.beforeEach(async ({ page }) => { + seo = new Seo(page); + }); + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[0].path}`); + await test.step('SEO Canonical Tag is present in CC home page', async () => { + await page.goto(`${baseURL}${features[0].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[0].path}`); + }); + await test.step('SEO Canonical Tag is present CC home page US locale', async () => { + await page.waitForLoadState(); + expect(await seo.seoCanonicalLink).toBeTruthy(); + }); + }); + + test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { + console.info(`[Test Page]: ${baseURL}${features[1].path}`); + await test.step('SEO Canonical Tag is present in photoshop home page', async () => { + await page.goto(`${baseURL}${features[1].path}`); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveURL(`${baseURL}${features[1].path}`); + }); + await test.step('SEO Canonical Tag is present photoshop US locale page', async () => { + await page.waitForLoadState(); + expect(await seo.seoCanonicalLink).toBeTruthy(); + }); + }); +});