diff --git a/web/themes/custom/contribtracker/cypress/e2e/accessibility-check.cy.js b/web/themes/custom/contribtracker/cypress/e2e/accessibility-check.cy.js new file mode 100644 index 00000000..a5176099 --- /dev/null +++ b/web/themes/custom/contribtracker/cypress/e2e/accessibility-check.cy.js @@ -0,0 +1,78 @@ +// Import the list of pages and their URLs from a fixture file +import pages from '../fixtures/page-check-urls.json'; + +describe('Accessibility Testing for All Pages', { tags: ['expensive'] }, () => { + // Iterate over each page in the `pages` object + Object.entries(pages).forEach(([pageName, pageData]) => { + it(`Validates accessibility for ${pageName} page`, () => { + // Send an HTTP request to the page's URL to verify its accessibility status + cy.request({ + url: pageData.url, // Page URL + failOnStatusCode: false, // Do not fail the test on non-2xx/3xx HTTP status codes + }).then((response) => { + // Skip testing the page if the server returns a 403 Forbidden status + if (response.status === 403) { + cy.log(`Skipping ${pageName} page due to 403 Forbidden.`); + return; + } + + // Visit the page to load it in the Cypress browser + cy.visit(pageData.url); + + // Inject the axe-core library into the page for accessibility testing + cy.injectAxe(); + + // Perform an accessibility check using axe + cy.checkA11y(null, null, (violations) => { + // If any violations are found, log them and fail the test + if (violations.length > 0) { + // Log detailed information about each violation + logAccessibilityViolations(violations, pageName); + + // Throw an error to fail the test, including a formatted list of violations + throw new Error( + `${violations.length} accessibility violations found on ${pageName}:\n${formatViolations( + violations + )}` + ); + } + }); + }); + }); + }); + + /** + * Logs detailed information about accessibility violations to the console. + * @param {Array} violations - The list of accessibility violations. + * @param {string} pageName - The name of the page being tested. + */ + function logAccessibilityViolations(violations, pageName) { + // Log the number of violations on the page + cy.task('log', `\nAccessibility violations on ${pageName}: ${violations.length}\n`); + violations.forEach(({ id, impact, description, nodes }) => { + // Log individual violation details + cy.task('log', `Violation ID: ${id}`); + cy.task('log', `Impact: ${impact}`); + cy.task('log', `Description: ${description}`); + cy.task('log', `Affected Elements: ${nodes.map((node) => node.target).join(', ')}`); + cy.task('log', '---------------------------'); + }); + } + + /** + * Formats the list of accessibility violations into a readable string. + * @param {Array} violations - The list of accessibility violations. + * @returns {string} - A formatted string of violation details. + */ + function formatViolations(violations) { + return violations + .map( + ({ id, impact, description, nodes }) => + `- Violation ID: ${id}\n Impact: ${impact}\n Description: ${description}\n Affected Elements: ${nodes + .map((node) => node.target) + .join(', ')}\n` + ) + .join('\n'); + } + }); + \ No newline at end of file diff --git a/web/themes/custom/contribtracker/cypress/e2e/responsive-layout-check.cy.js b/web/themes/custom/contribtracker/cypress/e2e/responsive-layout-check.cy.js new file mode 100644 index 00000000..6b2ad7bd --- /dev/null +++ b/web/themes/custom/contribtracker/cypress/e2e/responsive-layout-check.cy.js @@ -0,0 +1,32 @@ +// Describe the test suite for Responsive Layout Validation +describe('Responsive Layout Validation', { tags: ['critical-path'] }, () => { + // Viewports to test + const viewports = [ + { device: 'Mobile', width: 375, height: 667 }, + { device: 'Tablet', width: 768, height: 1024 }, + { device: 'Desktop', width: 1440, height: 900 }, + ]; + + // Test case for each viewport + viewports.forEach(({ device, width, height }) => { + it(`should render correctly on ${device} (${width}x${height})`, () => { + cy.viewport(width, height); + cy.visit('/'); + + // Validate elements are visible + cy.get('header').should('be.visible'); + cy.get('main').should('be.visible'); + cy.get('footer').should('be.visible'); + + // Validate layout integrity: main should not overlap footer + cy.get('main').then(($main) => { + const mainRect = $main[0].getBoundingClientRect(); + cy.get('footer').then(($footer) => { + const footerRect = $footer[0].getBoundingClientRect(); + expect(mainRect.bottom).to.be.at.most(footerRect.top, 'Main content ends before the footer starts'); + }); + }); + }); + }); + }); + \ No newline at end of file diff --git a/web/themes/custom/contribtracker/cypress/support/e2e.js b/web/themes/custom/contribtracker/cypress/support/e2e.js index 3945e152..a612a0dc 100644 --- a/web/themes/custom/contribtracker/cypress/support/e2e.js +++ b/web/themes/custom/contribtracker/cypress/support/e2e.js @@ -1,5 +1,5 @@ import './commands'; import 'cypress-real-events'; // cypress inbuilt trigger() will only affect events in JavaScript and will not trigger any effects in CSS. See for more details: https://docs.cypress.io/api/commands/hover - +import 'cypress-axe'; import registerCypressGrep from '@cypress/grep'; registerCypressGrep(); diff --git a/web/themes/custom/contribtracker/package-lock.json b/web/themes/custom/contribtracker/package-lock.json index 2cdcaf8c..2fb0c275 100644 --- a/web/themes/custom/contribtracker/package-lock.json +++ b/web/themes/custom/contribtracker/package-lock.json @@ -25,9 +25,11 @@ "@typescript-eslint/eslint-plugin": "^8.18.0", "@typescript-eslint/parser": "^8.18.0", "autoprefixer": "^10.4.20", + "axe-core": "^4.10.2", "breakpoint-sass": "^3.0.0", "browser-sync": "^3.0.2", "cypress": "13.17.0", + "cypress-axe": "^1.5.0", "cypress-real-events": "^1.13.0", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", @@ -2415,6 +2417,15 @@ "dev": true, "license": "MIT" }, + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -4390,6 +4401,19 @@ "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, + "node_modules/cypress-axe": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.5.0.tgz", + "integrity": "sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "axe-core": "^3 || ^4", + "cypress": "^10 || ^11 || ^12 || ^13" + } + }, "node_modules/cypress-real-events": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.13.0.tgz", diff --git a/web/themes/custom/contribtracker/package.json b/web/themes/custom/contribtracker/package.json index c0a775df..f8d48435 100644 --- a/web/themes/custom/contribtracker/package.json +++ b/web/themes/custom/contribtracker/package.json @@ -26,9 +26,11 @@ "@typescript-eslint/eslint-plugin": "^8.18.0", "@typescript-eslint/parser": "^8.18.0", "autoprefixer": "^10.4.20", + "axe-core": "^4.10.2", "breakpoint-sass": "^3.0.0", "browser-sync": "^3.0.2", "cypress": "13.17.0", + "cypress-axe": "^1.5.0", "cypress-real-events": "^1.13.0", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0",