diff --git a/.eslintrc.js b/.eslintrc.js index 6e2a699d3..48cfa6907 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,16 @@ module.exports = { root: true, - extends: ['airbnb-base', 'plugin:react-hooks/recommended', 'plugin:compat/recommended', 'plugin:ecmalist/recommended'], + extends: [ + 'airbnb-base', + 'plugin:react-hooks/recommended', + 'plugin:compat/recommended', + 'plugin:ecmalist/recommended', + ], settings: { es: { aggressive: true } }, - env: { browser: true, mocha: true }, + env: { + browser: true, + mocha: true, + }, parser: '@babel/eslint-parser', parserOptions: { allowImportExportEverywhere: true, @@ -11,11 +19,20 @@ module.exports = { }, rules: { 'chai-friendly/no-unused-expressions': 2, - 'import/extensions': ['error', { js: 'always' }], + 'import/extensions': [ + 'error', + { js: 'always' }, + ], 'import/no-cycle': 0, - 'linebreak-style': ['error', 'unix'], + 'linebreak-style': [ + 'error', + 'unix', + ], 'no-await-in-loop': 0, - 'no-param-reassign': [2, { props: false }], + 'no-param-reassign': [ + 2, + { props: false }, + ], 'no-restricted-syntax': [ 'error', { @@ -31,20 +48,59 @@ module.exports = { message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', }, ], - 'no-return-assign': ['error', 'except-parens'], + 'no-return-assign': [ + 'error', + 'except-parens', + ], 'no-unused-expressions': 0, - 'object-curly-newline': ['error', { - ObjectExpression: { multiline: true, minProperties: 6 }, - ObjectPattern: { multiline: true, minProperties: 6 }, - ImportDeclaration: { multiline: true, minProperties: 6 }, - ExportDeclaration: { multiline: true, minProperties: 6 }, - }], + 'object-curly-newline': [ + 'error', + { + ObjectExpression: { + multiline: true, + minProperties: 6, + }, + ObjectPattern: { + multiline: true, + minProperties: 6, + }, + ImportDeclaration: { + multiline: true, + minProperties: 6, + }, + ExportDeclaration: { + multiline: true, + minProperties: 6, + }, + }, + ], }, overrides: [ { - files: ['test/**/*.js'], + files: [ + 'test/**/*.js', + ], rules: { 'no-console': 0 }, }, + { + files: [ + 'nala/**/*.js', + 'nala/**/*.test.js', + ], + rules: { + 'no-console': 0, + 'import/no-extraneous-dependencies': 0, + 'max-len': 0, + 'chai-friendly/no-unused-expressions': 0, + 'no-plusplus': 0, + 'global-require': 0, + 'object-curly-newline': 'off', + 'no-unused-vars': 'off', + 'arrow-parens': 'off', + 'compat/compat': 'off', + 'one-var-declaration-per-line': 'off', + }, + }, ], ignorePatterns: [ '/libs/deps/*', diff --git a/.github/workflows/nala-pr.yml b/.github/workflows/nala-pr.yml new file mode 100644 index 000000000..2e62b9de9 --- /dev/null +++ b/.github/workflows/nala-pr.yml @@ -0,0 +1,50 @@ +name: Run Nala Tests + +on: + push: + branches: + - stage + - main + pull_request: + branches: + - stage + - main + types: [opened, synchronize, reopened] + + workflow_dispatch: + +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@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 2 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e + 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 8b130e633..986ec0431 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ CODEOWNERS #Test files /**/test-files/empty.* /**/test-files/bad.* + +test-html-results/ +test-results/ +test-a11y-results/ diff --git a/nala/blocks/demo/demo.page.js b/nala/blocks/demo/demo.page.js new file mode 100644 index 000000000..12023a2c2 --- /dev/null +++ b/nala/blocks/demo/demo.page.js @@ -0,0 +1,6 @@ +export default class DemoBlock { + constructor(page) { + this.page = page; + this.header = this.page.locator('h1'); + } +} diff --git a/nala/blocks/demo/demo.spec.js b/nala/blocks/demo/demo.spec.js new file mode 100644 index 000000000..746f98644 --- /dev/null +++ b/nala/blocks/demo/demo.spec.js @@ -0,0 +1,15 @@ +module.exports = { + FeatureName: 'Demo Block', + features: [ + { + tcid: '0', + name: '@demo-basic', + // ✅ Default path to let you run the demo test after onboarding + path: '/drafts/nala/demo/demo-page', + // 💡 Replace with your project-specific test page after onboarding + // Example: path: '/drafts/nala/blocks/accordion/accordion-page', + data: { headerText: 'Nala Demo Test' }, + tags: '@demo @smoke', + }, + ], +}; diff --git a/nala/blocks/demo/demo.test.js b/nala/blocks/demo/demo.test.js new file mode 100644 index 000000000..3b5ca43b4 --- /dev/null +++ b/nala/blocks/demo/demo.test.js @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; +import DemoBlock from './demo.page.js'; +import { features } from './demo.spec.js'; + +let demoBlock; + +test.describe('Demo Block Test Suite', () => { + test.beforeEach(async ({ page }) => { + demoBlock = new DemoBlock(page); + }); + + test(`${features[0].name},${features[0].tags}`, async ({ page, baseURL }) => { + const { path, data } = features[0]; + const finalUrl = path.startsWith('http') ? path : `${baseURL}${path}`; + console.info(`[Demo Test] Navigating to: ${finalUrl}`); + await page.goto(finalUrl); + await expect(demoBlock.header).toBeVisible(); + await expect(demoBlock.header).toContainText(data.headerText); + }); +}); diff --git a/nala/libs/accessibility.js b/nala/libs/accessibility.js new file mode 100644 index 000000000..c22c69047 --- /dev/null +++ b/nala/libs/accessibility.js @@ -0,0 +1,132 @@ +/* eslint-disable import/prefer-default-export */ + +import { test } from '@playwright/test'; +import { AccessibilityError } from './customerrors.js'; + +const AxeBuilder = require('@axe-core/playwright').default; + +/** + * Run accessibility test to meet legal compliance (WCAG 2.0/2.1 A & AA) + * @param {Object} page - The page object. + * @param {string} [testScope='body'] - Optional scope for the accessibility test. Default is the entire page ('body'). + * @param {string[]} [includeTags=['wcag2a', 'wcag2aa']] - Optional tags to include in the accessibility test. Default is WCAG 2.0/2.1 A & AA. + * @param {number} [maxViolations=0] - Optional maximum number of allowed violations before the test fails. Default is 0 (any violation fails the test). + * @param {boolean} [skipA11yTest=false] - If true, the test step is logged and skipped. + */ +async function runAccessibilityTest({ + page, + testScope = 'body', + includeTags = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'], + maxViolations = 0, + skipA11yTest = false, +} = {}) { + if (skipA11yTest) { + console.log( + `[Skipping]: Accessibility test step skipped for this component ${testScope}`, + ); + return; + } + + let testName; + try { + // Retrive the test title (name) + testName = test.info().title.includes(',') + ? test.info().title.split(',')[0] + : test.info().title; + + let scopeDescription = 'entire page'; + let testElement = testScope; + + let violationsDetails = ''; + + // Handle a case where testScope is a string or locator from POM + if (typeof testScope === 'string') { + if (testScope === 'body') { + testElement = 'body'; + } else { + scopeDescription = `section: ${testScope}`; + } + } else if ( + typeof testScope === 'object' + && testScope.constructor.name === 'Locator' + ) { + const eleHandle = await testScope.elementHandle(); + if (!eleHandle) { + throw new AccessibilityError('Element not found for the given locator'); + } + testElement = eleHandle; + scopeDescription = `the provided locator: ${testScope}`; + } else { + scopeDescription = testScope === 'body' ? 'the entire page' : `section: ${testScope}`; + } + + console.log('Scope description:', scopeDescription); + // Run the Axe accessibility test on the given scope and tags + const axe = await new AxeBuilder({ page }) + .withTags(includeTags) + .include(testElement) + .analyze(); + + const enhancedAxeResults = { + testName, + testScope: + testScope === 'body' ? 'Entire Page' : `Specific Section: ${testScope}`, + ...axe, + }; + + const violationCount = enhancedAxeResults.violations.length; + + if (violationCount > maxViolations) { + // Accessibility violations details + violationsDetails += '\n========== Accessibility Test ==========\n'; + violationsDetails += `[Test Name ]: ${testName}\n`; + violationsDetails += `[Test Page URL]: ${page.url()}\n`; + violationsDetails += `[Accessibility]: Running accessibility test on ${scopeDescription}\n`; + + enhancedAxeResults.violations.forEach((violation, index) => { + violationsDetails += `[Result ]: Accessibility test found ${violationCount} accessibility violation(s) for ${testName}\n`; + violationsDetails += '[Violation Details]:\n'; + violationsDetails += `\n${index + 1}. Violation: ${ + violation.description + }\n`; + violationsDetails += ` - Axe Rule ID: ${violation.id}\n`; + violationsDetails += ` - Severity: ${violation.impact}\n`; + + const wcagTags = Array.isArray(violation.tags) + ? violation.tags.join(', ') + : 'N/A'; + violationsDetails += ` - WCAG Tags: ${wcagTags}\n`; + violationsDetails += ' - Nodes affected:\n'; + + violation.nodes.forEach((node, nodeIndex) => { + violationsDetails += ` ${nodeIndex + 1}. ${node.html}\n`; + }); + violationsDetails += ` - Fix: ${violation.helpUrl}\n`; + }); + + // attached accessibility voilations details to test results + await test.info().attach('Accessibility Test Results', { + body: JSON.stringify(enhancedAxeResults, null, 2), + contentType: 'application/json', + }); + + throw new AccessibilityError( + `\nAccessibility test failed : ${violationCount} violation(s) found. \n ${violationsDetails}`, + ); + } else if (violationCount > 0) { + console.info( + `[Accessibility Test]: Found ${violationCount} violation(s) for ${testName}, but under the threshold of ${maxViolations}.`, + ); + } else { + console.info( + `[Accessibility Test]: No accessibility violations found for ${testName}.`, + ); + } + } catch (err) { + throw new AccessibilityError( + `[Accessibility Test failed for ${testName}].\n ${err.message}`, + ); + } +} + +export { runAccessibilityTest }; diff --git a/nala/libs/baseurl.js b/nala/libs/baseurl.js new file mode 100644 index 000000000..3dbc00104 --- /dev/null +++ b/nala/libs/baseurl.js @@ -0,0 +1,17 @@ +/* eslint-disable import/no-extraneous-dependencies, import/prefer-default-export, max-len, no-console */ +import { head } from 'axios'; + +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; + } + 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 (${error.message})`); + return false; + } +} diff --git a/nala/libs/config.js b/nala/libs/config.js new file mode 100644 index 000000000..4cb5875b7 --- /dev/null +++ b/nala/libs/config.js @@ -0,0 +1,9 @@ +// CommonJS Config (.js with module.exports) +const PROJECT = 'dc'; +const ORG = 'adobecom'; +const BRANCHES = { main: 'main', stage: 'stage' }; +const MAIN_BRANCH_LIVE_URL = `https://${BRANCHES.main}--${PROJECT}--${ORG}.aem.live`; +const STAGE_BRANCH_URL = `https://${BRANCHES.stage}--${PROJECT}--${ORG}.aem.live`; +const BASE_URLS = { local: 'http://localhost:3000', stage: STAGE_BRANCH_URL, main: MAIN_BRANCH_LIVE_URL }; + +module.exports = { PROJECT, ORG, BRANCHES, MAIN_BRANCH_LIVE_URL, STAGE_BRANCH_URL, BASE_URLS }; diff --git a/nala/libs/constants.js b/nala/libs/constants.js new file mode 100644 index 000000000..bd20cead3 --- /dev/null +++ b/nala/libs/constants.js @@ -0,0 +1,37 @@ +// nala/libs/constants.js + +const DEFAULT_REPO = 'milo'; +const DEFAULT_ORG = 'adobecom'; + +// Default fallback BASE_URLS (not project-specific) +const BASE_URLS = { + local: 'http://localhost:3000', + stage: `http://stage--${DEFAULT_REPO}--${DEFAULT_ORG}.aem.live`, + main: `https://main--${DEFAULT_REPO}--${DEFAULT_ORG}.aem.live`, +}; + +// Utility function to generate branch live URLs dynamically +function getBranchUrl(branch, repo = DEFAULT_REPO, org = DEFAULT_ORG, path = '') { + return `https://${branch}--${repo}--${org}.aem.live${path.startsWith('/') ? '' : '/'}${path}`; +} + +// Optional extras you can extend: +const A11Y_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']; +const MAX_A11Y_VIOLATIONS = 0; +const SLACK_CHANNEL = '#nala-reports'; +const USER_AGENT_DESKTOP = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'; +const USER_AGENT_MOBILE_CHROME = 'Mozilla/5.0 (Linux; Android 10; Pixel 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36'; +const USER_AGENT_MOBILE_SAFARI = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1 NALA-Acom'; + +module.exports = { + DEFAULT_REPO, + DEFAULT_ORG, + BASE_URLS, + getBranchUrl, + A11Y_TAGS, + MAX_A11Y_VIOLATIONS, + SLACK_CHANNEL, + USER_AGENT_DESKTOP, + USER_AGENT_MOBILE_CHROME, + USER_AGENT_MOBILE_SAFARI, +}; diff --git a/nala/libs/customerrors.js b/nala/libs/customerrors.js new file mode 100644 index 000000000..0b9aad757 --- /dev/null +++ b/nala/libs/customerrors.js @@ -0,0 +1,8 @@ +/* eslint-disable import/prefer-default-export */ + +export class AccessibilityError extends Error { + constructor(messages) { + super(messages); + this.name = 'AccessibilityError'; + } +} diff --git a/nala/libs/webutil.js b/nala/libs/webutil.js new file mode 100644 index 000000000..1fcbe0a73 --- /dev/null +++ b/nala/libs/webutil.js @@ -0,0 +1,329 @@ +/* eslint-disable import/no-extraneous-dependencies, max-len, no-console, class-methods-use-this */ + +import { expect } from '@playwright/test'; + +const { request } = require('@playwright/test'); + +/** + * A utility class for common web interactions. + */ +export default 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. + */ + 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. + */ + 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 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(); + } + + /** + * 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'. + */ + async getGnavDaalh(project) { + 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|||'. + */ + async getPznGnavDaalh(pznExpName, pznFileName, project) { + 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'. + */ + 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|||'. + */ + 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. + */ + async getBlockDaalh(blockName, counter, pzn = false, pzntext = 'nopzn') { + const slicedBlockName = blockName.slice(0, 15); + 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 '---'. + */ + async getLinkDaall(linkText, counter, lastHeaderText) { + 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}`; + } +} diff --git a/nala/utils/a11y-bot.js b/nala/utils/a11y-bot.js new file mode 100644 index 000000000..0615b1a4f --- /dev/null +++ b/nala/utils/a11y-bot.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const { chromium } = require('playwright/test'); +const AxeBuilder = require('@axe-core/playwright'); +const chalk = require('chalk'); +const generateA11yReport = require('./a11y-report.js'); + +/** + * Run accessibility test for legal compliance (WCAG 2.0/2.1 A & AA) + * @param {Object} page - The page object. + * @param {string} [testScope='body'] - Optional scope for the accessibility test. Default is 'body'. + * @param {string[]} [includeTags=['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']] - WCAG compliance tags. + * @param {number} [maxViolations=0] - Maximum violations before test fails. Default is 0. + * @returns {Object} - Results containing violations or success message. + */ +async function runAccessibilityTest(page, testScope = 'body', includeTags = [], maxViolations = 0) { + const result = { + url: page.url(), + testScope, + violations: [], + }; + + const wcagTags = includeTags.length > 0 ? includeTags : ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']; + + try { + const testElement = testScope === 'body' ? 'body' : testScope; + + console.log(chalk.blue('Accessibility Test Scope:'), testScope); + console.log(chalk.blue('WCAG Tags:'), wcagTags); + + const axe = new AxeBuilder({ page }) + .withTags(wcagTags) + .include(testElement) + .analyze(); + + result.violations = (await axe).violations; + const violationCount = result.violations.length; + + if (violationCount > maxViolations) { + let violationsDetails = `${violationCount} accessibility violations found:\n`; + result.violations.forEach((violation, index) => { + violationsDetails += ` + ${chalk.red(index + 1)}. Violation: ${chalk.yellow(violation.description)} + - Rule ID: ${chalk.cyan(violation.id)} + - Severity: ${chalk.magenta(violation.impact)} + - Fix: ${chalk.cyan(violation.helpUrl)} + `; + + violation.nodes.forEach((node, nodeIndex) => { + violationsDetails += ` Node ${nodeIndex + 1}: ${chalk.yellow(node.html)}\n`; + }); + }); + + throw new Error(violationsDetails); + } else { + console.info(chalk.green('No accessibility violations found.')); + } + } catch (err) { + console.error(chalk.red(`Accessibility test failed: ${err.message}`)); + } + + return result; +} + +/** + * Opens a browser, navigates to a page, runs accessibility test, and returns results. + * @param {string} url - The URL to test. + * @param {Object} options - Test options (scope, tags, maxViolations). + * @returns {Object} - Accessibility test results. + */ +async function runA11yTestOnPage(url, options = {}) { + const { scope = 'body', tags, maxViolations = 0 } = options; + const browser = await chromium.launch(); + const context = await browser.newContext({ + extraHTTPHeaders: { + 'sec-ch-ua': '"Chromium"', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36', + }, + }); + + const page = await context.newPage(); + let result; + + try { + console.log(chalk.blue(`Testing URL: ${url}`)); + await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' }); + result = await runAccessibilityTest(page, scope, tags, maxViolations); + } finally { + await browser.close(); + } + + return result; +} + +/** + * Processes URLs from a file and generates accessibility report. + * @param {string} filePath - Path to file with URLs. + * @param {Object} options - Test options. + */ +async function processUrlsFromFile(filePath, options = {}) { + const urls = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean); + console.log(chalk.blue('Processing URLs from file:'), urls); + const results = []; + + for (const url of urls) { + const result = await runA11yTestOnPage(url, options); + if (result && result.violations.length > 0) results.push(result); + } + + await generateA11yReport(results, options.outputDir || './test-a11y-results'); +} + +/** + * Processes URLs directly from command-line arguments and generates report. + * @param {string[]} urls - Array of URLs. + * @param {Object} options - Test options. + */ +async function processUrlsFromCommand(urls, options = {}) { + console.log(chalk.blue('Processing URLs from command-line input:'), urls); + const results = []; + + for (const url of urls) { + const result = await runA11yTestOnPage(url, options); + if (result && result.violations.length > 0) results.push(result); + } + + await generateA11yReport(results, options.outputDir || './reports'); +} + +module.exports = { + runA11yTestOnPage, + processUrlsFromFile, + processUrlsFromCommand, +}; diff --git a/nala/utils/a11y-report.js b/nala/utils/a11y-report.js new file mode 100644 index 000000000..20b075afb --- /dev/null +++ b/nala/utils/a11y-report.js @@ -0,0 +1,249 @@ +const path = require('path'); +const fs = require('fs').promises; + +// Pretty print HTML with proper indentation +function prettyPrintHTML(html) { + const tab = ' '; // Define the indentation level + let result = ''; + let indentLevel = 0; + + html.split(/>\s* { + if (element.match(/^\/\w/)) { + // Closing tag + indentLevel -= 1; + } + result += `${tab.repeat(indentLevel)}<${element}>\n`; + if (element.match(/^]*[^/]$/)) { + // Opening tag + indentLevel += 1; + } + }); + return result.trim(); +} + +function escapeHTML(html) { + return html.replace(/[&<>'"]/g, (char) => { + switch (char) { + case '&': return '&'; + case '<': return '<'; + case '>': return '>'; + case "'": return '''; + case '"': return '"'; + default: return char; + } + }); +} + +async function generateA11yReport(report, outputDir) { + const time = new Date(); + const reportName = `nala-a11y-report-${time + .toISOString() + .replace(/[:.]/g, '-')}.html`; + + const reportPath = path.resolve(outputDir, reportName); + + // Ensure the output directory exists + try { + await fs.mkdir(outputDir, { recursive: true }); + } catch (err) { + console.error(`Failed to create directory ${outputDir}: ${err.message}`); + return; + } + + try { + const files = await fs.readdir(outputDir); + for (const file of files) { + if (file.startsWith('nala-a11y-report') && file.endsWith('.html')) { + await fs.unlink(path.resolve(outputDir, file)); + } + } + } catch (err) { + console.error(`Failed to delete the old report files in ${outputDir}: ${err.message}`); + } + + // Check if the report contains violations + if (!report || report.length === 0) { + console.error('No accessibility violations to report.'); + return; + } + + const totalViolations = report.reduce( + (sum, result) => sum + (result.violations ? result.violations.length : 0), + 0, + ); + + const severityCount = { + critical: 0, + serious: 0, + moderate: 0, + minor: 0, + }; + + report.forEach((result) => { + result.violations?.forEach((violation) => { + if (violation.impact) { + severityCount[violation.impact] += 1; + } + }); + }); + + // Inline CSS for the report with wrapping for pre blocks + const inlineCSS = ` + `; + + // Inline JavaScript for collapsible functionality and filtering + const inlineJS = ` + `; + + let htmlContent = ` + + + Nala Accessibility Test Report + ${inlineCSS} + + + + +
+ + + + + +
`; + + // Test details section + report.forEach((result, resultIndex) => { + htmlContent += ` +
+

#${resultIndex + 1} Test URL: ${result.url}

+ + + + + + + + + + + + + `; + + result.violations.forEach((violation, index) => { + const severityClass = `severity-${violation.impact.toLowerCase()}`; + const wcagTags = Array.isArray(violation.tags) ? violation.tags.join(', ') : 'N/A'; + const nodesAffected = violation.nodes + .map((node) => `

${prettyPrintHTML(escapeHTML(node.html))}

`) + .join('\n'); + const possibleFix = violation.helpUrl ? `Fix` : 'N/A'; + + htmlContent += ` + + + + + + + + + `; + }); + + htmlContent += ` + +
#ViolationAxe Rule IDSeverityWCAG TagsNodes AffectedFix
${index + 1}${violation.description}${violation.id}${violation.impact}${wcagTags} + + +
${nodesAffected} +
+
${possibleFix}
+
`; + }); + + htmlContent += ` + ${inlineJS} + + `; + + // Write the HTML report to file + try { + await fs.writeFile(reportPath, htmlContent); + console.info(`Accessibility report saved at: ${reportPath}`); + // eslint-disable-next-line consistent-return + return reportPath; + } catch (err) { + console.error(`Failed to save accessibility report: ${err.message}`); + } +} + +module.exports = generateA11yReport; diff --git a/nala/utils/base-reporter.js b/nala/utils/base-reporter.js new file mode 100644 index 000000000..86235f6f2 --- /dev/null +++ b/nala/utils/base-reporter.js @@ -0,0 +1,223 @@ +/* eslint-disable max-len, class-methods-use-this, no-empty-function, no-console */ + +const { sendSlackMessage } = require('./slack.js'); + +// 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, ''); +} + +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 += 1; + } else if (failedStatus.includes(status)) { + this.failedTests += 1; + } else if (status === 'skipped') { + this.skippedTests += 1; + } + } + + async onEnd() { + const summary = this.printResultSummary(); + const resultSummary = { summary }; + + if (process.env.SLACK_WH) { + try { + 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 || 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, + }; + } + + 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.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, + }; + } +} +export default BaseReporter; diff --git a/nala/utils/global.setup.js b/nala/utils/global.setup.js new file mode 100644 index 000000000..153821599 --- /dev/null +++ b/nala/utils/global.setup.js @@ -0,0 +1,124 @@ +/* eslint-disable import/no-extraneous-dependencies, no-console */ + +const { execSync } = require('child_process'); +const { isBranchURLValid } = require('../libs/baseurl.js'); + +// Dynamically load PROJECT, ORG, BASE_URLS +let PROJECT; let ORG; let BASE_URLS; let getBranchUrl; +try { + ({ PROJECT, ORG, BASE_URLS } = require('../libs/config.js')); + ({ getBranchUrl } = require('../libs/constants.js')); +} catch { + ({ DEFAULT_REPO: PROJECT, DEFAULT_ORG: ORG, BASE_URLS, getBranchUrl } = require('../libs/constants.js')); +} + +async function getGitHubPRBranchLiveUrl() { + const prReference = process.env.GITHUB_REF; + const prHeadReference = process.env.GITHUB_HEAD_REF; + + const prNumber = prReference.startsWith('refs/pull/') + ? prReference.split('/')[2] + : null; + + const prBranch = prHeadReference + ? prHeadReference.replace(/\//g, '-') + : prReference.split('/')[2].replace(/\//g, '-'); + + const repository = process.env.GITHUB_REPOSITORY; + const repoParts = repository.split('/'); + const toRepoOrg = repoParts[0]; + const toRepoName = repoParts[1]; + + const prFromOrg = process.env.prOrg || toRepoOrg; + const prFromRepoName = process.env.prRepo || toRepoName; + + let prBranchLiveUrl; + if (prBranch === 'main') { + prBranchLiveUrl = BASE_URLS.main; + } else { + prBranchLiveUrl = `https://${prBranch}--${prFromRepoName}--${prFromOrg}.aem.live`; + } + + try { + if (await isBranchURLValid(prBranchLiveUrl)) { + process.env.PR_BRANCH_LIVE_URL = prBranchLiveUrl; + } + console.info('GH Ref : ', prReference); + console.info('GH Head Ref : ', prHeadReference); + 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(U) : ', prBranch); + console.info('PR Number : ', prNumber || 'Auto-PR'); + console.info('PR 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 getCircleCIBranchLiveUrl() { + const stageBranchLiveUrl = BASE_URLS.stage; + try { + if (await isBranchURLValid(stageBranchLiveUrl)) { + process.env.PR_BRANCH_LIVE_URL = stageBranchLiveUrl; + } + console.info('Stage Branch Live URL : ', stageBranchLiveUrl); + } catch (err) { + console.error('Error => Error in setting Stage Branch test URL : ', stageBranchLiveUrl); + process.exit(1); + } +} + +async function getLocalNonGitBranchLiveUrl() { + const localTestLiveUrl = process.env.LOCAL_TEST_LIVE_URL || BASE_URLS.local; + console.info(`✅ Using Non-Git Local Test URL: ${localTestLiveUrl}`); + process.env.PR_BRANCH_LIVE_URL = localTestLiveUrl; +} + +async function getLocalBranchLiveUrl() { + try { + const localGitRootDir = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); + 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(); + const localTestLiveUrl = process.env.LOCAL_TEST_LIVE_URL || BASE_URLS.local; + console.info(`✅ Git ORG: ${localOrg}, REPO: ${localRepo}, Branch: ${localBranch}`); + console.info(`✅ Local Test Live URL: ${localTestLiveUrl}`); + process.env.PR_BRANCH_LIVE_URL = localTestLiveUrl; + return; + } + } catch (err) { + console.info('âš ī¸ Git not detected, falling back to non-Git local flow.'); + await getLocalNonGitBranchLiveUrl(); + } +} + +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'); + 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(); + } +} + +module.exports = globalSetup; diff --git a/nala/utils/nala.cli.js b/nala/utils/nala.cli.js new file mode 100644 index 000000000..b643585cd --- /dev/null +++ b/nala/utils/nala.cli.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/* eslint-disable default-param-last */ +const { Command } = require('commander'); +const chalk = require('chalk'); +const fs = require('fs'); +const { processUrlsFromCommand, processUrlsFromFile } = require('./a11y-bot.js'); + +// Dynamic config loader +let PROJECT; let ORG; let BASE_URLS; let + getBranchUrl; +try { + ({ PROJECT, ORG, BASE_URLS } = require('../libs/config.js')); + ({ getBranchUrl } = require('../libs/constants.js')); +} catch { + ({ DEFAULT_REPO: PROJECT, DEFAULT_ORG: ORG, BASE_URLS, getBranchUrl } = require('../libs/constants.js')); +} + +const program = new Command(); + +program + .name('nala-a11y-bot') + .description('Nala Accessibility Testing Bot') + .version('1.0.0'); + +program + .command('a11y [env] [path]') + .description('Run an accessibility test on an environment with optional path or a file with URLs') + .option('-f, --file ', 'Specify a file with multiple URLs') + .option('-s, --scope ', 'Specify the test scope (default: body)', 'body') + .option('-t, --tags ', 'Specify the tags to include', (val) => val.split(',')) + .option('-m, --max-violations ', 'Max allowed violations before failing (default: 0)', (val) => parseInt(val, 10), 0) + .option('-o, --output-dir ', 'Directory to save HTML report (default: ./test-a11y-results)', './test-a11y-results') + .action(async (env, path = '', options) => { + const urls = []; + + try { + if (options.file) { + if (!fs.existsSync(options.file)) { + console.error(chalk.red(`Error: The file path "${options.file}" does not exist.`)); + process.exit(1); + } + await processUrlsFromFile(options.file, options); + return; + } + + if (env && BASE_URLS[env]) { + const fullUrl = `${BASE_URLS[env]}${path.startsWith('/') ? '' : '/'}${path}`; + urls.push(fullUrl); + } else if (env && (env.startsWith('http://') || env.startsWith('https://'))) { + urls.push(env); + } else if (env) { + const branchUrl = getBranchUrl(env, PROJECT, ORG, path); + urls.push(branchUrl); + } else if (!options.file) { + console.error(chalk.red('Error: Invalid environment, URL, or file provided.')); + process.exit(1); + } + + const validUrls = urls.filter((url) => url.startsWith('http://') || url.startsWith('https://')); + + if (validUrls.length > 0) { + console.log(chalk.green(`Running accessibility tests on:\n${validUrls.join('\n')}\n`)); + await processUrlsFromCommand(validUrls, options); + } else { + console.error(chalk.red('No valid URLs to test.')); + process.exit(1); + } + } catch (error) { + console.error(chalk.red(`An error occurred during processing: ${error.message}`)); + process.exit(1); + } + }); + +program.parse(process.argv); diff --git a/nala/utils/nala.run.js b/nala/utils/nala.run.js new file mode 100644 index 000000000..1f69a59b0 --- /dev/null +++ b/nala/utils/nala.run.js @@ -0,0 +1,196 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ + +const { spawn } = require('child_process'); + +// Dynamic config loader +let PROJECT; let ORG; let BASE_URLS; let + getBranchUrl; +try { + ({ PROJECT, ORG, BASE_URLS } = require('../libs/config.js')); + ({ getBranchUrl } = require('../libs/constants.js')); +} catch { + ({ DEFAULT_REPO: PROJECT, DEFAULT_ORG: ORG, BASE_URLS, getBranchUrl } = require('../libs/constants.js')); +} + +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: milo-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: PROJECT, + owner: ORG, + }; + + 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 || 'milo'; + } 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 = `${parsedParams.repo}-live-${parsedParams.browser}`; + } + + return parsedParams; +} + +function getLocalTestLiveUrl(env, milolibs, repo = PROJECT, owner = ORG) { + if (env === 'main') return BASE_URLS.main; + if (env === 'stage') return BASE_URLS.stage; + if (env === 'local') return BASE_URLS.local; + if (env === 'libs') return 'http://localhost:6456'; + + if (milolibs) { + process.env.MILO_LIBS = `?milolibs=${milolibs}`; + } + + return getBranchUrl(env, repo, owner); +} + +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 (require.main === module) { + runNalaTest(); +} + +module.exports = { + displayHelp, + parseArgs, + getLocalTestLiveUrl, + buildPlaywrightCommand, + runNalaTest, +}; diff --git a/nala/utils/pr.run.sh b/nala/utils/pr.run.sh new file mode 100755 index 000000000..032ad3022 --- /dev/null +++ b/nala/utils/pr.run.sh @@ -0,0 +1,83 @@ +#!/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} + +# Handle PR Branch Live URL +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=dc-live-chromium --project=dc-live-firefox --project=dc-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/nala/utils/slack.js b/nala/utils/slack.js new file mode 100644 index 000000000..f6283f627 --- /dev/null +++ b/nala/utils/slack.js @@ -0,0 +1,21 @@ +/* eslint-disable import/no-extraneous-dependencies, no-console */ + +import axios from 'axios'; + +/** + * Sends a message to Slack using a webhook URL. + * @param {string} webhookUrl - The Slack channel webhook. + * @param {Object} messageContent - The content of the message to send. + */ +export default async function sendSlackMessage(webhookUrl, messageContent) { + try { + const response = await axios.post(webhookUrl, messageContent, { headers: { 'Content-Type': 'application/json' } }); + + if (response.status !== 200) { + throw new Error(`Error sending message to Slack. Status: ${response.status}. Message: ${response.data}`); + } + console.log('---Result summary is sent to slack---'); + } catch (error) { + console.error('Axios error:', error); + } +} diff --git a/package-lock.json b/package-lock.json index bc64180e3..501019c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,20 +20,25 @@ "async": "^3.2.5", "bowser": "^2.11.0", "jest-fetch-mock": "^3.0.3", - "playwright": "^1.53.1", "ws": "^8.18.0", "yargs": "^17.7.2" }, "devDependencies": { "@amwp/platform-ui-automation": "^0.0.8", "@amwp/platform-ui-lib-adobe": "^0.0.10", + "@axe-core/playwright": "4.10.0", "@babel/core": "7.23.2", "@babel/eslint-parser": "7.22.15", "@babel/register": "7.22.15", "@esm-bundle/chai": "4.3.4-fix.0", + "@playwright/test": "1.56.0", "@web/dev-server-import-maps": "^0.2.1", + "axe-html-reporter": "2.2.11", + "axios": "1.7.5", "braces": "^3.0.3", "chai": "4.3.6", + "chalk": "4.1.2", + "commander": "12.1.0", "compare-versions": "^6.1.0", "eslint": "8.11.0", "eslint-config-airbnb-base": "15.0.0", @@ -46,6 +51,7 @@ "jest-environment-jsdom": "^29.5.0", "koa-proxies": "^0.12.4", "microbundle": "^0.15.1", + "playwright": "1.56.0", "sinon": "13.0.1", "stylelint": "14.6.0", "stylelint-config-prettier": "9.0.3", @@ -127,6 +133,27 @@ "integrity": "sha512-s/OwRocmlv5efTuRpT5rfX0sjP9mAH8wtp1DXSK5DOPQC5X1NX3oof3vuKXrhXaHBnhdkh448IkuoY9lie0zZQ==", "dev": true }, + "node_modules/@axe-core/playwright": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.0.tgz", + "integrity": "sha512-kEr3JPEVUSnKIYp/egV2jvFj+chIjCjPp3K3zlpJMza/CB3TFw8UZNbI9agEC2uMz4YbgAOyzlbUy0QS+OofFA==", + "dev": true, + "dependencies": { + "axe-core": "~4.10.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@axe-core/playwright/node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2077,21 +2104,6 @@ "uuid": "10.0.0" } }, - "node_modules/@cucumber/cucumber/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@cucumber/cucumber/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2101,32 +2113,13 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@cucumber/cucumber/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@cucumber/cucumber/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@cucumber/cucumber/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=14" } }, "node_modules/@cucumber/cucumber/node_modules/glob": { @@ -2786,49 +2779,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/core": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", @@ -2876,49 +2826,6 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -3034,49 +2941,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -3159,49 +3023,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", @@ -3219,49 +3040,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -3375,12 +3153,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", - "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", + "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==", "dev": true, "dependencies": { - "playwright": "1.53.1" + "playwright": "1.56.0" }, "bin": { "playwright": "cli.js" @@ -3760,37 +3538,6 @@ "node": ">=8" } }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/@testing-library/dom/node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -3811,18 +3558,6 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "peer": true }, - "node_modules/@testing-library/dom/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/user-event": { "version": "14.5.2", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", @@ -4959,10 +4694,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axe-html-reporter": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.11.tgz", + "integrity": "sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==", + "dev": true, + "dependencies": { + "mustache": "^4.0.1" + }, + "engines": { + "node": ">=8.9.0" + }, + "peerDependencies": { + "axe-core": ">=3" + } + }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -4995,49 +4755,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -5576,6 +5293,21 @@ "node": ">=4" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chalk-template": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", @@ -5590,7 +5322,7 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, - "node_modules/chalk-template/node_modules/ansi-styles": { + "node_modules/chalk/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -5604,22 +5336,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/chalk-template/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template/node_modules/supports-color": { + "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -5952,12 +5669,12 @@ } }, "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/commondir": { @@ -6088,49 +5805,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -7353,37 +7027,6 @@ "node": ">=10" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7460,18 +7103,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -9585,49 +9216,6 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -9699,53 +9287,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { "@jest/core": "^29.7.0", @@ -9775,49 +9320,6 @@ } } }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-config": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", @@ -9863,49 +9365,6 @@ } } }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -9921,49 +9380,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-docblock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", @@ -9992,49 +9408,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-environment-jsdom": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", @@ -10150,49 +9523,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -10213,49 +9543,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-mock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", @@ -10329,49 +9616,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-runner": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", @@ -10404,37 +9648,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-runner/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10454,18 +9667,6 @@ "source-map": "^0.6.0" } }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-runtime": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", @@ -10499,49 +9700,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", @@ -10573,37 +9731,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-snapshot/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10631,18 +9758,6 @@ "node": ">=10" } }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-snapshot/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -10666,49 +9781,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", @@ -10726,49 +9798,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-watcher": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", @@ -10788,49 +9817,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", @@ -11839,6 +10825,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -12694,11 +11689,11 @@ } }, "node_modules/playwright": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", - "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", + "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", "dependencies": { - "playwright-core": "1.53.1" + "playwright-core": "1.56.0" }, "bin": { "playwright": "cli.js" @@ -12711,9 +11706,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", - "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", + "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", "bin": { "playwright-core": "cli.js" }, @@ -14369,37 +13364,6 @@ "postcss": "8.x" } }, - "node_modules/rollup-plugin-postcss/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/rollup-plugin-postcss/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/rollup-plugin-postcss/node_modules/pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -14412,18 +13376,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rollup-plugin-postcss/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/rollup-plugin-terser": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", diff --git a/package.json b/package.json index 6d28f90b2..9e8e2ee81 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "ewtest": "jest --testPathPattern=edgeworkers", "ewprod2stg": "node ./edgeworkers/scripts/prod2stg.js", "ewbuild": "node ./edgeworkers/scripts/build.js", - "ewsetbundle": "node ./edgeworkers/scripts/setbundle.js" + "ewsetbundle": "node ./edgeworkers/scripts/setbundle.js", + "nala": "node nala/utils/nala.run.js", + "a11y": "node nala/utils/nala.cli.js a11y" }, "repository": { "type": "git", @@ -40,13 +42,19 @@ "devDependencies": { "@amwp/platform-ui-automation": "^0.0.8", "@amwp/platform-ui-lib-adobe": "^0.0.10", + "@axe-core/playwright": "4.10.0", "@babel/core": "7.23.2", "@babel/eslint-parser": "7.22.15", "@babel/register": "7.22.15", "@esm-bundle/chai": "4.3.4-fix.0", + "@playwright/test": "1.56.0", "@web/dev-server-import-maps": "^0.2.1", + "axe-html-reporter": "2.2.11", + "axios": "1.7.5", "braces": "^3.0.3", "chai": "4.3.6", + "chalk": "4.1.2", + "commander": "12.1.0", "compare-versions": "^6.1.0", "eslint": "8.11.0", "eslint-config-airbnb-base": "15.0.0", @@ -59,6 +67,7 @@ "jest-environment-jsdom": "^29.5.0", "koa-proxies": "^0.12.4", "microbundle": "^0.15.1", + "playwright": "1.56.0", "sinon": "13.0.1", "stylelint": "14.6.0", "stylelint-config-prettier": "9.0.3", @@ -76,7 +85,6 @@ "async": "^3.2.5", "bowser": "^2.11.0", "jest-fetch-mock": "^3.0.3", - "playwright": "^1.53.1", "ws": "^8.18.0", "yargs": "^17.7.2" }, @@ -105,4 +113,4 @@ "^https://main--unity--adobecom.hlx.live/unitylibs/(.*)$": "/test/mocks/unitylibs/$1" } } -} +} \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..b90e2d60c --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,84 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable global-require */ +/* eslint-disable import/no-extraneous-dependencies */ + +const { devices } = require('@playwright/test'); + +let PROJECT; let ORG; let + BASE_URLS; +try { + ({ PROJECT, ORG, BASE_URLS } = require('./nala/libs/config.js')); +} catch { + const { DEFAULT_REPO, DEFAULT_ORG, BASE_URLS: DEFAULT_URLS } = require('./nala/libs/constants.js'); + PROJECT = DEFAULT_REPO; + ORG = DEFAULT_ORG; + BASE_URLS = DEFAULT_URLS; +} + +const USER_AGENT_DESKTOP = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36 NALA-Acom'; +const USER_AGENT_MOBILE_CHROME = 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 NALA-Acom'; +const USER_AGENT_MOBILE_SAFARI = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1 NALA-Acom'; + +/** @type {import('@playwright/test').PlaywrightTestConfig} */ +const config = { + testDir: './nala', + outputDir: './test-results', + globalSetup: './nala/utils/global.setup.js', + timeout: 30 * 1000, + expect: { timeout: 5000 }, + testMatch: '**/*.test.js', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 7 : 3, + reporter: process.env.CI + ? [['github'], ['list'], ['./nala/utils/base-reporter.js']] + : [['html', { outputFolder: 'test-html-results' }], ['list'], ['./nala/utils/base-reporter.js']], + use: { + actionTimeout: 60000, + trace: 'on-first-retry', + baseURL: + process.env.PR_BRANCH_LIVE_URL + || process.env.LOCAL_TEST_LIVE_URL + || BASE_URLS.main, + }, + projects: [ + { + name: `${PROJECT}-live-chromium`, + use: { + ...devices['Desktop Chrome'], + userAgent: USER_AGENT_DESKTOP, + }, + }, + { + name: `${PROJECT}-live-firefox`, + use: { + ...devices['Desktop Firefox'], + userAgent: USER_AGENT_DESKTOP, + }, + }, + { + name: `${PROJECT}-live-webkit`, + use: { + ...devices['Desktop Safari'], + userAgent: USER_AGENT_DESKTOP, + }, + }, + { + name: 'mobile-chrome-pixel5', + use: { + ...devices['Pixel 5'], + userAgent: USER_AGENT_MOBILE_CHROME, + }, + }, + { + name: 'mobile-safari-iPhone12', + use: { + ...devices['iPhone 12'], + userAgent: USER_AGENT_MOBILE_SAFARI, + }, + }, + ], +}; + +module.exports = config;