From c13e2befc308f4957d3e12c4e40587cf8c428f74 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Thu, 12 Dec 2024 18:23:34 +0100 Subject: [PATCH 01/17] MWPW-160756: Setup Nala tests for studio --- action.yml | 12 + .../.nala-snippets/spec-snippet.code-snippets | 25 ++ nala/libs/baseurl.js | 19 + nala/libs/imslogin.js | 31 ++ nala/libs/webutil.js | 418 ++++++++++++++++++ nala/studio/studio.page.js | 31 ++ nala/studio/studio.spec.js | 24 + nala/studio/studio.test.js | 50 +++ nala/utils/base-reporter.js | 221 +++++++++ nala/utils/global.setup.js | 136 ++++++ nala/utils/nala.run.js | 191 ++++++++ package.json | 5 +- playwright.config.js | 71 +++ pr.run.sh | 59 +++ 14 files changed, 1292 insertions(+), 1 deletion(-) create mode 100644 action.yml create mode 100644 nala/.nala-snippets/spec-snippet.code-snippets create mode 100644 nala/libs/baseurl.js create mode 100644 nala/libs/imslogin.js create mode 100644 nala/libs/webutil.js create mode 100644 nala/studio/studio.page.js create mode 100644 nala/studio/studio.spec.js create mode 100644 nala/studio/studio.test.js create mode 100644 nala/utils/base-reporter.js create mode 100644 nala/utils/global.setup.js create mode 100644 nala/utils/nala.run.js create mode 100644 playwright.config.js create mode 100644 pr.run.sh diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..91cad7ea --- /dev/null +++ b/action.yml @@ -0,0 +1,12 @@ +name: 'Nala' + +runs: + using: 'composite' + steps: + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: lts/* + + - run: $GITHUB_ACTION_PATH/pr.run.sh + shell: bash \ No newline at end of file diff --git a/nala/.nala-snippets/spec-snippet.code-snippets b/nala/.nala-snippets/spec-snippet.code-snippets new file mode 100644 index 00000000..c842f061 --- /dev/null +++ b/nala/.nala-snippets/spec-snippet.code-snippets @@ -0,0 +1,25 @@ +{ + "Create Nala Spec": { + "prefix": "create nala spec", + "body": [ + "module.exports = {", + " FeatureName: '${1:Block or Feature Name}',", + " features: [", + " {", + " tcid: '0',", + " name: '@${2:spec-name}',", + " path: '/drafts/nala/[${3:test-page-path}]',", + " data: {", + " attribute-1: '${4:value}',", + " attribute-2: '${5:value}',", + " attribute-3: '${6:value}',", + " },", + " tags: '@Block @smoke @regression @dme',", + " },", + " ],", + "};" + ], + "description": "Create a Nala spec with block name or feature name" + } +} + diff --git a/nala/libs/baseurl.js b/nala/libs/baseurl.js new file mode 100644 index 00000000..4606d244 --- /dev/null +++ b/nala/libs/baseurl.js @@ -0,0 +1,19 @@ +/* eslint-disable */ +import pkg from 'axios'; + +const { head } = pkg; +export async function isBranchURLValid(url) { + try { + const response = await head(url); + if (response.status === 200) { + console.info(`\nURL (${url}) returned a 200 status code. It is valid.`); + return true; + } else { + console.info(`\nURL (${url}) returned a non-200 status code (${response.status}). It is invalid.`); + return false; + } + } catch (error) { + console.info(`\nError checking URL (${url}): returned a non-200 status code (${response.status})`); + return false; + } +} diff --git a/nala/libs/imslogin.js b/nala/libs/imslogin.js new file mode 100644 index 00000000..21f8b489 --- /dev/null +++ b/nala/libs/imslogin.js @@ -0,0 +1,31 @@ +/* eslint-disable import/no-import-module-exports */ +import { expect } from '@playwright/test'; + +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('.spectrum-Heading1').first().innerText(); + expect(heading).toBe('Sign in'); + + // Fill out Sign-in Form + await expect(async () => { + await page.locator('#EmailPage-EmailField').fill(process.env.IMS_EMAIL); + await page.locator('[data-id=EmailPage-ContinueButton]').click(); + await expect(page.locator('text=Reset your password')).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('.spectrum-Heading1', { hasText: 'Enter your password' }).first().innerText(); + expect(heading).toBe('Enter your password'); + await page.locator('#PasswordPage-PasswordField').fill(process.env.IMS_PASS); + await page.locator('[data-id=PasswordPage-ContinueButton]').click(); + await page.locator('div.ActionList-Item:nth-child(1)').click(); + await page.waitForURL(`${props.url}#`); + await expect(page).toHaveURL(`${props.url}#`); +} + +export default { fillOutSignInForm }; diff --git a/nala/libs/webutil.js b/nala/libs/webutil.js new file mode 100644 index 00000000..72ac22b4 --- /dev/null +++ b/nala/libs/webutil.js @@ -0,0 +1,418 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +/* eslint-disable max-len */ +// eslint-disable-next-line import/no-import-module-exports +import { expect } from '@playwright/test'; + +const fs = require('fs'); +// eslint-disable-next-line import/no-extraneous-dependencies +const yaml = require('js-yaml'); +const { request } = require('@playwright/test'); + +/** + * A utility class for common web interactions. + */ +exports.WebUtil = class WebUtil { + /** + * Create a new instance of WebUtil. + * @param {object} page - A Playwright page object. + */ + constructor(page) { + this.page = page; + this.locator = null; + } + + /** + * Check if the element associated with the current locator is visible. + * @param {Locator} locator - The Playwright locator for the element to check. + * + */ + static async isVisible(locator) { + this.locator = locator; + await expect(this.locator).toBeVisible(); + return true; + } + + /** + * Check if the element associated with the current locator is displayed. + * @param {Locator} locator - The Playwright locator for the element to check. + * @returns {Promise} - Resolves to `true` if the element is displayed, or `false`. + */ + static async isDisplayed(locator) { + this.locator = locator; + try { + return await this.locator.evaluate((e) => e.offsetWidth > 0 && e.offsetHeight > 0); + } catch (e) { + console.error(`Error checking if element is displayed for locator: ${locator.toString()}`, e); + return false; + } + } + + /** + * Click the element associated with the current locator. + * @param {Locator} locator - The Playwright locator for the element to click. + * @returns {Promise} A Promise that resolves when the element has been clicked. + */ + static async click(locator) { + this.locator = locator; + return this.locator.click(); + } + + /** + * Get the inner text of the element associated with the current locator. + * @param {Locator} locator - The Playwright locator for the element to retrieve text from. + * @returns {Promise} A Promise that resolves to the inner text of the element. + */ + static async getInnerText(locator) { + this.locator = locator; + const innerText = await this.locator.innerText(); + return innerText; + } + + /** + * Get the text of the element associated with the current locator, filtered by the specified tag name. + * @param {Locator} locator - The Playwright locator for the element to retrieve text from. + * @param {string} tagName - The name of the tag to filter by (e.g. "p", "span", etc.). + * @returns {Promise} A Promise that resolves to the text of the element, filtered by the specified tag name. + */ + static async getTextByTag(locator, tagName) { + this.locator = locator; + return this.locator.$eval(tagName, (e) => e.textContent); + } + + /** + * Get the value of the specified attribute on the element associated with the current locator. + * @param {Locator} locator - The Playwright locator for the element to retrieve the attribute from. + * @param {string} attributeName - The name of the attribute to retrieve (e.g. "class", "data-attr", etc.). + * @returns {Promise} A Promise that resolves to the value of the specified attribute on the element. + */ + static async getAttribute(locator, attributeName) { + this.locator = locator; + return this.locator.getAttribute(attributeName); + } + + /** + * Verifies that the specified CSS properties of the given locator match the expected values. + * @param {Object} locator - The locator to verify CSS properties for. + * @param {Object} cssProps - The CSS properties and expected values to verify. + * @returns {Boolean} - True if all CSS properties match the expected values, false otherwise. + */ + static async verifyCSS(locator, cssProps) { + this.locator = locator; + // Verify the CSS properties and values + let result = true; + await Promise.allSettled( + Object.entries(cssProps).map(async ([property, expectedValue]) => { + try { + await expect(this.locator).toHaveCSS(property, expectedValue); + } catch (error) { + console.error(`CSS property ${property} not found:`, error); + result = false; + } + }), + ); + return result; + } + + /** + * Verifies that the specified CSS properties of the given locator match the expected values. + * @param {Object} locator - The locator to verify CSS properties for. + * @param {Object} cssProps - The CSS properties and expected values to verify. + * @returns {Boolean} - True if all CSS properties match the expected values, false otherwise. + */ + // eslint-disable-next-line no-underscore-dangle + async verifyCSS_(locator, cssProps) { + this.locator = locator; + let result = true; + await Promise.allSettled( + Object.entries(cssProps).map(async ([property, expectedValue]) => { + try { + await expect(this.locator).toHaveCSS(property, expectedValue); + } catch (error) { + console.error(`CSS property ${property} not found:`, error); + result = false; + } + }), + ); + return result; + } + + /** + * Verifies that the specified attribute properties of the given locator match the expected values. + * @param {Object} locator - The locator to verify attributes. + * @param {Object} attProps - The attribute properties and expected values to verify. + * @returns {Boolean} - True if all attribute properties match the expected values, false otherwise. + */ + static async verifyAttributes(locator, attProps) { + this.locator = locator; + let result = true; + await Promise.allSettled( + Object.entries(attProps).map(async ([property, expectedValue]) => { + if (property === 'class' && typeof expectedValue === 'string') { + // If the property is 'class' and the expected value is an string, + // split the string value into individual classes + const classes = expectedValue.split(' '); + try { + await expect(this.locator).toHaveClass(classes.join(' ')); + } catch (error) { + console.error('Attribute class not found:', error); + result = false; + } + } else { + try { + await expect(this.locator).toHaveAttribute(property, expectedValue); + } catch (error) { + console.error(`Attribute ${property} not found:`, error); + result = false; + } + } + }), + ); + return result; + } + + /** + * Verifies that the specified attribute properties of the given locator match the expected values. + * @param {Object} locator - The locator to verify attributes. + * @param {Object} attProps - The attribute properties and expected values to verify. + * @returns {Boolean} - True if all attribute properties match the expected values, false otherwise. + */ + // eslint-disable-next-line no-underscore-dangle + async verifyAttributes_(locator, attProps) { + this.locator = locator; + let result = true; + await Promise.allSettled( + Object.entries(attProps).map(async ([property, expectedValue]) => { + if (property === 'class' && typeof expectedValue === 'string') { + // If the property is 'class' and the expected value is an string, + // split the string value into individual classes + const classes = expectedValue.split(' '); + try { + await expect(await this.locator).toHaveClass(classes.join(' ')); + } catch (error) { + console.error('Attribute class not found:', error); + result = false; + } + } else { + try { + await expect(await this.locator).toHaveAttribute(property, expectedValue); + } catch (error) { + console.error(`Attribute ${property} not found:`, error); + result = false; + } + } + }), + ); + return result; + } + + /** + * Slow/fast scroll of entire page JS evaluation method, aides with lazy loaded content. + * This wrapper method calls a scroll script in page.evaluate, i.e. page.evaluate(scroll, { dir: 'direction', spd: 'speed' }); + * @param direction string direction you want to scroll on the page + * @param speed string speed you would like to scroll through the page. Options: slow, fast + */ + async scrollPage(direction, speed) { + const scroll = async (args) => { + const { dir, spd } = args; + // eslint-disable-next-line no-promise-executor-return + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const scrollHeight = () => document.body.scrollHeight; + const start = dir === 'down' ? 0 : scrollHeight(); + const shouldStop = (position) => (dir === 'down' ? position > scrollHeight() : position < 0); + const increment = dir === 'down' ? 100 : -100; + const delayTime = spd === 'slow' ? 30 : 5; + console.error(start, shouldStop(start), increment); + for (let i = start; !shouldStop(i); i += increment) { + window.scrollTo(0, i); + // eslint-disable-next-line no-await-in-loop + await delay(delayTime); + } + }; + + await this.page.evaluate(scroll, { dir: direction, spd: speed }); + } + + /** + * Check if the modal associated with the current locator is within the viewport. + * @param page - calling method page object. + * @returns {Promise} - Resolves to true if the modal is within the viewport, or false. + */ + static async isModalInViewport(page, selector) { + try { + const inViewport = await page.evaluate((sel) => { + const modalDialog = document.querySelector('.dialog-modal'); + if (!modalDialog) { + throw new Error(`Modal element with selector '${sel}' not found.`); + } + const rect = modalDialog.getBoundingClientRect(); + return ( + rect.top >= 0 + && rect.left >= 0 + && rect.bottom + <= (window.innerHeight || document.documentElement.clientHeight) + && rect.right + <= (window.innerWidth || document.documentElement.clientWidth) + ); + }, selector); + + return inViewport; + } catch (error) { + console.error('Error verifying modal veiwport:', error); + return false; + } + } + + /** + * Load test data from yml file or json file in local + * @param {string} filePath + */ + static async loadTestData(dataFilePath) { + return dataFilePath.includes('.yml') ? yaml.load(fs.readFileSync(dataFilePath, 'utf8')) : JSON.parse(fs.readFileSync(dataFilePath, 'utf8')); + } + + /** + * Load test data from remote json file + * @param {string} path + * @param {string} url + */ + static async loadTestDataFromAPI(url, path) { + const context = await request.newContext({ baseURL: url }); + const res = await context.fetch(path); + return res.json(); + } + + /** + * Makes a GET request + * @param {string} url - The URL to make the GET request to. + * @returns {object} The response object. + */ + static async getRequest(url) { + const requestContext = await request.newContext(); + const response = await requestContext.get(url); + return response; + } + + /** + * Enable network logging + * @param {Array} networklogs - An array to store all network logs + */ + async enableNetworkLogging(networklogs) { + await this.page.route('**', (route) => { + const url = route.request().url(); + if (url.includes('sstats.adobe.com/ee/or2/v1/interact') + || url.includes('sstats.adobe.com/ee/or2/v1/collect')) { + networklogs.push(url); + const firstEvent = route.request().postDataJSON().events[0]; + // eslint-disable-next-line no-underscore-dangle + if (firstEvent.data._adobe_corpnew.digitalData.primaryEvent) { + // eslint-disable-next-line no-underscore-dangle + networklogs.push(JSON.stringify(firstEvent.data._adobe_corpnew.digitalData.primaryEvent)); + } + + // eslint-disable-next-line no-underscore-dangle + if (firstEvent.data._adobe_corpnew.digitalData.search) { + // eslint-disable-next-line no-underscore-dangle + networklogs.push(JSON.stringify(firstEvent.data._adobe_corpnew.digitalData.search)); + } + } + route.continue(); + }); + } + + /** + * Disable network logging + */ + async disableNetworkLogging() { + await this.page.unroute('**'); + } + + /** + * Generates analytic string for a given project. + * @param {string} project - The project identifier, defaulting to 'milo' if not provided. + * @returns {string} - A string formatted as 'gnav||nopzn|nopzn'. + */ + // eslint-disable-next-line class-methods-use-this + async getGnavDaalh(project = milo) { + return `gnav|${project}|nopzn|nopzn`; + } + + /** + * Generates analytic string for a given project. + * @param {string} project - The project identifier, defaulting to 'milo' if not provided. + * @param {string} pznExpName - Personalized experience name, which is sliced to its first 15 characters. + * @param {string} pznFileName - Manifest filename, which is sliced to its first 20 characters. + * @returns {string} - A string formatted as 'gnav|||'. + */ + // eslint-disable-next-line class-methods-use-this, default-param-last + async getPznGnavDaalh(project = milo, pznExpName, pznFileName) { + const slicedExpName = pznExpName.slice(0, 15); + const slicedFileName = pznFileName.slice(0, 15); + return `gnav|${project}|${slicedExpName}|${slicedFileName}`; + } + + /** + * Generates analytic string for a section based on a given counter value. + * @param {number|string} counter - A counter value used to generate the section identifier. + * @returns {string} - A string formatted as 's'. + */ + // eslint-disable-next-line class-methods-use-this + async getSectionDaalh(counter) { + return `s${counter}`; + } + + /** + * Generates personalization analytic string for a given block name and a counter. + * @param {string} blockName - The name of the block, which is sliced to its first 20 characters. + * @param {number|string} counter - A counter value i.e. block number. + * @param {string} pznExpName - Personalized experience name, which is sliced to its first 15 characters. + * @param {string} pznExpName - Manifest filename, which is sliced to its first 20 characters. + * @returns {string} - A string formatted as 'b|||'. + */ + // eslint-disable-next-line class-methods-use-this + async getPznBlockDaalh(blockName, counter, pznExpName, pznFileName) { + const slicedBlockName = blockName.slice(0, 20); + const slicedExpName = pznExpName.slice(0, 15); + const slicedFileName = pznFileName.slice(0, 15); + return `b${counter}|${slicedBlockName}|${slicedExpName}|${slicedFileName}`; + } + + /** + * Generates an analytic string for a given block name and a counter. + * @param {string} blockName - The name of the block, which is sliced to its first 20 characters. + * @param {number|string} counter - A counter value, i.e., block number. + * @param {boolean} [pzn=false] - A boolean flag indicating whether to use pzntext. + * @param {string} [pzntext='nopzn'] - The pzntext to use when pzn is true, sliced to its first 15 characters. + * @returns {string} - A formatted string. + */ + // eslint-disable-next-line class-methods-use-this + async getBlockDaalh(blockName, counter, pzn = false, pzntext = 'nopzn') { + const slicedBlockName = blockName.slice(0, 20); + const slicedPzntext = pzntext.slice(0, 15); + if (pzn) { + return `b${counter}|${slicedBlockName}|${slicedPzntext}|nopzn`; + } + return `b${counter}|${slicedBlockName}`; + } + + /** + * Generates analytic string for link or button based on link/button text , a counter, and the last header text. + * @param {string} linkText - The text of the link, which is cleaned and sliced to its first 20 characters. + * @param {number|string} counter - A counter value used in the identifier. + * @param {string} lastHeaderText - The last header text, which is cleaned and sliced to its first 20 characters. + * @param {boolean} [pzn=false] - boolean parameter, defaulting to false.(for personalization) + * @returns {string} - A string formatted as '---'. + */ + // eslint-disable-next-line class-methods-use-this + async getLinkDaall(linkText, counter, lastHeaderText, pzn = false) { + const cleanAndSliceText = (text) => text + ?.replace(/[^\w\s]+/g, ' ') + .replace(/\s+/g, ' ') + .replace(/^_+|_+$/g, '') + .trim() + .slice(0, 20); + const slicedLinkText = cleanAndSliceText(linkText); + const slicedLastHeaderText = cleanAndSliceText(lastHeaderText); + return `${slicedLinkText}-${counter}--${slicedLastHeaderText}`; + } +}; \ No newline at end of file diff --git a/nala/studio/studio.page.js b/nala/studio/studio.page.js new file mode 100644 index 00000000..a8ab9981 --- /dev/null +++ b/nala/studio/studio.page.js @@ -0,0 +1,31 @@ +export default class StudioPage { + constructor(page) { + this.page = page; + + this.searchInput = page.locator('sp-search input'); + this.searchIcon = page.locator('sp-search sp-icon-magnify'); + this.filter = page.locator('sp-action-button[label="Filter"]'); + this.topFolder = page.locator('sp-picker[label="TopFolder"] > button'); + this.renderView = page.locator('render-view'); + this.suggestedCard = page.locator('merch-card[variant="ccd-suggested"]'); + this.sliceCard = page.locator('merch-card[variant="ccd-slice"]'); + this.sliceCardWide = page.locator('merch-card[variant="ccd-slice"][size="wide"]'); + + } + + async getCard(id, cardType) { + const cardVariant = { + suggested: this.suggestedCard, + slice: this.sliceCard, + 'slice-wide': this.sliceCardWide, + }; + + const card = cardVariant[cardType]; + if (!card) { + throw new Error(`Invalid card type: ${cardType}`); + } + + return card.filter({ has: this.page.locator(`aem-fragment[fragment="${id}"]`) }); + } + +} diff --git a/nala/studio/studio.spec.js b/nala/studio/studio.spec.js new file mode 100644 index 00000000..cb57158d --- /dev/null +++ b/nala/studio/studio.spec.js @@ -0,0 +1,24 @@ +export default { + FeatureName: 'M@S Studio', + features: [ + { + tcid: '0', + name: '@studio-direct-search', + path: '/studio.html', + data: { + cardid: '206a8742-0289-4196-92d4-ced99ec4191e', + title: 'Automation Test Card', + eyebrow: 'DO NOT EDIT', + description: 'MAS repo validation card for Nala tests.', + price: 'US$22.99/mo', + strikethroughPrice: 'US$37.99/mo', + cta: 'Buy now', + offerid: '30404A88D89A328584307175B8B27616', + linkText: 'See terms', + linkUrl: '', + }, + browserParams: '#query=', + tags: '@mas @mas-studio @smoke @regression', + }, + ], +}; diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js new file mode 100644 index 00000000..9f32b917 --- /dev/null +++ b/nala/studio/studio.test.js @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import studiopkg from './studio.spec.js'; +import StudioPage from './studio.page.js'; +import ims from '../libs/imslogin.js'; + +const miloLibs = process.env.MILO_LIBS || ''; + +let studio; +const { features } = studiopkg; + +test.beforeEach(async ({ page, browserName }) => { + test.skip(browserName !== 'chromium', 'Not supported to run on multiple browsers.'); + studio = new StudioPage(page); +}); + + +test.describe('M@S Studio feature test suite', () => { + // @studio-direct-search - Validate direct search feature in mas studio + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + test.slow(); + const { data } = features[0]; + const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`; + console.info('[Test Page]: ', testPage); + + await test.step('step-1: Log in to MAS studio', async () => { + await page.goto(testPage); + await page.waitForURL('**/auth.services.adobe.com/en_US/index.html**/'); + features[0].url = 'https://main--mas--adobecom.aem.live/studio.html'; + await ims.fillOutSignInForm(features[0], page); + await expect(async () => { + const response = await page.request.get(features[0].url); + expect(response.status()).toBe(200); + }).toPass(); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-2: Go to MAS Studio test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-3: Validate search results', async () => { + await expect(await studio.renderView).toBeVisible(); + + const cards = await studio.renderView.locator('merch-card'); + expect(await cards.count()).toBe(1); + }); + }); + +}); diff --git a/nala/utils/base-reporter.js b/nala/utils/base-reporter.js new file mode 100644 index 00000000..4ed0718d --- /dev/null +++ b/nala/utils/base-reporter.js @@ -0,0 +1,221 @@ +/* eslint-disable */ +// Playwright will include ANSI color characters and regex from below +// https://github.com/microsoft/playwright/issues/13522 +// https://github.com/chalk/ansi-regex/blob/main/index.js#L3 + +const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', +].join('|'); + +const ansiRegex = new RegExp(pattern, 'g'); + +// limit failed status +const failedStatus = ['failed', 'flaky', 'timedOut', 'interrupted']; + +function stripAnsi(str) { + if (!str || typeof str !== 'string') return str; + return str.replace(ansiRegex, ''); +} + +export default class BaseReporter { + constructor(options) { + this.options = options; + this.results = []; + this.passedTests = 0; + this.failedTests = 0; + this.skippedTests = 0; + } + + onBegin(config, suite) { + this.config = config; + this.rootSuite = suite; + + } + + async onTestEnd(test, result) { + const { title, retries, _projectId } = test; + const { name, tags, url, browser, env, branch, repo} = this.parseTestTitle(title, _projectId); + const { + status, + duration, + error: { message: errorMessage, value: errorValue, stack: errorStack } = {}, + retry, + } = result; + + if (retry < retries && status === 'failed') { + return; + } + this.results.push({ + title, + name, + tags, + url, + env, + browser, + branch, + repo, + status: failedStatus.includes(status) ? 'failed' : status, + errorMessage: stripAnsi(errorMessage), + errorValue, + errorStack: stripAnsi(errorStack), + stdout: test.stdout, + stderr: test.stderr, + duration, + retry, + }); + if (status === 'passed') { + this.passedTests++; + } else if (failedStatus.includes(status)) { + this.failedTests++; + } else if (status === 'skipped') { + this.skippedTests++; + } + } + + async onEnd() { + //this.printPersistingOption(); + //await this.persistData(); + const summary = this.printResultSummary(); + const resultSummary = { summary }; + + if (process.env.SLACK_WH) { + try { + console.log('----success to publish result to slack channel----'); +// await sendSlackMessage(process.env.SLACK_WH, resultSummary); + } catch (error){ + console.log('----Failed to publish result to slack channel----'); + } + } + } + + printResultSummary() { + const totalTests = this.results.length; + const passPercentage = ((this.passedTests / totalTests) * 100).toFixed(2); + const failPercentage = ((this.failedTests / totalTests) * 100).toFixed(2); + const miloLibs = process.env.MILO_LIBS || ''; + const prBranchUrl = process.env.PR_BRANCH_LIVE_URL ? (process.env.PR_BRANCH_LIVE_URL + miloLibs) : undefined; + const projectBaseUrl = this.config.projects[0].use.baseURL; + const envURL = prBranchUrl ? prBranchUrl : projectBaseUrl; + + let exeEnv = 'Local Environment'; + let runUrl = 'Local Environment'; + let runName = 'Nala Local Run'; + + if (process.env.GITHUB_ACTIONS === 'true') { + exeEnv = 'GitHub Actions Environment'; + const repo = process.env.GITHUB_REPOSITORY; + const runId = process.env.GITHUB_RUN_ID; + const prNumber = process.env.GITHUB_REF.split('/')[2]; + runUrl = `https://github.com/${repo}/actions/runs/${runId}`; + runName = `${process.env.WORKFLOW_NAME ? (process.env.WORKFLOW_NAME || 'Nala Daily Run') : 'Nala PR Run'} (${prNumber})`; + } else if (process.env.CIRCLECI) { + exeEnv = 'CircleCI Environment'; + const workflowId = process.env.CIRCLE_WORKFLOW_ID; + const jobNumber = process.env.CIRCLE_BUILD_NUM; + runUrl = `https://app.circle.ci.adobe.com/pipelines/github/wcms/nala/${jobNumber}/workflows/${workflowId}/jobs/${jobNumber}`; + runName = 'Nala CircleCI/Stage Run'; + } + + const summary = ` + \x1b[1m\x1b[34m---------Nala Test Run Summary------------\x1b[0m + \x1b[1m\x1b[33m# Total Test executed:\x1b[0m \x1b[32m${totalTests}\x1b[0m + \x1b[1m\x1b[33m# Test Pass :\x1b[0m \x1b[32m${this.passedTests} (${passPercentage}%)\x1b[0m + \x1b[1m\x1b[33m# Test Fail :\x1b[0m \x1b[31m${this.failedTests} (${failPercentage}%)\x1b[0m + \x1b[1m\x1b[33m# Test Skipped :\x1b[0m \x1b[32m${this.skippedTests}\x1b[0m + \x1b[1m\x1b[33m** Application URL :\x1b[0m \x1b[32m${envURL}\x1b[0m + \x1b[1m\x1b[33m** Executed on :\x1b[0m \x1b[32m${exeEnv}\x1b[0m + \x1b[1m\x1b[33m** Execution details:\x1b[0m \x1b[32m${runUrl}\x1b[0m + \x1b[1m\x1b[33m** Workflow name :\x1b[0m \x1b[32m${runName}\x1b[0m`; + + console.log(summary); + + if (this.failedTests > 0) { + console.log('-------- Test Failures --------'); + this.results + .filter((result) => result.status === 'failed') + .forEach((failedTest) => { + console.log(`Test: ${failedTest.title.split('@')[1]}`); + console.log(`Error Message: ${failedTest.errorMessage}`); + console.log(`Error Stack: ${failedTest.errorStack}`); + console.log('-------------------------'); + }); + } + return summary; + } + + /** + This method takes test title and projectId strings and then processes it . + @param {string, string} str - The input string to be processed + @returns {'name', 'tags', 'url', 'browser', 'env', 'branch' and 'repo'} + */ + parseTestTitle(title, projectId) { + let env = 'live'; + let browser = 'chrome'; + let branch; + let repo; + let url; + + const titleParts = title.split('@'); + const name = titleParts[1].trim(); + const tags = titleParts.slice(2).map(tag => tag.trim()); + + const projectConfig = this.config.projects.find(project => project.name === projectId); + + // Get baseURL from project config + if (projectConfig?.use?.baseURL) { + ({ baseURL: url, defaultBrowserType: browser } = projectConfig.use); + } else if (this.config.baseURL) { + url = this.config.baseURL; + } + // Get environment from baseURL + if (url.includes('prod')) { + env = 'prod'; + } else if (url.includes('stage')) { + env = 'stage'; + } + // Get branch and repo from baseURL + if (url.includes('localhost')) { + branch = 'local'; + repo = 'local'; + } else { + const urlParts = url.split('/'); + const branchAndRepo = urlParts[urlParts.length - 1]; + [branch, repo] = branchAndRepo.split('--'); + } + + return { name, tags, url, browser, env, branch, repo}; + } + + // eslint-disable-next-line class-methods-use-this, no-empty-function + async persistData() {} + + printPersistingOption() { + if (this.options?.persist) { + console.log( + `Persisting results using ${this.options.persist?.type} to ${this.options.persist?.path}`, + ); + } else { + console.log('Not persisting data'); + } + //this.branch1 = process.env.GITHUB_REF_NAME ?? 'local'; + this.branch = process.env.LOCAL_TEST_LIVE_URL; + } + + getPersistedDataObject() { + const gitBranch = process.env.GITHUB_REF_NAME ?? 'local'; + + // strip out git owner since it can usually be too long to show on the ui + const [, gitRepo] = /[A-Za-z0-9_.-]+\/([A-Za-z0-9_.-]+)/.exec( + process.env.GITHUB_REPOSITORY, + ) ?? [null, 'local']; + + const currTime = new Date(); + return { + gitBranch, + gitRepo, + results: this.results, + timestamp: currTime, + }; + } +} diff --git a/nala/utils/global.setup.js b/nala/utils/global.setup.js new file mode 100644 index 00000000..bf4c005d --- /dev/null +++ b/nala/utils/global.setup.js @@ -0,0 +1,136 @@ +/* eslint-disable */ +import { exit } from 'process'; +import { execSync } from 'child_process'; +import { isBranchURLValid } from '../libs/baseurl.js'; + +const MAIN_BRANCH_LIVE_URL = 'https://main--mas--adobecom.aem.live'; +const STAGE_URL = 'https://mas.stage.adobe.com'; + +async function getGitHubPRBranchLiveUrl() { + // get the pr number + const prReference = process.env.GITHUB_REF; + const prNumber = prReference.split('/')[2]; + + // get the pr branch name + const branch = process.env.GITHUB_HEAD_REF; + const prBranch = branch.replace(/\//g, '-'); + + // get the org and repo + const repository = process.env.GITHUB_REPOSITORY; + const repoParts = repository.split('/'); + const toRepoOrg = repoParts[0]; + const toRepoName = repoParts[1]; + + // Get the org and repo from the environment variables + const prFromOrg = process.env.prOrg; + const prFromRepoName = process.env.prRepo; + + const prBranchLiveUrl = `https://${prBranch}--${prFromRepoName}--${prFromOrg}.aem.live`; + + try { + if (await isBranchURLValid(prBranchLiveUrl)) { + process.env.PR_BRANCH_LIVE_URL = prBranchLiveUrl; + } + console.info('PR Repository : ', repository); + console.info('PR TO ORG : ', toRepoOrg); + console.info('PR TO REPO : ', toRepoName); + console.info('PR From ORG : ', prFromOrg); + console.info('PR From REPO : ', prFromRepoName); + console.info('PR Branch : ', branch); + console.info('PR Branch(U) : ', prBranch); + console.info('PR Number : ', prNumber); + console.info('PR From Branch live url : ', prBranchLiveUrl); + } catch (err) { + console.error(`Error => Error in setting PR Branch test URL : ${prBranchLiveUrl}`); + console.info(`Note: PR branch test url ${prBranchLiveUrl} is not valid, Exiting test execution.`); + process.exit(1); + } +} + +async function getGitHubMiloLibsBranchLiveUrl() { + const repository = process.env.GITHUB_REPOSITORY; + + let prBranchLiveUrl; + let miloLibs; + + prBranchLiveUrl = process.env.PR_BRANCH_MILOLIBS_LIVE_URL; + miloLibs = process.env.MILO_LIBS; + + try { + if (await isBranchURLValid(prBranchLiveUrl)) { + process.env.PR_BRANCH_LIVE_URL = prBranchLiveUrl; + } + console.info('PR Repository : ', repository); + console.info('PR Branch live url : ', prBranchLiveUrl); + console.info('Milo Libs : ', miloLibs); + } catch (err) { + console.error(`Error => Error in setting PR Branch test URL : ${prBranchLiveUrl}`); + console.info(`Note: PR branch test url ${prBranchLiveUrl} is not valid, Exiting test execution.`); + process.exit(1); + } +} + +async function getCircleCIBranchLiveUrl() { + const stageUrl = STAGE_URL; + + try { + if (await isBranchURLValid(stageUrl)) { + process.env.PR_BRANCH_LIVE_URL = stageUrl; + } + console.info('Stage Branch Live URL : ', stageUrl); + } catch (err) { + console.error('Error => Error in setting Stage Branch test URL : ', stageUrl); + console.info('Note: Stage branch test url is not valid, Exiting test execution.'); + process.exit(1); + } +} + +async function getLocalBranchLiveUrl() { + let localTestLiveUrl + try { + const localGitRootDir = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + + if (localGitRootDir) { + const gitRemoteOriginUrl = execSync('git config --get remote.origin.url', { cwd: localGitRootDir, encoding: 'utf-8' }).trim(); + const match = gitRemoteOriginUrl.match(/github\.com\/(.*?)\/(.*?)\.git/); + + if (match) { + const [localOrg, localRepo] = match.slice(1, 3); + const localBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: localGitRootDir, encoding: 'utf-8' }).trim(); + localTestLiveUrl = process.env.LOCAL_TEST_LIVE_URL || MAIN_BRANCH_LIVE_URL + if (await isBranchURLValid(localTestLiveUrl)) { + console.info('Git ORG : ', localOrg); + console.info('Git REPO : ', localRepo); + console.info('Local Branch : ', localBranch); + console.info('Local Test Live URL : ', localTestLiveUrl); + } + } + } + } catch (error) { + console.error(`Error => Error in setting local test URL : ${localTestLiveUrl}\n`); + console.info(`Note: Local or branch test url is not valid, Exiting test execution.\n`); + process.exit(1); + } +} + +async function globalSetup() { + console.info('---- Executing Nala Global setup ----\n'); + + if (process.env.GITHUB_ACTIONS === 'true') { + console.info('---- Running Nala Tests in the GitHub environment ----\n'); + + if (process.env.MILO_LIBS_RUN === 'true') { + await getGitHubMiloLibsBranchLiveUrl(); + } else { + await getGitHubPRBranchLiveUrl(); + } + } else if (process.env.CIRCLECI) { + console.info('---- Running Nala Tests in the CircleCI environment ----\n'); + await getCircleCIBranchLiveUrl(); + } else { + console.info('---- Running Nala Tests in the Local environment ----\n'); + await getLocalBranchLiveUrl(); + } +} + +export default globalSetup; diff --git a/nala/utils/nala.run.js b/nala/utils/nala.run.js new file mode 100644 index 00000000..46b6358f --- /dev/null +++ b/nala/utils/nala.run.js @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ + +import { spawn } from 'child_process'; + +function displayHelp() { + console.log(` + +\x1b[1m\x1b[37m## Nala command:\x1b[0m \x1b[1m\x1b[32mnpm run nala [env] [options]\x1b[0m + +\x1b[1m1] Env:\x1b[0m [\x1b[32mlocal\x1b[0m | \x1b[32mlibs\x1b[0m | \x1b[32mbranch\x1b[0m | \x1b[32mstage\x1b[0m | \x1b[32metc\x1b[0m ] \x1b[3mdefault: local\x1b[0m + +\x1b[1m2] Options:\x1b[0m + + \x1b[33m* browser=\x1b[0m Browser to run the test in + \x1b[33m* device=\x1b[0m Device type to run the test on + \x1b[33m* test=<.test.js>\x1b[0m Specific test file to run (runs all tests in the file) + \x1b[33m* -g, --g=<@tag>\x1b[0m Annotation Tag to filter and run tests by annotation (e.g., @test1, @accordion, @marquee) + \x1b[33m* mode=\x1b[0m Mode (default: headless) + \x1b[33m* config=\x1b[0m Custom configuration file to use (default: Playwright's default) + \x1b[33m* project=\x1b[0m Project configuration (default: mas-live-chromium) + \x1b[33m* milolibs=\x1b[0m Milo library environment (default: none) + \x1b[33m* owner=\x1b[0m repo owner (default owner = adobecom) + +\x1b[1mExamples:\x1b[0m + | \x1b[36mCommand\x1b[0m | \x1b[36mDescription\x1b[0m | + |--------------------------------------------------------|------------------------------------------------------------------------------------| + | npm run nala local | Runs all nala tests on local environment on chrome browser | + | npm run nala local accordion.test.js | Runs only accordion tests on local environment on chrome browser | + | npm run nala local @accordion | Runs only accordion annotated/tagged tests on local environment on chrome browser | + | npm run nala local @accordion browser=firefox | Runs only accordion annotated/tagged tests on local environment on firefox browser | + | npm run nala local mode=ui | Runs all nala tests on local environment in UI mode on chrome browser | + | npm run nala local -g=@accordion | Runs tests annotated with tag i.e @accordion on local env on chrome browser | + | npm run nala local -g=@accordion browser=firefox | Runs tests annotated with tag i.e @accordion on local env on Firefox browser | + | npm run nala owner='' | Runs all nala tests on the specified feature branch for the given repo owner | + +\x1b[1mDebugging:\x1b[0m +----------- + | \x1b[36mCommand\x1b[0m | \x1b[36mDescription\x1b[0m | + |--------------------------------------------------------|------------------------------------------------------------------------------------| + | npm run nala local @test1 mode=debug | Runs @test1 on local environment in debug mode | + +`); +} + +function parseArgs(args) { + const defaultParams = { + env: 'local', + browser: 'chromium', + device: 'desktop', + test: '', + tag: '', + mode: 'headless', + config: '', + project: '', + milolibs: '', + repo: 'mas', + owner: 'adobecom', + }; + + const parsedParams = { ...defaultParams }; + + args.forEach((arg) => { + if (arg.includes('=')) { + const [key, value] = arg.split('='); + parsedParams[key] = value; + } else if (arg.startsWith('-g') || arg.startsWith('--g')) { + const value = arg.includes('=') ? arg.split('=')[1] : args[args.indexOf(arg) + 1]; + parsedParams.tag = value; + } else if (arg.startsWith('@')) { + parsedParams.tag += parsedParams.tag ? ` ${arg.substring(1)}` : arg.substring(1); + } else if (arg.endsWith('.test.js')) { + parsedParams.test = arg; + } else if (arg.endsWith('.config.js')) { + parsedParams.config = arg; + } else if (['ui', 'debug', 'headless', 'headed'].includes(arg)) { + parsedParams.mode = arg; + } else if (arg.startsWith('repo=')) { + const repo = arg.split('=')[1]; + parsedParams.repo = repo || 'mas'; + } else if (arg.startsWith('owner=')) { + const owner = arg.split('=')[1]; + parsedParams.owner = owner || 'adobecom'; + } else { + parsedParams.env = arg; + } + }); + + // Set the project if not provided + if (!parsedParams.project) { + parsedParams.project = `mas-live-${parsedParams.browser}`; + } + + return parsedParams; +} + +function getLocalTestLiveUrl(env, milolibs, repo = 'mas', owner = 'adobecom') { + if (milolibs) { + process.env.MILO_LIBS = `?milolibs=${milolibs}`; + if (env === 'local') { + return 'http://127.0.0.1:3000'; + } if (env === 'libs') { + return 'http://127.0.0.1:6456'; + } + return `https://${env}--${repo}--${owner}.aem.live`; + } + if (env === 'local') { + return 'http://127.0.0.1:3000'; + } if (env === 'libs') { + return 'http://127.0.0.1:6456'; + } + return `https://${env}--${repo}--${owner}.aem.live`; +} + +function buildPlaywrightCommand(parsedParams, localTestLiveUrl) { + const { + browser, device, test, tag, mode, config, project, + } = parsedParams; + + const envVariables = { + ...process.env, + BROWSER: browser, + DEVICE: device, + HEADLESS: mode === 'headless' || mode === 'headed' ? 'true' : 'false', + LOCAL_TEST_LIVE_URL: localTestLiveUrl, + }; + + const command = 'npx playwright test'; + const options = []; + + if (test) { + options.push(test); + } + + options.push(`--project=${project}`); + options.push('--grep-invert nopr'); + + if (tag) { + options.push(`-g "${tag.replace(/,/g, ' ')}"`); + } + + if (mode === 'ui' || mode === 'headed') { + options.push('--headed'); + } else if (mode === 'debug') { + options.push('--debug'); + } + + if (config) { + options.push(`--config=${config}`); + } + + return { finalCommand: `${command} ${options.join(' ')}`, envVariables }; +} + +function runNalaTest() { + const args = process.argv.slice(2); + + if (args.length === 0 || args.includes('help')) { + displayHelp(); + process.exit(0); + } + + const parsedParams = parseArgs(args); + const localTestLiveUrl = getLocalTestLiveUrl(parsedParams.env, parsedParams.milolibs, parsedParams.repo, parsedParams.owner); + const { finalCommand, envVariables } = buildPlaywrightCommand(parsedParams, localTestLiveUrl); + + console.log(`\n Executing nala run command: ${finalCommand}`); + console.log(`\n Using URL: ${localTestLiveUrl}\n`); + console.log(`\n\x1b[1m\x1b[33mExecuting nala run command:\x1b[0m \x1b[32m${finalCommand}\x1b[0m\n\x1b[1m\x1b[33mUsing URL:\x1b[0m \x1b[32m${localTestLiveUrl}\x1b[0m\n`); + + const testProcess = spawn(finalCommand, { stdio: 'inherit', shell: true, env: envVariables }); + + testProcess.on('close', (code) => { + // eslint-disable-next-line no-console + console.log(`Nala tests exited with code ${code}`); + process.exit(code); + }); +} + +if (import.meta.url === new URL(import.meta.url).href) { + runNalaTest(); +} + +export { + displayHelp, + parseArgs, + getLocalTestLiveUrl, + buildPlaywrightCommand, + runNalaTest, +}; diff --git a/package.json b/package.json index 53ae9961..9e87c5a4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "build": "npm run build --workspaces", "studio": "cd studio && npm run proxy & aem up", "studio:qa": "cd studio && npm run proxy:qa & aem up", - "gallery": "cd studio && npm run proxy:publish & aem up" + "gallery": "cd studio && npm run proxy:publish & aem up", + "nala": "node nala/utils/nala.run.js" }, "repository": { "type": "git", @@ -21,6 +22,7 @@ }, "dependencies": { "@adobecom/milo": "github:adobecom/milo#stage", + "axios": "^1.7.4", "@spectrum-css/button": "^13.5.0", "@spectrum-css/link": "^5.2.0", "@spectrum-css/page": "^8.2.0", @@ -62,6 +64,7 @@ "@spectrum-web-components/tooltip": "^0.47.2", "@spectrum-web-components/top-nav": "^0.47.2", "@spectrum-web-components/tray": "^0.47.2", + "@playwright/test": "^1.42.1", "prosemirror-commands": "^1.6.1", "prosemirror-example-setup": "^1.2.3", "prosemirror-keymap": "^1.2.2", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..ee54f539 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,71 @@ +import { devices } from '@playwright/test'; + +// const envs = require('./envs/envs.js'); + +/** + * @see https://playwright.dev/docs/test-configuration + * @type {import('@playwright/test').PlaywrightTestConfig} + */ + +const config = { + testDir: './nala', + outputDir: './test-results', + globalSetup: './nala/utils/global.setup.js', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + testMatch: '**/*.test.js', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 4 : 3, + /* Reporter to use. */ + reporter: process.env.CI + ? [['github'], ['list'], ['./nala/utils/base-reporter.js']] + : [['html', { outputFolder: 'test-html-results' }], ['list'], ['./nala/utils/base-reporter.js']], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 60000, + + trace: 'on-first-retry', + baseURL: process.env.PR_BRANCH_LIVE_URL || (process.env.LOCAL_TEST_LIVE_URL || 'https://main--mas--adobecom.aem.live'), + + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'mas-live-chromium', + use: { ...devices['Desktop Chrome'] }, + bypassCSP: true, + launchOptions: { args: ['--disable-web-security', '--disable-gpu'] }, + }, + + { + name: 'mas-live-firefox', + use: { ...devices['Desktop Firefox'] }, + bypassCSP: true, + }, + { + name: 'mas-live-webkit', + use: { + ...devices['Desktop Safari'], + ignoreHTTPSErrors: true, + }, + bypassCSP: true, + }, + ], +}; + +export default config; \ No newline at end of file diff --git a/pr.run.sh b/pr.run.sh new file mode 100644 index 00000000..8cf4e9f3 --- /dev/null +++ b/pr.run.sh @@ -0,0 +1,59 @@ +#!/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.aem.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 labels to tags 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}" + +cd "$GITHUB_ACTION_PATH" || exit +npm ci +npm install +npx playwright install --with-deps + +echo "*** Running tests on specific projects ***" +npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --project=mas-live-chromium --project=mas-live-firefox --project=mas-live-webkit ${REPORTER} || EXIT_STATUS=$? + + +# Check to determine if the script should exit with an error. +if [ $EXIT_STATUS -ne 0 ]; then + echo "Some tests failed. Exiting with error." + exit $EXIT_STATUS +else + echo "All tests passed successfully." +fi \ No newline at end of file From 75615b8530c5af8508ae8cfb4e6e411dea6d1164 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Thu, 12 Dec 2024 18:36:08 +0100 Subject: [PATCH 02/17] update setup for PRs --- .github/workflows/run-nala.yml | 46 +++++++++++++++++++ .gitignore | 2 + action.yml | 12 ----- nala/utils/pr.run.sh | 84 ++++++++++++++++++++++++++++++++++ pr.run.sh | 59 ------------------------ 5 files changed, 132 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/run-nala.yml delete mode 100644 action.yml create mode 100644 nala/utils/pr.run.sh delete mode 100644 pr.run.sh diff --git a/.github/workflows/run-nala.yml b/.github/workflows/run-nala.yml new file mode 100644 index 00000000..ebe47d13 --- /dev/null +++ b/.github/workflows/run-nala.yml @@ -0,0 +1,46 @@ +name: Run Nala Tests + +on: + push: + branches: + - main + pull_request: + branches: + - 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/.gitignore b/.gitignore index 1f076657..8cab1c4c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ swc.json studio.json *.key *.crt +test-html-results/ +test-results/ # IO Runtime Config config.json diff --git a/action.yml b/action.yml deleted file mode 100644 index 91cad7ea..00000000 --- a/action.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: 'Nala' - -runs: - using: 'composite' - steps: - - name: Set up Node - uses: actions/setup-node@v3 - with: - node-version: lts/* - - - run: $GITHUB_ACTION_PATH/pr.run.sh - shell: bash \ No newline at end of file diff --git a/nala/utils/pr.run.sh b/nala/utils/pr.run.sh new file mode 100644 index 00000000..c36507a8 --- /dev/null +++ b/nala/utils/pr.run.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +TAGS="" +REPORTER="" +EXCLUDE_TAGS="--grep-invert nopr" +EXIT_STATUS=0 + +echo "GITHUB_REF: $GITHUB_REF" +echo "GITHUB_HEAD_REF: $GITHUB_HEAD_REF" + +if [[ "$GITHUB_REF" == refs/pull/* ]]; then + # extract PR number and branch name + PR_NUMBER=$(echo "$GITHUB_REF" | awk -F'/' '{print $3}') + FEATURE_BRANCH="$GITHUB_HEAD_REF" +elif [[ "$GITHUB_REF" == refs/heads/* ]]; then + # extract branch name from GITHUB_REF + FEATURE_BRANCH=$(echo "$GITHUB_REF" | awk -F'/' '{print $3}') +else + echo "Unknown reference format" +fi + +# Replace "/" characters in the feature branch name with "-" +FEATURE_BRANCH=$(echo "$FEATURE_BRANCH" | sed 's/\//-/g') + +echo "PR Number: ${PR_NUMBER:-"N/A"}" +echo "Feature Branch Name: $FEATURE_BRANCH" + +repository=${GITHUB_REPOSITORY} +repoParts=(${repository//\// }) +toRepoOrg=${repoParts[0]} +toRepoName=${repoParts[1]} + +prRepo=${prRepo:-$toRepoName} +prOrg=${prOrg:-$toRepoOrg} + +# TODO ADD HLX5 SUPPORT +PR_BRANCH_LIVE_URL_GH="https://$FEATURE_BRANCH--$prRepo--$prOrg.aem.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" + + +# 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 branch: $FEATURE_BRANCH " +echo "Tags : ${TAGS:-"No @tags or annotations on this PR"}" +echo "Run Command : npx playwright test ${TAGS} ${EXCLUDE_TAGS} ${REPORTER}" +echo -e "\n" +echo "*******************************" + +# 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 --project=milo-live-webkit ${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 diff --git a/pr.run.sh b/pr.run.sh deleted file mode 100644 index 8cf4e9f3..00000000 --- a/pr.run.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/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.aem.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 labels to tags 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}" - -cd "$GITHUB_ACTION_PATH" || exit -npm ci -npm install -npx playwright install --with-deps - -echo "*** Running tests on specific projects ***" -npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --project=mas-live-chromium --project=mas-live-firefox --project=mas-live-webkit ${REPORTER} || EXIT_STATUS=$? - - -# Check to determine if the script should exit with an error. -if [ $EXIT_STATUS -ne 0 ]; then - echo "Some tests failed. Exiting with error." - exit $EXIT_STATUS -else - echo "All tests passed successfully." -fi \ No newline at end of file From c52d61a7e5d459bbaa14d351633ea472da277dff Mon Sep 17 00:00:00 2001 From: cod23684 Date: Thu, 12 Dec 2024 18:52:42 +0100 Subject: [PATCH 03/17] run lint --- nala/libs/baseurl.js | 2 +- nala/libs/webutil.js | 34 ++--- nala/studio/studio.page.js | 52 +++---- nala/studio/studio.spec.js | 44 +++--- nala/studio/studio.test.js | 75 ++++++----- nala/utils/base-reporter.js | 2 +- nala/utils/nala.run.js | 261 +++++++++++++++++++----------------- 7 files changed, 248 insertions(+), 222 deletions(-) diff --git a/nala/libs/baseurl.js b/nala/libs/baseurl.js index 4606d244..8f19a059 100644 --- a/nala/libs/baseurl.js +++ b/nala/libs/baseurl.js @@ -1,4 +1,4 @@ -/* eslint-disable */ + import pkg from 'axios'; const { head } = pkg; diff --git a/nala/libs/webutil.js b/nala/libs/webutil.js index 72ac22b4..a9c630f0 100644 --- a/nala/libs/webutil.js +++ b/nala/libs/webutil.js @@ -1,6 +1,6 @@ -/* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ -/* eslint-disable max-len */ + + + // eslint-disable-next-line import/no-import-module-exports import { expect } from '@playwright/test'; @@ -120,7 +120,7 @@ exports.WebUtil = class WebUtil { * @param {Object} cssProps - The CSS properties and expected values to verify. * @returns {Boolean} - True if all CSS properties match the expected values, false otherwise. */ - // eslint-disable-next-line no-underscore-dangle + async verifyCSS_(locator, cssProps) { this.locator = locator; let result = true; @@ -177,7 +177,7 @@ exports.WebUtil = class WebUtil { * @param {Object} attProps - The attribute properties and expected values to verify. * @returns {Boolean} - True if all attribute properties match the expected values, false otherwise. */ - // eslint-disable-next-line no-underscore-dangle + async verifyAttributes_(locator, attProps) { this.locator = locator; let result = true; @@ -215,7 +215,7 @@ exports.WebUtil = class WebUtil { async scrollPage(direction, speed) { const scroll = async (args) => { const { dir, spd } = args; - // eslint-disable-next-line no-promise-executor-return + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const scrollHeight = () => document.body.scrollHeight; const start = dir === 'down' ? 0 : scrollHeight(); @@ -225,7 +225,7 @@ exports.WebUtil = class WebUtil { console.error(start, shouldStop(start), increment); for (let i = start; !shouldStop(i); i += increment) { window.scrollTo(0, i); - // eslint-disable-next-line no-await-in-loop + await delay(delayTime); } }; @@ -304,15 +304,15 @@ exports.WebUtil = class WebUtil { || url.includes('sstats.adobe.com/ee/or2/v1/collect')) { networklogs.push(url); const firstEvent = route.request().postDataJSON().events[0]; - // eslint-disable-next-line no-underscore-dangle + if (firstEvent.data._adobe_corpnew.digitalData.primaryEvent) { - // eslint-disable-next-line no-underscore-dangle + networklogs.push(JSON.stringify(firstEvent.data._adobe_corpnew.digitalData.primaryEvent)); } - // eslint-disable-next-line no-underscore-dangle + if (firstEvent.data._adobe_corpnew.digitalData.search) { - // eslint-disable-next-line no-underscore-dangle + networklogs.push(JSON.stringify(firstEvent.data._adobe_corpnew.digitalData.search)); } } @@ -332,7 +332,7 @@ exports.WebUtil = class WebUtil { * @param {string} project - The project identifier, defaulting to 'milo' if not provided. * @returns {string} - A string formatted as 'gnav||nopzn|nopzn'. */ - // eslint-disable-next-line class-methods-use-this + async getGnavDaalh(project = milo) { return `gnav|${project}|nopzn|nopzn`; } @@ -344,7 +344,7 @@ exports.WebUtil = class WebUtil { * @param {string} pznFileName - Manifest filename, which is sliced to its first 20 characters. * @returns {string} - A string formatted as 'gnav|||'. */ - // eslint-disable-next-line class-methods-use-this, default-param-last + async getPznGnavDaalh(project = milo, pznExpName, pznFileName) { const slicedExpName = pznExpName.slice(0, 15); const slicedFileName = pznFileName.slice(0, 15); @@ -356,7 +356,7 @@ exports.WebUtil = class WebUtil { * @param {number|string} counter - A counter value used to generate the section identifier. * @returns {string} - A string formatted as 's'. */ - // eslint-disable-next-line class-methods-use-this + async getSectionDaalh(counter) { return `s${counter}`; } @@ -369,7 +369,7 @@ exports.WebUtil = class WebUtil { * @param {string} pznExpName - Manifest filename, which is sliced to its first 20 characters. * @returns {string} - A string formatted as 'b|||'. */ - // eslint-disable-next-line class-methods-use-this + async getPznBlockDaalh(blockName, counter, pznExpName, pznFileName) { const slicedBlockName = blockName.slice(0, 20); const slicedExpName = pznExpName.slice(0, 15); @@ -385,7 +385,7 @@ exports.WebUtil = class WebUtil { * @param {string} [pzntext='nopzn'] - The pzntext to use when pzn is true, sliced to its first 15 characters. * @returns {string} - A formatted string. */ - // eslint-disable-next-line class-methods-use-this + async getBlockDaalh(blockName, counter, pzn = false, pzntext = 'nopzn') { const slicedBlockName = blockName.slice(0, 20); const slicedPzntext = pzntext.slice(0, 15); @@ -403,7 +403,7 @@ exports.WebUtil = class WebUtil { * @param {boolean} [pzn=false] - boolean parameter, defaulting to false.(for personalization) * @returns {string} - A string formatted as '---'. */ - // eslint-disable-next-line class-methods-use-this + async getLinkDaall(linkText, counter, lastHeaderText, pzn = false) { const cleanAndSliceText = (text) => text ?.replace(/[^\w\s]+/g, ' ') diff --git a/nala/studio/studio.page.js b/nala/studio/studio.page.js index a8ab9981..900db83e 100644 --- a/nala/studio/studio.page.js +++ b/nala/studio/studio.page.js @@ -1,31 +1,35 @@ export default class StudioPage { - constructor(page) { - this.page = page; + constructor(page) { + this.page = page; - this.searchInput = page.locator('sp-search input'); - this.searchIcon = page.locator('sp-search sp-icon-magnify'); - this.filter = page.locator('sp-action-button[label="Filter"]'); - this.topFolder = page.locator('sp-picker[label="TopFolder"] > button'); - this.renderView = page.locator('render-view'); - this.suggestedCard = page.locator('merch-card[variant="ccd-suggested"]'); - this.sliceCard = page.locator('merch-card[variant="ccd-slice"]'); - this.sliceCardWide = page.locator('merch-card[variant="ccd-slice"][size="wide"]'); + this.searchInput = page.locator('sp-search input'); + this.searchIcon = page.locator('sp-search sp-icon-magnify'); + this.filter = page.locator('sp-action-button[label="Filter"]'); + this.topFolder = page.locator('sp-picker[label="TopFolder"] > button'); + this.renderView = page.locator('render-view'); + this.suggestedCard = page.locator( + 'merch-card[variant="ccd-suggested"]', + ); + this.sliceCard = page.locator('merch-card[variant="ccd-slice"]'); + this.sliceCardWide = page.locator( + 'merch-card[variant="ccd-slice"][size="wide"]', + ); + } - } + async getCard(id, cardType) { + const cardVariant = { + suggested: this.suggestedCard, + slice: this.sliceCard, + 'slice-wide': this.sliceCardWide, + }; - async getCard(id, cardType) { - const cardVariant = { - suggested: this.suggestedCard, - slice: this.sliceCard, - 'slice-wide': this.sliceCardWide, - }; + const card = cardVariant[cardType]; + if (!card) { + throw new Error(`Invalid card type: ${cardType}`); + } - const card = cardVariant[cardType]; - if (!card) { - throw new Error(`Invalid card type: ${cardType}`); + return card.filter({ + has: this.page.locator(`aem-fragment[fragment="${id}"]`), + }); } - - return card.filter({ has: this.page.locator(`aem-fragment[fragment="${id}"]`) }); - } - } diff --git a/nala/studio/studio.spec.js b/nala/studio/studio.spec.js index cb57158d..0dced7c5 100644 --- a/nala/studio/studio.spec.js +++ b/nala/studio/studio.spec.js @@ -1,24 +1,24 @@ export default { - FeatureName: 'M@S Studio', - features: [ - { - tcid: '0', - name: '@studio-direct-search', - path: '/studio.html', - data: { - cardid: '206a8742-0289-4196-92d4-ced99ec4191e', - title: 'Automation Test Card', - eyebrow: 'DO NOT EDIT', - description: 'MAS repo validation card for Nala tests.', - price: 'US$22.99/mo', - strikethroughPrice: 'US$37.99/mo', - cta: 'Buy now', - offerid: '30404A88D89A328584307175B8B27616', - linkText: 'See terms', - linkUrl: '', - }, - browserParams: '#query=', - tags: '@mas @mas-studio @smoke @regression', - }, - ], + FeatureName: 'M@S Studio', + features: [ + { + tcid: '0', + name: '@studio-direct-search', + path: '/studio.html', + data: { + cardid: '206a8742-0289-4196-92d4-ced99ec4191e', + title: 'Automation Test Card', + eyebrow: 'DO NOT EDIT', + description: 'MAS repo validation card for Nala tests.', + price: 'US$22.99/mo', + strikethroughPrice: 'US$37.99/mo', + cta: 'Buy now', + offerid: '30404A88D89A328584307175B8B27616', + linkText: 'See terms', + linkUrl: '', + }, + browserParams: '#query=', + tags: '@mas @mas-studio @smoke @regression', + }, + ], }; diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index 9f32b917..2625ab86 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -9,42 +9,49 @@ let studio; const { features } = studiopkg; test.beforeEach(async ({ page, browserName }) => { - test.skip(browserName !== 'chromium', 'Not supported to run on multiple browsers.'); - studio = new StudioPage(page); + test.skip( + browserName !== 'chromium', + 'Not supported to run on multiple browsers.', + ); + studio = new StudioPage(page); }); - test.describe('M@S Studio feature test suite', () => { - // @studio-direct-search - Validate direct search feature in mas studio - test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { - test.slow(); - const { data } = features[0]; - const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`; - console.info('[Test Page]: ', testPage); - - await test.step('step-1: Log in to MAS studio', async () => { - await page.goto(testPage); - await page.waitForURL('**/auth.services.adobe.com/en_US/index.html**/'); - features[0].url = 'https://main--mas--adobecom.aem.live/studio.html'; - await ims.fillOutSignInForm(features[0], page); - await expect(async () => { - const response = await page.request.get(features[0].url); - expect(response.status()).toBe(200); - }).toPass(); - await page.waitForLoadState('domcontentloaded'); - }); - - await test.step('step-2: Go to MAS Studio test page', async () => { - await page.goto(testPage); - await page.waitForLoadState('domcontentloaded'); + // @studio-direct-search - Validate direct search feature in mas studio + test(`${features[0].name},${features[0].tags}`, async ({ + page, + baseURL, + }) => { + test.slow(); + const { data } = features[0]; + const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`; + console.info('[Test Page]: ', testPage); + + await test.step('step-1: Log in to MAS studio', async () => { + await page.goto(testPage); + await page.waitForURL( + '**/auth.services.adobe.com/en_US/index.html**/', + ); + features[0].url = + 'https://main--mas--adobecom.aem.live/studio.html'; + await ims.fillOutSignInForm(features[0], page); + await expect(async () => { + const response = await page.request.get(features[0].url); + expect(response.status()).toBe(200); + }).toPass(); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-2: Go to MAS Studio test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-3: Validate search results', async () => { + await expect(await studio.renderView).toBeVisible(); + + const cards = await studio.renderView.locator('merch-card'); + expect(await cards.count()).toBe(1); + }); }); - - await test.step('step-3: Validate search results', async () => { - await expect(await studio.renderView).toBeVisible(); - - const cards = await studio.renderView.locator('merch-card'); - expect(await cards.count()).toBe(1); - }); - }); - }); diff --git a/nala/utils/base-reporter.js b/nala/utils/base-reporter.js index 4ed0718d..ce97b984 100644 --- a/nala/utils/base-reporter.js +++ b/nala/utils/base-reporter.js @@ -187,7 +187,7 @@ export default class BaseReporter { return { name, tags, url, browser, env, branch, repo}; } - // eslint-disable-next-line class-methods-use-this, no-empty-function + async persistData() {} printPersistingOption() { diff --git a/nala/utils/nala.run.js b/nala/utils/nala.run.js index 46b6358f..c289d363 100644 --- a/nala/utils/nala.run.js +++ b/nala/utils/nala.run.js @@ -1,11 +1,9 @@ #!/usr/bin/env node -/* eslint-disable no-console */ - import { spawn } from 'child_process'; function displayHelp() { - console.log(` + console.log(` \x1b[1m\x1b[37m## Nala command:\x1b[0m \x1b[1m\x1b[32mnpm run nala [env] [options]\x1b[0m @@ -45,147 +43,164 @@ function displayHelp() { } function parseArgs(args) { - const defaultParams = { - env: 'local', - browser: 'chromium', - device: 'desktop', - test: '', - tag: '', - mode: 'headless', - config: '', - project: '', - milolibs: '', - repo: 'mas', - owner: 'adobecom', - }; - - const parsedParams = { ...defaultParams }; - - args.forEach((arg) => { - if (arg.includes('=')) { - const [key, value] = arg.split('='); - parsedParams[key] = value; - } else if (arg.startsWith('-g') || arg.startsWith('--g')) { - const value = arg.includes('=') ? arg.split('=')[1] : args[args.indexOf(arg) + 1]; - parsedParams.tag = value; - } else if (arg.startsWith('@')) { - parsedParams.tag += parsedParams.tag ? ` ${arg.substring(1)}` : arg.substring(1); - } else if (arg.endsWith('.test.js')) { - parsedParams.test = arg; - } else if (arg.endsWith('.config.js')) { - parsedParams.config = arg; - } else if (['ui', 'debug', 'headless', 'headed'].includes(arg)) { - parsedParams.mode = arg; - } else if (arg.startsWith('repo=')) { - const repo = arg.split('=')[1]; - parsedParams.repo = repo || 'mas'; - } else if (arg.startsWith('owner=')) { - const owner = arg.split('=')[1]; - parsedParams.owner = owner || 'adobecom'; - } else { - parsedParams.env = arg; + const defaultParams = { + env: 'local', + browser: 'chromium', + device: 'desktop', + test: '', + tag: '', + mode: 'headless', + config: '', + project: '', + milolibs: '', + repo: 'mas', + owner: 'adobecom', + }; + + const parsedParams = { ...defaultParams }; + + args.forEach((arg) => { + if (arg.includes('=')) { + const [key, value] = arg.split('='); + parsedParams[key] = value; + } else if (arg.startsWith('-g') || arg.startsWith('--g')) { + const value = arg.includes('=') + ? arg.split('=')[1] + : args[args.indexOf(arg) + 1]; + parsedParams.tag = value; + } else if (arg.startsWith('@')) { + parsedParams.tag += parsedParams.tag + ? ` ${arg.substring(1)}` + : arg.substring(1); + } else if (arg.endsWith('.test.js')) { + parsedParams.test = arg; + } else if (arg.endsWith('.config.js')) { + parsedParams.config = arg; + } else if (['ui', 'debug', 'headless', 'headed'].includes(arg)) { + parsedParams.mode = arg; + } else if (arg.startsWith('repo=')) { + const repo = arg.split('=')[1]; + parsedParams.repo = repo || 'mas'; + } else if (arg.startsWith('owner=')) { + const owner = arg.split('=')[1]; + parsedParams.owner = owner || 'adobecom'; + } else { + parsedParams.env = arg; + } + }); + + // Set the project if not provided + if (!parsedParams.project) { + parsedParams.project = `mas-live-${parsedParams.browser}`; } - }); - - // Set the project if not provided - if (!parsedParams.project) { - parsedParams.project = `mas-live-${parsedParams.browser}`; - } - return parsedParams; + return parsedParams; } function getLocalTestLiveUrl(env, milolibs, repo = 'mas', owner = 'adobecom') { - if (milolibs) { - process.env.MILO_LIBS = `?milolibs=${milolibs}`; + if (milolibs) { + process.env.MILO_LIBS = `?milolibs=${milolibs}`; + if (env === 'local') { + return 'http://127.0.0.1:3000'; + } + if (env === 'libs') { + return 'http://127.0.0.1:6456'; + } + return `https://${env}--${repo}--${owner}.aem.live`; + } if (env === 'local') { - return 'http://127.0.0.1:3000'; - } if (env === 'libs') { - return 'http://127.0.0.1:6456'; + return 'http://127.0.0.1:3000'; + } + if (env === 'libs') { + return 'http://127.0.0.1:6456'; } return `https://${env}--${repo}--${owner}.aem.live`; - } - if (env === 'local') { - return 'http://127.0.0.1:3000'; - } if (env === 'libs') { - return 'http://127.0.0.1:6456'; - } - return `https://${env}--${repo}--${owner}.aem.live`; } function buildPlaywrightCommand(parsedParams, localTestLiveUrl) { - const { - browser, device, test, tag, mode, config, project, - } = parsedParams; - - const envVariables = { - ...process.env, - BROWSER: browser, - DEVICE: device, - HEADLESS: mode === 'headless' || mode === 'headed' ? 'true' : 'false', - LOCAL_TEST_LIVE_URL: localTestLiveUrl, - }; - - const command = 'npx playwright test'; - const options = []; - - if (test) { - options.push(test); - } - - options.push(`--project=${project}`); - options.push('--grep-invert nopr'); - - if (tag) { - options.push(`-g "${tag.replace(/,/g, ' ')}"`); - } - - if (mode === 'ui' || mode === 'headed') { - options.push('--headed'); - } else if (mode === 'debug') { - options.push('--debug'); - } - - if (config) { - options.push(`--config=${config}`); - } - - return { finalCommand: `${command} ${options.join(' ')}`, envVariables }; -} + const { browser, device, test, tag, mode, config, project } = parsedParams; -function runNalaTest() { - const args = process.argv.slice(2); + const envVariables = { + ...process.env, + BROWSER: browser, + DEVICE: device, + HEADLESS: mode === 'headless' || mode === 'headed' ? 'true' : 'false', + LOCAL_TEST_LIVE_URL: localTestLiveUrl, + }; + + const command = 'npx playwright test'; + const options = []; + + if (test) { + options.push(test); + } - if (args.length === 0 || args.includes('help')) { - displayHelp(); - process.exit(0); - } + options.push(`--project=${project}`); + options.push('--grep-invert nopr'); - const parsedParams = parseArgs(args); - const localTestLiveUrl = getLocalTestLiveUrl(parsedParams.env, parsedParams.milolibs, parsedParams.repo, parsedParams.owner); - const { finalCommand, envVariables } = buildPlaywrightCommand(parsedParams, localTestLiveUrl); + if (tag) { + options.push(`-g "${tag.replace(/,/g, ' ')}"`); + } - console.log(`\n Executing nala run command: ${finalCommand}`); - console.log(`\n Using URL: ${localTestLiveUrl}\n`); - console.log(`\n\x1b[1m\x1b[33mExecuting nala run command:\x1b[0m \x1b[32m${finalCommand}\x1b[0m\n\x1b[1m\x1b[33mUsing URL:\x1b[0m \x1b[32m${localTestLiveUrl}\x1b[0m\n`); + if (mode === 'ui' || mode === 'headed') { + options.push('--headed'); + } else if (mode === 'debug') { + options.push('--debug'); + } - const testProcess = spawn(finalCommand, { stdio: 'inherit', shell: true, env: envVariables }); + if (config) { + options.push(`--config=${config}`); + } + + return { finalCommand: `${command} ${options.join(' ')}`, envVariables }; +} + +function runNalaTest() { + const args = process.argv.slice(2); + + if (args.length === 0 || args.includes('help')) { + displayHelp(); + process.exit(0); + } - testProcess.on('close', (code) => { - // eslint-disable-next-line no-console - console.log(`Nala tests exited with code ${code}`); - process.exit(code); - }); + const parsedParams = parseArgs(args); + const localTestLiveUrl = getLocalTestLiveUrl( + parsedParams.env, + parsedParams.milolibs, + parsedParams.repo, + parsedParams.owner, + ); + const { finalCommand, envVariables } = buildPlaywrightCommand( + parsedParams, + localTestLiveUrl, + ); + + console.log(`\n Executing nala run command: ${finalCommand}`); + console.log(`\n Using URL: ${localTestLiveUrl}\n`); + console.log( + `\n\x1b[1m\x1b[33mExecuting nala run command:\x1b[0m \x1b[32m${finalCommand}\x1b[0m\n\x1b[1m\x1b[33mUsing URL:\x1b[0m \x1b[32m${localTestLiveUrl}\x1b[0m\n`, + ); + + const testProcess = spawn(finalCommand, { + stdio: 'inherit', + shell: true, + env: envVariables, + }); + + testProcess.on('close', (code) => { + console.log(`Nala tests exited with code ${code}`); + process.exit(code); + }); } if (import.meta.url === new URL(import.meta.url).href) { - runNalaTest(); + runNalaTest(); } export { - displayHelp, - parseArgs, - getLocalTestLiveUrl, - buildPlaywrightCommand, - runNalaTest, + displayHelp, + parseArgs, + getLocalTestLiveUrl, + buildPlaywrightCommand, + runNalaTest, }; From 7c162e3204729e876d661d8b7059fe8d2d082f68 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Thu, 12 Dec 2024 18:53:40 +0100 Subject: [PATCH 04/17] sync package-lock.json --- package-lock.json | 126 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e121e68..f3e3e27f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ ], "dependencies": { "@adobecom/milo": "github:adobecom/milo#stage", + "@playwright/test": "^1.42.1", "@spectrum-css/button": "^13.5.0", "@spectrum-css/link": "^5.2.0", "@spectrum-css/page": "^8.2.0", @@ -53,6 +54,7 @@ "@spectrum-web-components/tooltip": "^0.47.2", "@spectrum-web-components/top-nav": "^0.47.2", "@spectrum-web-components/tray": "^0.47.2", + "axios": "^1.7.4", "prosemirror-commands": "^1.6.1", "prosemirror-example-setup": "^1.2.3", "prosemirror-keymap": "^1.2.2", @@ -564,6 +566,20 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@preact/signals": { "version": "1.0.4", "license": "MIT", @@ -2368,6 +2384,11 @@ "lodash": "^4.17.14" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/axe-core": { "version": "4.10.2", "dev": true, @@ -2376,6 +2397,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, @@ -2750,6 +2781,17 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/command-line-args": { "version": "5.2.1", "dev": true, @@ -2963,6 +3005,14 @@ "node": ">=8" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "dev": true, @@ -3640,6 +3690,38 @@ "version": "5.2.1", "license": "W3C" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fresh": { "version": "0.5.2", "dev": true, @@ -3658,6 +3740,19 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "dev": true, @@ -4786,7 +4881,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4794,7 +4888,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5303,6 +5396,34 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/portfinder": { "version": "1.0.32", "dev": true, @@ -5878,7 +5999,6 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "dev": true, "license": "MIT" }, "node_modules/prr": { From 21bb1810d104c1805de3039534782a1852621463 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Thu, 12 Dec 2024 18:59:06 +0100 Subject: [PATCH 05/17] fix nala project names --- nala/utils/pr.run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nala/utils/pr.run.sh b/nala/utils/pr.run.sh index c36507a8..ca3f0287 100644 --- a/nala/utils/pr.run.sh +++ b/nala/utils/pr.run.sh @@ -73,7 +73,7 @@ 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 --project=milo-live-webkit ${REPORTER} || EXIT_STATUS=$? +npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --project=mas-live-chromium --project=mas-live-firefox --project=mas-live-webkit ${REPORTER} || EXIT_STATUS=$? # Check if tests passed or failed if [ $EXIT_STATUS -ne 0 ]; then From e7f079f0c48eed188b9fee77dd15a416382255ce Mon Sep 17 00:00:00 2001 From: cod23684 Date: Fri, 13 Dec 2024 13:14:36 +0100 Subject: [PATCH 06/17] fix test --- nala/studio/studio.spec.js | 2 +- nala/studio/studio.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nala/studio/studio.spec.js b/nala/studio/studio.spec.js index 0dced7c5..2f3fe653 100644 --- a/nala/studio/studio.spec.js +++ b/nala/studio/studio.spec.js @@ -18,7 +18,7 @@ export default { linkUrl: '', }, browserParams: '#query=', - tags: '@mas @mas-studio @smoke @regression', + tags: '@mas-studio', }, ], }; diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index 2625ab86..1218e932 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -33,7 +33,7 @@ test.describe('M@S Studio feature test suite', () => { '**/auth.services.adobe.com/en_US/index.html**/', ); features[0].url = - 'https://main--mas--adobecom.aem.live/studio.html'; + `${baseURL}/studio.html`; await ims.fillOutSignInForm(features[0], page); await expect(async () => { const response = await page.request.get(features[0].url); From 9e5cc1792349b84c1f837748b221c0d3107ce4c6 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Fri, 13 Dec 2024 14:12:16 +0100 Subject: [PATCH 07/17] add upload artifacts on failure --- .github/workflows/run-nala.yml | 8 ++++++++ nala/studio/studio.test.js | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-nala.yml b/.github/workflows/run-nala.yml index ebe47d13..4029ca91 100644 --- a/.github/workflows/run-nala.yml +++ b/.github/workflows/run-nala.yml @@ -44,3 +44,11 @@ jobs: GITHUB_ACTION_PATH: ${{ github.workspace }} IMS_EMAIL: ${{ secrets.IMS_EMAIL }} IMS_PASS: ${{ secrets.IMS_PASS }} + + - name: Upload screenshots + uses: actions/upload-artifact@latest + if: failure() + with: + name: screenshots-studio + path: screenshots/studio + retention-days: 7 diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index 1218e932..b157e943 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -1,12 +1,12 @@ import { expect, test } from '@playwright/test'; -import studiopkg from './studio.spec.js'; +import studioSpec from './studio.spec.js'; import StudioPage from './studio.page.js'; import ims from '../libs/imslogin.js'; const miloLibs = process.env.MILO_LIBS || ''; let studio; -const { features } = studiopkg; +const { features } = studioSpec; test.beforeEach(async ({ page, browserName }) => { test.skip( From 65518ca27209debf6671c00a6bfa3981e973208e Mon Sep 17 00:00:00 2001 From: cod23684 Date: Fri, 13 Dec 2024 14:57:05 +0100 Subject: [PATCH 08/17] change version --- .github/workflows/run-nala.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-nala.yml b/.github/workflows/run-nala.yml index 4029ca91..137172dd 100644 --- a/.github/workflows/run-nala.yml +++ b/.github/workflows/run-nala.yml @@ -46,7 +46,7 @@ jobs: IMS_PASS: ${{ secrets.IMS_PASS }} - name: Upload screenshots - uses: actions/upload-artifact@latest + uses: actions/upload-artifact@v3 if: failure() with: name: screenshots-studio From 56b227aeaa0390330a95317f2fcb809ae791d6e2 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Fri, 13 Dec 2024 15:09:27 +0100 Subject: [PATCH 09/17] debug failure --- nala/libs/screenshot/take.js | 25 +++++++++++++++++++++++++ nala/libs/screenshot/utils.js | 12 ++++++++++++ nala/studio/studio.test.js | 12 ++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 nala/libs/screenshot/take.js create mode 100644 nala/libs/screenshot/utils.js diff --git a/nala/libs/screenshot/take.js b/nala/libs/screenshot/take.js new file mode 100644 index 00000000..6e771b50 --- /dev/null +++ b/nala/libs/screenshot/take.js @@ -0,0 +1,25 @@ +/** + * Take a screenshot of a page + * @param {Page} page - The page object + * @param {string} folderPath - The folder path to save the screenshot, e.g., screenshots/milo + * @param {string} fileName - The file name of the screenshot + * @param {object} options - The screenshot options, see https://playwright.dev/docs/api/class-page#page-screenshot + * @returns {object} The screenshot result +*/ +async function take(page, folderPath, fileName, options = {}) { + const urls = []; + const result = {}; + const name = `${folderPath}/${fileName}.png`; + urls.push(page.url()); + options.path = name; + if (options.selector) { + await page.locator(options.selector).screenshot(options); + } else { + await page.screenshot(options); + } + result.a = name; + result.urls = urls.join(' | '); + return result; +} + +export default { take }; diff --git a/nala/libs/screenshot/utils.js b/nala/libs/screenshot/utils.js new file mode 100644 index 00000000..3698de16 --- /dev/null +++ b/nala/libs/screenshot/utils.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line import/no-extraneous-dependencies, import/no-import-module-exports, import/extensions +const fs = require('fs'); +const path = require('path'); + +const ALLOWED_BASE_DIRECTORY = 'screenshots'; + +function writeResultsToFile(folderPath, testInfo, results) { + const resultFilePath = `${folderPath}/results-${testInfo.workerIndex}.json`; + fs.writeFileSync(validatePath(resultFilePath, { forWriting: true }), JSON.stringify(results, null, 2)); +} + +module.exports = { compareScreenshots, writeResultsToFile, validatePath }; diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index b157e943..1ec036bc 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -2,8 +2,10 @@ import { expect, test } from '@playwright/test'; import studioSpec from './studio.spec.js'; import StudioPage from './studio.page.js'; import ims from '../libs/imslogin.js'; +import take from '../libs/screenshot/take.js' const miloLibs = process.env.MILO_LIBS || ''; +const folderPath = 'screenshots/studio'; let studio; const { features } = studioSpec; @@ -22,6 +24,8 @@ test.describe('M@S Studio feature test suite', () => { page, baseURL, }) => { + const name = `${features[0].name}`; + test.slow(); const { data } = features[0]; const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`; @@ -29,6 +33,14 @@ test.describe('M@S Studio feature test suite', () => { await test.step('step-1: Log in to MAS studio', async () => { await page.goto(testPage); + page.waitForTimeout(5000); + const result = await take.take( + page, + folderPath, + name, + ); + // writeResultsToFile(folderPath, testInfo, result); + await page.waitForURL( '**/auth.services.adobe.com/en_US/index.html**/', ); From 2f7670fa1eb444439dd0e524be9bb95c72c48302 Mon Sep 17 00:00:00 2001 From: cod23684 Date: Fri, 13 Dec 2024 19:04:15 +0100 Subject: [PATCH 10/17] add locators and more tests --- nala/libs/screenshot/utils.js | 12 ---- nala/studio/studio.page.js | 38 ++++++++++++ nala/studio/studio.spec.js | 22 +++++++ nala/studio/studio.test.js | 112 +++++++++++++++++++++++++--------- 4 files changed, 143 insertions(+), 41 deletions(-) delete mode 100644 nala/libs/screenshot/utils.js diff --git a/nala/libs/screenshot/utils.js b/nala/libs/screenshot/utils.js deleted file mode 100644 index 3698de16..00000000 --- a/nala/libs/screenshot/utils.js +++ /dev/null @@ -1,12 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies, import/no-import-module-exports, import/extensions -const fs = require('fs'); -const path = require('path'); - -const ALLOWED_BASE_DIRECTORY = 'screenshots'; - -function writeResultsToFile(folderPath, testInfo, results) { - const resultFilePath = `${folderPath}/results-${testInfo.workerIndex}.json`; - fs.writeFileSync(validatePath(resultFilePath, { forWriting: true }), JSON.stringify(results, null, 2)); -} - -module.exports = { compareScreenshots, writeResultsToFile, validatePath }; diff --git a/nala/studio/studio.page.js b/nala/studio/studio.page.js index 900db83e..d46a5383 100644 --- a/nala/studio/studio.page.js +++ b/nala/studio/studio.page.js @@ -7,6 +7,7 @@ export default class StudioPage { this.filter = page.locator('sp-action-button[label="Filter"]'); this.topFolder = page.locator('sp-picker[label="TopFolder"] > button'); this.renderView = page.locator('render-view'); + this.editorPanel = page.locator('editor-panel'); this.suggestedCard = page.locator( 'merch-card[variant="ccd-suggested"]', ); @@ -14,6 +15,43 @@ export default class StudioPage { this.sliceCardWide = page.locator( 'merch-card[variant="ccd-slice"][size="wide"]', ); + this.price = page.locator('span[data-template="price"]'); + this.priceStrikethrough = page.locator( + 'span[data-template="strikethrough"]', + ); + this.cardIcon = page.locator('merch-icon'); + this.cardBadge = page.locator('.ccd-slice-badge'); + // Editor panel fields + this.editorTitle = page.locator('#card-title'); + // suggested cards + this.suggestedCard = page.locator( + 'merch-card[variant="ccd-suggested"]', + ); + this.suggestedCardTitle = this.page.locator('h3[slot="heading-xs"]'); + this.suggestedCardEyebrow = page.locator('h4[slot="detail-s"]'); + this.suggestedCardDescription = page + .locator('div[slot="body-xs"] p') + .first(); + this.suggestedCardLegalLink = page.locator('div[slot="body-xs"] p > a'); + this.suggestedCardPrice = page.locator('p[slot="price"]'); + this.suggestedCardCTA = page.locator('div[slot="cta"] > sp-button'); + this.suggestedCardCTALink = page.locator( + 'div[slot="cta"] a[is="checkout-link"]', + ); + // slice cards + this.sliceCard = page.locator('merch-card[variant="ccd-slice"]'); + this.sliceCardWide = page.locator( + 'merch-card[variant="ccd-slice"][size="wide"]', + ); + this.sliceCardImage = page.locator('div[slot="image"] img'); + this.sliceCardDescription = page + .locator('div[slot="body-s"] p > strong') + .first(); + this.sliceCardLegalLink = page.locator('div[slot="body-s"] p > a'); + this.sliceCardCTA = page.locator('div[slot="footer"] > sp-button'); + this.sliceCardCTALink = page.locator( + 'div[slot="footer"] a[is="checkout-link"]', + ); } async getCard(id, cardType) { diff --git a/nala/studio/studio.spec.js b/nala/studio/studio.spec.js index 2f3fe653..b40e8bf9 100644 --- a/nala/studio/studio.spec.js +++ b/nala/studio/studio.spec.js @@ -20,5 +20,27 @@ export default { browserParams: '#query=', tags: '@mas-studio', }, + { + tcid: '1', + name: '@studio-search-field', + path: '/studio.html', + data: { + cardid: '206a8742-0289-4196-92d4-ced99ec4191e', + }, + browserParams: '#path=nala', + tags: '@mas-studio', + }, + { + tcid: '2', + name: '@studio-edit-title', + path: '/studio.html', + data: { + cardid: '206a8742-0289-4196-92d4-ced99ec4191e', + title: 'Automation Test Card', + newTitle: 'Change title', + }, + browserParams: '#query=', + tags: '@mas-studio', + }, ], }; diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index 1ec036bc..fad54202 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -1,21 +1,29 @@ import { expect, test } from '@playwright/test'; -import studioSpec from './studio.spec.js'; +import StudioSpec from './studio.spec.js'; import StudioPage from './studio.page.js'; import ims from '../libs/imslogin.js'; -import take from '../libs/screenshot/take.js' +const { features } = StudioSpec; const miloLibs = process.env.MILO_LIBS || ''; -const folderPath = 'screenshots/studio'; let studio; -const { features } = studioSpec; -test.beforeEach(async ({ page, browserName }) => { +test.beforeEach(async ({ page, browserName, baseURL }) => { + test.slow(); test.skip( browserName !== 'chromium', 'Not supported to run on multiple browsers.', ); studio = new StudioPage(page); + features[0].url = `${baseURL}/studio.html`; + await page.goto(features[0].url); + await page.waitForURL('**/auth.services.adobe.com/en_US/index.html**/'); + await ims.fillOutSignInForm(features[0], page); + await expect(async () => { + const response = await page.request.get(features[0].url); + expect(response.status()).toBe(200); + }).toPass(); + await page.waitForLoadState('domcontentloaded'); }); test.describe('M@S Studio feature test suite', () => { @@ -24,46 +32,92 @@ test.describe('M@S Studio feature test suite', () => { page, baseURL, }) => { - const name = `${features[0].name}`; + const name = `${features[0].name}`; test.slow(); const { data } = features[0]; const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`; console.info('[Test Page]: ', testPage); - await test.step('step-1: Log in to MAS studio', async () => { + await test.step('step-1: Go to MAS Studio test page', async () => { await page.goto(testPage); - page.waitForTimeout(5000); - const result = await take.take( - page, - folderPath, - name, - ); - // writeResultsToFile(folderPath, testInfo, result); - - await page.waitForURL( - '**/auth.services.adobe.com/en_US/index.html**/', - ); - features[0].url = - `${baseURL}/studio.html`; - await ims.fillOutSignInForm(features[0], page); - await expect(async () => { - const response = await page.request.get(features[0].url); - expect(response.status()).toBe(200); - }).toPass(); await page.waitForLoadState('domcontentloaded'); }); - await test.step('step-2: Go to MAS Studio test page', async () => { + await test.step('step-2: Validate search results', async () => { + await expect(await studio.renderView).toBeVisible(); + + const cards = await studio.renderView.locator('merch-card'); + expect(await cards.count()).toBe(1); + }); + }); + + // @studio-search-field - Validate search field in mas studio + test(`${features[1].name},${features[1].tags}`, async ({ + page, + baseURL, + }) => { + const name = `${features[0].name}`; + + test.slow(); + const { data } = features[1]; + const testPage = `${baseURL}${features[1].path}${miloLibs}${features[1].browserParams}`; + console.info('[Test Page]: ', testPage); + + await test.step('step-1: Go to MAS Studio test page', async () => { await page.goto(testPage); await page.waitForLoadState('domcontentloaded'); }); - await test.step('step-3: Validate search results', async () => { + await test.step('step-2: Validate search field rendered', async () => { + await expect(await studio.searchInput).toBeVisible(); + await expect(await studio.searchIcon).toBeVisible(); await expect(await studio.renderView).toBeVisible(); - const cards = await studio.renderView.locator('merch-card'); - expect(await cards.count()).toBe(1); + expect(await cards.count()).toBeGreaterThan(1); + }); + + await test.step('step-3: Validate search feature', async () => { + await studio.searchInput.fill(data.cardid); + await page.keyboard.press('Enter'); + await page.waitForTimeout(2000); + expect(await studio.getCard(data.cardid, 'suggested')).toBeVisible; + const searchResult = await studio.renderView.locator('merch-card'); + expect(await searchResult.count()).toBe(1); + }); + }); + + // @studio-edit-title - Validate edit title feature in mas studio + test(`${features[2].name},${features[2].tags}`, async ({ + page, + baseURL, + }) => { + const name = `${features[2].name}`; + + test.slow(); + const { data } = features[2]; + const testPage = `${baseURL}${features[2].path}${miloLibs}${features[2].browserParams}${data.cardid}`; + console.info('[Test Page]: ', testPage); + + await test.step('step-1: Go to MAS Studio test page', async () => { + await page.goto(testPage); + await page.waitForLoadState('domcontentloaded'); + }); + + await test.step('step-2: Open card editor', async () => { + expect(await studio.getCard(data.cardid, 'suggested')).toBeVisible; + await (await studio.getCard(data.cardid, 'suggested')).dblclick(); + expect(await studio.editorPanel).toBeVisible; + }); + await test.step('step-2: Open card editor', async () => { + expect(await studio.editorPanel.title).toBeVisible; + await expect( + await studio.editorPanel.locator(studio.editorTitle), + ).toHaveAttribute('value', `${data.title}`); + await studio.editorPanel + .locator(studio.editorTitle) + .locator('input') + .fill(data.newTitle); }); }); }); From 47cf36e299a79e41fd94d8a74b6ab2d91878dfda Mon Sep 17 00:00:00 2001 From: cod23684 Date: Fri, 13 Dec 2024 19:41:02 +0100 Subject: [PATCH 11/17] remove redundant lines --- nala/studio/studio.test.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index fad54202..b938caa6 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -32,9 +32,6 @@ test.describe('M@S Studio feature test suite', () => { page, baseURL, }) => { - const name = `${features[0].name}`; - - test.slow(); const { data } = features[0]; const testPage = `${baseURL}${features[0].path}${miloLibs}${features[0].browserParams}${data.cardid}`; console.info('[Test Page]: ', testPage); @@ -57,9 +54,6 @@ test.describe('M@S Studio feature test suite', () => { page, baseURL, }) => { - const name = `${features[0].name}`; - - test.slow(); const { data } = features[1]; const testPage = `${baseURL}${features[1].path}${miloLibs}${features[1].browserParams}`; console.info('[Test Page]: ', testPage); @@ -92,9 +86,6 @@ test.describe('M@S Studio feature test suite', () => { page, baseURL, }) => { - const name = `${features[2].name}`; - - test.slow(); const { data } = features[2]; const testPage = `${baseURL}${features[2].path}${miloLibs}${features[2].browserParams}${data.cardid}`; console.info('[Test Page]: ', testPage); @@ -109,7 +100,7 @@ test.describe('M@S Studio feature test suite', () => { await (await studio.getCard(data.cardid, 'suggested')).dblclick(); expect(await studio.editorPanel).toBeVisible; }); - await test.step('step-2: Open card editor', async () => { + await test.step('step-3: Edit title field', async () => { expect(await studio.editorPanel.title).toBeVisible; await expect( await studio.editorPanel.locator(studio.editorTitle), From 0ca6359820083c33cd3a0c12dd30a9edb6c68977 Mon Sep 17 00:00:00 2001 From: Mariia Lukianets Date: Mon, 23 Dec 2024 14:13:49 +0100 Subject: [PATCH 12/17] enable FF tests --- nala/studio/studio.test.js | 8 ++++---- nala/utils/pr.run.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index b938caa6..1cc014cc 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -10,10 +10,10 @@ let studio; test.beforeEach(async ({ page, browserName, baseURL }) => { test.slow(); - test.skip( - browserName !== 'chromium', - 'Not supported to run on multiple browsers.', - ); + // test.skip( + // browserName !== 'chromium', + // 'Not supported to run on multiple browsers.', + // ); studio = new StudioPage(page); features[0].url = `${baseURL}/studio.html`; await page.goto(features[0].url); diff --git a/nala/utils/pr.run.sh b/nala/utils/pr.run.sh index ca3f0287..ef09c803 100644 --- a/nala/utils/pr.run.sh +++ b/nala/utils/pr.run.sh @@ -73,7 +73,7 @@ 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=mas-live-chromium --project=mas-live-firefox --project=mas-live-webkit ${REPORTER} || EXIT_STATUS=$? +npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --project=mas-live-chromium --project=mas-live-firefox ${REPORTER} || EXIT_STATUS=$? # Check if tests passed or failed if [ $EXIT_STATUS -ne 0 ]; then From 8201c47a8e12406485cf6430c6fecbd126e31210 Mon Sep 17 00:00:00 2001 From: Mariia Lukianets Date: Mon, 23 Dec 2024 14:18:55 +0100 Subject: [PATCH 13/17] try fix header --- nala/studio/studio.test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index 1cc014cc..12db3d22 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -10,10 +10,11 @@ let studio; test.beforeEach(async ({ page, browserName, baseURL }) => { test.slow(); - // test.skip( - // browserName !== 'chromium', - // 'Not supported to run on multiple browsers.', - // ); + if (browserName === 'chromium') { + await page.setExtraHTTPHeaders({ + 'sec-ch-ua': '"Chromium";v="123", "Not:A-Brand";v="8"', + }); + } studio = new StudioPage(page); features[0].url = `${baseURL}/studio.html`; await page.goto(features[0].url); From f0dcd196da1186f2e9326902ec389b7f7cde8f2d Mon Sep 17 00:00:00 2001 From: Mariia Lukianets Date: Mon, 23 Dec 2024 14:40:46 +0100 Subject: [PATCH 14/17] chrome single test --- nala/studio/studio.test.js | 112 ++++++++++++++++++------------------- nala/utils/pr.run.sh | 3 +- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/nala/studio/studio.test.js b/nala/studio/studio.test.js index 12db3d22..da38f9e8 100644 --- a/nala/studio/studio.test.js +++ b/nala/studio/studio.test.js @@ -50,66 +50,66 @@ test.describe('M@S Studio feature test suite', () => { }); }); - // @studio-search-field - Validate search field in mas studio - test(`${features[1].name},${features[1].tags}`, async ({ - page, - baseURL, - }) => { - const { data } = features[1]; - const testPage = `${baseURL}${features[1].path}${miloLibs}${features[1].browserParams}`; - console.info('[Test Page]: ', testPage); + // // @studio-search-field - Validate search field in mas studio + // test(`${features[1].name},${features[1].tags}`, async ({ + // page, + // baseURL, + // }) => { + // const { data } = features[1]; + // const testPage = `${baseURL}${features[1].path}${miloLibs}${features[1].browserParams}`; + // console.info('[Test Page]: ', testPage); - await test.step('step-1: Go to MAS Studio test page', async () => { - await page.goto(testPage); - await page.waitForLoadState('domcontentloaded'); - }); + // await test.step('step-1: Go to MAS Studio test page', async () => { + // await page.goto(testPage); + // await page.waitForLoadState('domcontentloaded'); + // }); - await test.step('step-2: Validate search field rendered', async () => { - await expect(await studio.searchInput).toBeVisible(); - await expect(await studio.searchIcon).toBeVisible(); - await expect(await studio.renderView).toBeVisible(); - const cards = await studio.renderView.locator('merch-card'); - expect(await cards.count()).toBeGreaterThan(1); - }); + // await test.step('step-2: Validate search field rendered', async () => { + // await expect(await studio.searchInput).toBeVisible(); + // await expect(await studio.searchIcon).toBeVisible(); + // await expect(await studio.renderView).toBeVisible(); + // const cards = await studio.renderView.locator('merch-card'); + // expect(await cards.count()).toBeGreaterThan(1); + // }); - await test.step('step-3: Validate search feature', async () => { - await studio.searchInput.fill(data.cardid); - await page.keyboard.press('Enter'); - await page.waitForTimeout(2000); - expect(await studio.getCard(data.cardid, 'suggested')).toBeVisible; - const searchResult = await studio.renderView.locator('merch-card'); - expect(await searchResult.count()).toBe(1); - }); - }); + // await test.step('step-3: Validate search feature', async () => { + // await studio.searchInput.fill(data.cardid); + // await page.keyboard.press('Enter'); + // await page.waitForTimeout(2000); + // expect(await studio.getCard(data.cardid, 'suggested')).toBeVisible; + // const searchResult = await studio.renderView.locator('merch-card'); + // expect(await searchResult.count()).toBe(1); + // }); + // }); - // @studio-edit-title - Validate edit title feature in mas studio - test(`${features[2].name},${features[2].tags}`, async ({ - page, - baseURL, - }) => { - const { data } = features[2]; - const testPage = `${baseURL}${features[2].path}${miloLibs}${features[2].browserParams}${data.cardid}`; - console.info('[Test Page]: ', testPage); + // // @studio-edit-title - Validate edit title feature in mas studio + // test(`${features[2].name},${features[2].tags}`, async ({ + // page, + // baseURL, + // }) => { + // const { data } = features[2]; + // const testPage = `${baseURL}${features[2].path}${miloLibs}${features[2].browserParams}${data.cardid}`; + // console.info('[Test Page]: ', testPage); - await test.step('step-1: Go to MAS Studio test page', async () => { - await page.goto(testPage); - await page.waitForLoadState('domcontentloaded'); - }); + // await test.step('step-1: Go to MAS Studio test page', async () => { + // await page.goto(testPage); + // await page.waitForLoadState('domcontentloaded'); + // }); - await test.step('step-2: Open card editor', async () => { - expect(await studio.getCard(data.cardid, 'suggested')).toBeVisible; - await (await studio.getCard(data.cardid, 'suggested')).dblclick(); - expect(await studio.editorPanel).toBeVisible; - }); - await test.step('step-3: Edit title field', async () => { - expect(await studio.editorPanel.title).toBeVisible; - await expect( - await studio.editorPanel.locator(studio.editorTitle), - ).toHaveAttribute('value', `${data.title}`); - await studio.editorPanel - .locator(studio.editorTitle) - .locator('input') - .fill(data.newTitle); - }); - }); + // await test.step('step-2: Open card editor', async () => { + // expect(await studio.getCard(data.cardid, 'suggested')).toBeVisible; + // await (await studio.getCard(data.cardid, 'suggested')).dblclick(); + // expect(await studio.editorPanel).toBeVisible; + // }); + // await test.step('step-3: Edit title field', async () => { + // expect(await studio.editorPanel.title).toBeVisible; + // await expect( + // await studio.editorPanel.locator(studio.editorTitle), + // ).toHaveAttribute('value', `${data.title}`); + // await studio.editorPanel + // .locator(studio.editorTitle) + // .locator('input') + // .fill(data.newTitle); + // }); + // }); }); diff --git a/nala/utils/pr.run.sh b/nala/utils/pr.run.sh index ef09c803..e2825c90 100644 --- a/nala/utils/pr.run.sh +++ b/nala/utils/pr.run.sh @@ -73,7 +73,8 @@ 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=mas-live-chromium --project=mas-live-firefox ${REPORTER} || EXIT_STATUS=$? +npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --project=mas-live-chromium ${REPORTER} || EXIT_STATUS=$? +#npx playwright test --config=./playwright.config.js ${TAGS} ${EXCLUDE_TAGS} --ui --project=mas-live-chromium ${REPORTER} || EXIT_STATUS=$? # Check if tests passed or failed if [ $EXIT_STATUS -ne 0 ]; then From b49cdda8613f053a774f45ed3ddd311c6c8483a9 Mon Sep 17 00:00:00 2001 From: Mariia Lukianets Date: Mon, 23 Dec 2024 14:57:30 +0100 Subject: [PATCH 15/17] try ssh again --- .github/workflows/run-nala.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-nala.yml b/.github/workflows/run-nala.yml index 137172dd..90ba1c32 100644 --- a/.github/workflows/run-nala.yml +++ b/.github/workflows/run-nala.yml @@ -22,6 +22,9 @@ jobs: with: fetch-depth: 2 + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: From 35088aad42ed8983d326c91609a5760c75949948 Mon Sep 17 00:00:00 2001 From: Mariia Lukianets Date: Mon, 23 Dec 2024 15:08:21 +0100 Subject: [PATCH 16/17] limi to actor --- nala/libs/imslogin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nala/libs/imslogin.js b/nala/libs/imslogin.js index 21f8b489..4b938312 100644 --- a/nala/libs/imslogin.js +++ b/nala/libs/imslogin.js @@ -23,7 +23,7 @@ async function fillOutSignInForm(props, page) { expect(heading).toBe('Enter your password'); await page.locator('#PasswordPage-PasswordField').fill(process.env.IMS_PASS); await page.locator('[data-id=PasswordPage-ContinueButton]').click(); - await page.locator('div.ActionList-Item:nth-child(1)').click(); + //await page.locator('div.ActionList-Item:nth-child(1)').click(); await page.waitForURL(`${props.url}#`); await expect(page).toHaveURL(`${props.url}#`); } From 360f6b2f266827d3baa564b986df3a2842b55a4f Mon Sep 17 00:00:00 2001 From: Mariia Lukianets Date: Mon, 23 Dec 2024 15:21:28 +0100 Subject: [PATCH 17/17] add env vars --- .github/workflows/run-nala.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/run-nala.yml b/.github/workflows/run-nala.yml index 90ba1c32..18443b68 100644 --- a/.github/workflows/run-nala.yml +++ b/.github/workflows/run-nala.yml @@ -24,6 +24,20 @@ jobs: - name: Setup tmate session uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: true + 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 }} - name: Set up Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4