From 6eeae815c00de38e20dbb7346aa85c33075909c7 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 11 Dec 2024 07:39:12 +0000 Subject: [PATCH 01/15] WIP: Accessibility e2e test suite and support --- .gitignore | 2 + cypress.config.ts | 16 +++- cypress/e2e/tests/accessibility/login.spec.ts | 19 +++++ cypress/globals.d.ts | 5 ++ cypress/support/commands/accessiblity.ts | 82 +++++++++++++++++++ cypress/support/e2e.ts | 3 + .../support/plugins/accessibility/index.ts | 67 +++++++++++++++ cypress/tsconfig.json | 3 +- package.json | 7 +- yarn.lock | 66 ++++++--------- 10 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 cypress/e2e/tests/accessibility/login.spec.ts create mode 100644 cypress/support/commands/accessiblity.ts create mode 100644 cypress/support/plugins/accessibility/index.ts diff --git a/.gitignore b/.gitignore index 302960cdf52..45366e5d13b 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,8 @@ sw.* # Cypress e2e testing cypress/videos cypress/screenshots +cypress/accessibility +setupTestEnv.sh # Storybook storybook-static/ diff --git a/cypress.config.ts b/cypress.config.ts index dcf8600021b..d9297fcd3a6 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'cypress'; import { removeDirectory } from 'cypress-delete-downloads-folder'; import { getSpecPattern } from '@/scripts/cypress'; +import path from 'path'; + // Required for env vars to be available in cypress require('dotenv').config(); @@ -9,13 +11,19 @@ require('dotenv').config(); * VARIABLES */ const hasCoverage = (process.env.TEST_INSTRUMENT === 'true') || false; // Add coverage if instrumented -const testDirs = ['priority', 'components', 'setup', 'pages', 'navigation', 'global-ui', 'features', 'extensions']; +let testDirs = ['accessibility', 'priority', 'components', 'setup', 'pages', 'navigation', 'global-ui', 'features', 'extensions']; const skipSetup = process.env.TEST_SKIP?.includes('setup'); const baseUrl = (process.env.TEST_BASE_URL || 'https://localhost:8005').replace(/\/$/, ''); const DEFAULT_USERNAME = 'admin'; const username = process.env.TEST_USERNAME || DEFAULT_USERNAME; const apiUrl = process.env.API || (baseUrl.endsWith('/dashboard') ? baseUrl.split('/').slice(0, -1).join('/') : baseUrl); +if (process.env.TEST_DIRS) { + testDirs = process.env.TEST_DIRS.split(',').map((s) => s.trim()); + + console.log(` Using test dirs: ${ testDirs }`); +} + /** * LOGS: * Summary of the environment variables that we have detected (or are going ot use) @@ -94,7 +102,8 @@ export default defineConfig({ azureClientId: process.env.AZURE_CLIENT_ID, azureClientSecret: process.env.AZURE_CLIENT_SECRET, customNodeIp: process.env.CUSTOM_NODE_IP, - customNodeKey: process.env.CUSTOM_NODE_KEY + customNodeKey: process.env.CUSTOM_NODE_KEY, + a11yFolder: path.join('.', 'cypress', 'accessibility'), }, e2e: { fixturesFolder: 'cypress/e2e/blueprints', @@ -103,6 +112,9 @@ export default defineConfig({ require('@cypress/code-coverage/task')(on, config); require('@cypress/grep/src/plugin')(config); // For more info: https://www.npmjs.com/package/cypress-delete-downloads-folder + + require('./cypress/support/plugins/accessibility').default(on, config); + on('task', { removeDirectory }); return config; diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts new file mode 100644 index 00000000000..3c3dc43e62d --- /dev/null +++ b/cypress/e2e/tests/accessibility/login.spec.ts @@ -0,0 +1,19 @@ +// const WAIT = 600; + +describe('Login page a11y testing', { tags: ['@adminUser', '@standardUser'] }, () => { + it('wcag21aa test', () => { + cy.visit(`${ Cypress.config().baseUrl }/auth/login?local`); + cy.injectAxe(); + // cy.wait(WAIT); + cy.checkAccessibility(); + }); + + it('username fieldt', () => { + cy.visit(`${ Cypress.config().baseUrl }/auth/login?local`); + cy.injectAxe(); + // cy.wait(WAIT); + // cy.checkPageAccessibility(); + + cy.checkAccessibility('#username'); + }); +}); diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 060c3b6e1eb..6ea5dc32ac8 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -168,6 +168,11 @@ declare global { * Check if the vai FF is enabled */ isVaiCacheEnabled(): Chainable; + + /** + * Run an accessibility check on the current page or the specified element + */ + checkAccessibility(selector?: string); } } } diff --git a/cypress/support/commands/accessiblity.ts b/cypress/support/commands/accessiblity.ts new file mode 100644 index 00000000000..5cbb2d31e42 --- /dev/null +++ b/cypress/support/commands/accessiblity.ts @@ -0,0 +1,82 @@ +import { createHtmlReport } from 'axe-html-reporter'; + +// Custom violation callback function that prints a list of violations +const severityIndicators = { + minor: '⚪', + moderate: '🟡', + serious: '🟠', + critical: '🔴', +}; + +const RULES = { rules: { 'color-contrast': { enabled: false } } }; + +// Define at the top of the spec file or just import it +function terminalLog(violations) { + cy.task( + 'log', + `${ violations.length } accessibility violation${ + violations.length === 1 ? '' : 's' + } ${ violations.length === 1 ? 'was' : 'were' } detected` + ); + // pluck specific keys to keep the table readable + const violationData = violations.map( + ({ + id, impact, description, nodes + }) => ({ + id, + impact, + description, + nodes: nodes.length + }) + ); + + cy.task('table', violationData); +} + +function logToFile(violations) { + cy.writeFile('accessibilityReport.json', `${ JSON.stringify(violations, null, 2) } \n`, { flag: 'a+' }); +} + +function printAccessibilityViolations(violations) { + // Log to the console + terminalLog(violations); + logToFile(violations); + + cy.task('a11y', violations); + + // Log in Cypress + violations.forEach((violation) => { + const nodes = Cypress.$(violation.nodes.map((item) => item.target).join(',')); + + Cypress.log({ + name: `${ severityIndicators[violation.impact] } A11y`, + consoleProps: () => violation, + $el: nodes, + message: `[${ violation.help }][${ violation.helpUrl }]` + }); + + violation.nodes.forEach(({ target }) => { + Cypress.log({ + name: `🔨`, + consoleProps: () => violation, + $el: Cypress.$(target.join(',')), + message: target + }); + + cy.get(target.join(', ')).then(($el) => { + $el.css('border', '2px solid red'); + }); + + // Highlight each node so that they are visible when we take a screenshot + // Cypress.$(target.join(',')).css('style', 'border: 2px solid red'); + }); + }); + + cy.task('log', Cypress.currentTest); + cy.screenshot(`a11y_${ Cypress.currentTest.title }`); +} + +// skipFailures = true will not fail the test when there are accessibility failures +Cypress.Commands.add('checkAccessibility', (subject: any) => { + cy.checkA11y(subject, RULES, printAccessibilityViolations, true); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 0078bedba18..74b89319ed8 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -2,9 +2,12 @@ import '@cypress/code-coverage/support'; import './commands/commands'; import './commands/chainable'; import './commands/rancher-api-commands'; +import './commands/accessiblity'; + import registerCypressGrep from '@cypress/grep/src/support'; import { addCustomCommand } from 'cypress-delete-downloads-folder'; import 'cypress-mochawesome-reporter/register'; +import 'cypress-axe'; registerCypressGrep(); addCustomCommand(); diff --git a/cypress/support/plugins/accessibility/index.ts b/cypress/support/plugins/accessibility/index.ts new file mode 100644 index 00000000000..653267bfcab --- /dev/null +++ b/cypress/support/plugins/accessibility/index.ts @@ -0,0 +1,67 @@ +/* eslint-disable no-console */ +import * as fs from 'fs'; +import * as path from 'path'; +import { createHtmlReport } from 'axe-html-reporter'; + +const allViolations = [] as any[]; +let folder; + +function registerHooks(on, config) { + // Get the folder to write the reports into + folder = config.env.a11yFolder; + + // fs.rmdirSync(folder); + if (!fs.existsSync(folder)) { + fs.mkdirSync(folder); + } + + on('task', { + a11y(violations: any[]) { + allViolations.push(...violations); + + console.log(this); + + return null; + } + }); + + on('task', { + log(message) { + console.log(message); + + return null; + }, + table(message) { + console.table(message); + + return null; + } + }); + + on('before:spec', (spec) => { + console.log('Before spec'); + console.log(spec); + }); + + on('after:run', () => { + fs.writeFileSync(path.join(folder, 'accessibility.json'), JSON.stringify(allViolations, null, 2)); + + const reportHTML = createHtmlReport({ + results: { violations: allViolations }, + options: { + projectKey: 'Rancher Manager', + doNotCreateReportFile: true, + }, + }); + + fs.writeFileSync(path.join(folder, 'accessibility.html'), reportHTML); + + // Write the validation data to disk and transform to HTML + + return null; + }); + + return config; +} + +export default registerHooks; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 19ca6aeb9ce..5a16432a6f2 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -18,7 +18,8 @@ "noEmit": true, "types": [ "cypress", - "@cypress/grep" + "@cypress/grep", + "cypress-axe" ] }, "include": [ diff --git a/package.json b/package.json index 09c399e0829..ca9ca6616c1 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,8 @@ "@vue/test-utils": "2.0.2", "@vue/vue3-jest": "27.0.0", "add": "2.0.6", + "axe-core": "^4.10.2", + "axe-html-reporter": "^2.2.11", "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.1.0", "babel-jest": "27.5.1", @@ -170,6 +172,7 @@ "csv-loader": "3.0.3", "cy2": "4.0.9", "cypress": "11.1.0", + "cypress-axe": "^1.5.0", "cypress-delete-downloads-folder": "0.0.4", "cypress-mochawesome-reporter": "^3.8.2", "eslint": "7.32.0", @@ -179,6 +182,7 @@ "eslint-plugin-cypress": "2.12.1", "eslint-plugin-import": "2.23.4", "eslint-plugin-jest": "24.4.0", + "eslint-plugin-local-rules": "link:./eslint-plugin-local-rules", "eslint-plugin-node": "11.1.0", "eslint-plugin-vue": "9.10.0", "flush-promises": "1.0.2", @@ -197,8 +201,7 @@ "webpack-virtual-modules": "0.4.3", "worker-loader": "3.0.8", "yaml-lint": "1.7.0", - "yarn": "1.22.18", - "eslint-plugin-local-rules": "link:./eslint-plugin-local-rules" + "yarn": "1.22.18" }, "resolutions": { "html-webpack-plugin": "^5.0.0" diff --git a/yarn.lock b/yarn.lock index 9bba36ee0c5..845ab97cd1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5202,6 +5202,18 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== +axe-core@^4.10.2: + version "4.10.2" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" + integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== + +axe-html-reporter@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/axe-html-reporter/-/axe-html-reporter-2.2.11.tgz#749a4d5836f6aebb6780049933c2f6030e06c7d0" + integrity sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ== + dependencies: + mustache "^4.0.1" + axios-retry@3.1.9: version "3.1.9" resolved "https://registry.npmjs.org/axios-retry/-/axios-retry-3.1.9.tgz#6c30fc9aeb4519aebaec758b90ef56fa03fe72e8" @@ -6636,6 +6648,11 @@ cy2@4.0.9: source-map-support "^0.5.21" tmp "^0.2.1" +cypress-axe@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.5.0.tgz#95082734583da77b51ce9b7784e14a442016c7a1" + integrity sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ== + cypress-delete-downloads-folder@0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/cypress-delete-downloads-folder/-/cypress-delete-downloads-folder-0.0.4.tgz#00693d4d7a36b552e7a11148e49d2c4c34ed3ec3" @@ -9057,18 +9074,7 @@ html-tags@^3.3.1: tapable "^1.1.3" util.promisify "1.0.0" -"html-webpack-plugin-5@npm:html-webpack-plugin@^5": - version "5.6.0" - resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== - dependencies: - "@types/html-minifier-terser" "^6.0.0" - html-minifier-terser "^6.0.2" - lodash "^4.17.21" - pretty-error "^4.0.0" - tapable "^2.0.0" - -html-webpack-plugin@^5.0.0, html-webpack-plugin@^5.1.0: +"html-webpack-plugin-5@npm:html-webpack-plugin@^5", html-webpack-plugin@^5.0.0, html-webpack-plugin@^5.1.0: version "5.6.0" resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== @@ -11237,6 +11243,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +mustache@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + mz@^2.4.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -13667,7 +13678,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13684,15 +13695,6 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -13744,7 +13746,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13765,13 +13767,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -15047,7 +15042,7 @@ worker-loader@3.0.8: loader-utils "^2.0.0" schema-utils "^3.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15073,15 +15068,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 16c46116cc0a753e7c7e63b55f2219ae55d7b2fb Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 16:12:02 +0000 Subject: [PATCH 02/15] Add e2e job for accessibility tests --- .github/workflows/test.yaml | 59 +++++++ cypress.config.ts | 8 +- cypress/e2e/tests/accessibility/login.spec.ts | 26 +-- cypress/globals.d.ts | 7 +- cypress/support/commands/accessiblity.ts | 136 ++++++++++++---- .../support/plugins/accessibility/index.ts | 149 +++++++++++++++++- 6 files changed, 330 insertions(+), 55 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 646866f4078..508d7afcac6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -140,7 +140,66 @@ jobs: name: ${{github.run_number}}-${{github.run_attempt}}-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} path: cypress/screenshots + a11y-test: + if: "!contains( github.event.pull_request.labels.*.name, 'ci/skip-e2e')" + needs: e2e-ui-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Setup env + uses: ./.github/actions/setup + + # Installing fixed version of Chrome since latest version does not work (128 didn't work) + # Leaving this here again in case we need to pin to a specific Chrome version in the future + - name: Install Chrome 127 + run: | + sudo apt-get install -y wget libu2f-udev + cd /tmp + wget -q http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_127.0.6533.72-1_amd64.deb + sudo dpkg -i google-chrome-stable_127.0.6533.72-1_amd64.deb + sudo apt-get install -y --allow-downgrades ./google-chrome-stable_127.0.6533.72-1_amd64.deb + google-chrome --version + - name: Download e2e build + uses: actions/download-artifact@v4 + with: + name: ${{ env.E2E_BUILD_DIST_NAME }} + path: ${{ env.E2E_BUILD_DIST_DIR }} + - name: Download e2e build ember + uses: actions/download-artifact@v4 + with: + name: ${{ env.E2E_BUILD_DIST_EMBER_NAME }} + path: ${{ env.E2E_BUILD_DIST_EMBER_DIR }} + + - name: Run Rancher + run: yarn e2e:docker + + - name: Setup Rancher and user + run: | + yarn e2e:prod + env: + GREP_TAGS: "@adminSetup+@accessibility --@jenkins" + TEST_USERNAME: admin + TEST_ONLY: setup + - name: Run user tests + run: | + yarn e2e:prod + [ "$BUILD_DASHBOARD" != "false" ] || exit 0 + env: + TEST_A11Y: true + TEST_SKIP: setup + GREP_TAGS: "@admin+@accessibility --@jenkins" + TEST_USERNAME: admin + + # - name: Upload screenshots + # uses: actions/upload-artifact@v4 + # if: ${{ failure() }} + # with: + # name: ${{github.run_number}}-${{github.run_attempt}}-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} + # path: cypress/screenshots + unit-test: runs-on: ubuntu-latest steps: diff --git a/cypress.config.ts b/cypress.config.ts index d9297fcd3a6..65f22a84ce6 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -11,17 +11,15 @@ require('dotenv').config(); * VARIABLES */ const hasCoverage = (process.env.TEST_INSTRUMENT === 'true') || false; // Add coverage if instrumented -let testDirs = ['accessibility', 'priority', 'components', 'setup', 'pages', 'navigation', 'global-ui', 'features', 'extensions']; +let testDirs = ['priority', 'components', 'setup', 'pages', 'navigation', 'global-ui', 'features', 'extensions']; const skipSetup = process.env.TEST_SKIP?.includes('setup'); const baseUrl = (process.env.TEST_BASE_URL || 'https://localhost:8005').replace(/\/$/, ''); const DEFAULT_USERNAME = 'admin'; const username = process.env.TEST_USERNAME || DEFAULT_USERNAME; const apiUrl = process.env.API || (baseUrl.endsWith('/dashboard') ? baseUrl.split('/').slice(0, -1).join('/') : baseUrl); -if (process.env.TEST_DIRS) { - testDirs = process.env.TEST_DIRS.split(',').map((s) => s.trim()); - - console.log(` Using test dirs: ${ testDirs }`); +if (process.env.TEST_A11Y) { + testDirs = ['accessibility']; } /** diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts index 3c3dc43e62d..b809d4c98e4 100644 --- a/cypress/e2e/tests/accessibility/login.spec.ts +++ b/cypress/e2e/tests/accessibility/login.spec.ts @@ -1,19 +1,23 @@ -// const WAIT = 600; +import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; + +describe('Login page a11y testing', { tags: ['@adminUser', '@accessibility'] }, () => { + const loginPage = new LoginPagePo(); -describe('Login page a11y testing', { tags: ['@adminUser', '@standardUser'] }, () => { it('wcag21aa test', () => { - cy.visit(`${ Cypress.config().baseUrl }/auth/login?local`); + loginPage.goTo(); + loginPage.waitForPage(); + cy.injectAxe(); - // cy.wait(WAIT); - cy.checkAccessibility(); + cy.checkPageAccessibility(); }); - it('username fieldt', () => { - cy.visit(`${ Cypress.config().baseUrl }/auth/login?local`); - cy.injectAxe(); - // cy.wait(WAIT); - // cy.checkPageAccessibility(); + it('locale selector', () => { + loginPage.goTo(); + loginPage.waitForPage(); - cy.checkAccessibility('#username'); + cy.injectAxe(); + cy.get('[data-testid="locale-selector"]').click(); + cy.checkPageAccessibility(); + cy.checkElementAccessibility('#username', 'Username field checks'); }); }); diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 6ea5dc32ac8..061bcb547e4 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -172,7 +172,12 @@ declare global { /** * Run an accessibility check on the current page or the specified element */ - checkAccessibility(selector?: string); + checkPageAccessibility(description?: string); + + /** + * Run an accessibility check on the specified element + */ + checkElementAccessibility(selector: any, description?: string); } } } diff --git a/cypress/support/commands/accessiblity.ts b/cypress/support/commands/accessiblity.ts index 5cbb2d31e42..a67ed479cb9 100644 --- a/cypress/support/commands/accessiblity.ts +++ b/cypress/support/commands/accessiblity.ts @@ -1,6 +1,7 @@ -import { createHtmlReport } from 'axe-html-reporter'; +import { a11yScreenshot } from '../plugins/accessibility'; // Custom violation callback function that prints a list of violations +// Used when logging to the Cypress log const severityIndicators = { minor: '⚪', moderate: '🟡', @@ -8,9 +9,14 @@ const severityIndicators = { critical: '🔴', }; +// Ignore color contrast for now const RULES = { rules: { 'color-contrast': { enabled: false } } }; -// Define at the top of the spec file or just import it +// Used to track where multiple checks are done in a test to ensure we save +// the screenshots for them to unique filenames +const screenshotIndexes: {[key: string]: number} = {}; + +// Log violations to the terminal function terminalLog(violations) { cy.task( 'log', @@ -33,50 +39,116 @@ function terminalLog(violations) { cy.task('table', violationData); } -function logToFile(violations) { - cy.writeFile('accessibilityReport.json', `${ JSON.stringify(violations, null, 2) } \n`, { flag: 'a+' }); -} - -function printAccessibilityViolations(violations) { - // Log to the console - terminalLog(violations); - logToFile(violations); +/** + * Log the violations in several ways: + * 1. Log to the terminal + * 2. Log to a file + * 3. Log to the Cypress log + * 4. Save screenshot of the violations + */ +function getAccessibilityViolationsCallback(description?: string) { + return function printAccessibilityViolations(violations) { + terminalLog(violations); // Log to the console - cy.task('a11y', violations); + const title = Cypress.currentTest.titlePath.join(', '); + const index = screenshotIndexes[title] || 1; + const testPath = Cypress.currentTest.titlePath; + const lastName = Cypress.currentTest.titlePath[Cypress.currentTest.titlePath.length - 1]; - // Log in Cypress - violations.forEach((violation) => { - const nodes = Cypress.$(violation.nodes.map((item) => item.target).join(',')); + testPath.push(description || `${ lastName } (#${ index })`); - Cypress.log({ - name: `${ severityIndicators[violation.impact] } A11y`, - consoleProps: () => violation, - $el: nodes, - message: `[${ violation.help }][${ violation.helpUrl }]` + cy.task('a11y', { + violations, + titlePath: testPath, }); - violation.nodes.forEach(({ target }) => { + // Log in Cypress + violations.forEach((violation) => { + const nodes = Cypress.$(violation.nodes.map((item) => item.target).join(',')); + Cypress.log({ - name: `🔨`, + name: `${ severityIndicators[violation.impact] } A11y`, consoleProps: () => violation, - $el: Cypress.$(target.join(',')), - message: target + $el: nodes, + message: `[${ violation.help }][${ violation.helpUrl }]` }); - cy.get(target.join(', ')).then(($el) => { - $el.css('border', '2px solid red'); + violation.nodes.forEach(({ target }) => { + Cypress.log({ + name: `🔨`, + consoleProps: () => violation, + $el: Cypress.$(target.join(',')), + message: target + }); + + // Store the existing border and change it to clearly show the elements with violations + cy.get(target.join(', ')).invoke('css', 'border').then((border) => { + cy.get(target.join(', ')).then(($el) => { + const existingBorder = $el.data('border'); + + // If we have the original border, don't store again = covers a case an element has multiple violations + // and we would lose the original border + if (!existingBorder) { + $el.data('border', border); + } + + $el.css('border', '2px solid red'); + }); + }); }); + }); + + cy.screenshot(`a11y_${ Cypress.currentTest.title }_${ index }`); - // Highlight each node so that they are visible when we take a screenshot - // Cypress.$(target.join(',')).css('style', 'border: 2px solid red'); + // cy.screenshot(`a11y_${ Cypress.currentTest.title }_${ index }`, { + // onAfterScreenshot($el, props) { + // a11yScreenshot({ + // titlePath: testPath, + // props, + // }); + // }, + // }); + + cy.task('a11yScreenshot', { + titlePath: testPath, + name: `a11y_${ Cypress.currentTest.title }_${ index }` }); - }); - cy.task('log', Cypress.currentTest); - cy.screenshot(`a11y_${ Cypress.currentTest.title }`); + screenshotIndexes[title] = index + 1; + + // Reset the borders that were added to mark the elements with violations + violations.forEach((violation) => { + violation.nodes.forEach(({ target }) => { + cy.get(target.join(', ')).then(($el) => { + const border = $el.data('border'); + + if (!border.startsWith('0px none')) { + $el.css('border', $el.data('border')); + } else { + $el.css('border', ''); + } + + if ($el.attr('style')?.length === 0) { + $el.removeAttr('style'); + } + }); + }); + }); + } } +/** + * Checks accessibility of the entire page + */ +// skipFailures = true will not fail the test when there are accessibility failures +Cypress.Commands.add('checkPageAccessibility', (description?: string) => { + cy.checkA11y(undefined, RULES, getAccessibilityViolationsCallback(description), true); +}); + +/** + * Checks accessibility of a specific element + */ // skipFailures = true will not fail the test when there are accessibility failures -Cypress.Commands.add('checkAccessibility', (subject: any) => { - cy.checkA11y(subject, RULES, printAccessibilityViolations, true); +Cypress.Commands.add('checkElementAccessibility', (subject: any, description?: string) => { + cy.checkA11y(subject, RULES, getAccessibilityViolationsCallback(description), true); }); diff --git a/cypress/support/plugins/accessibility/index.ts b/cypress/support/plugins/accessibility/index.ts index 653267bfcab..a3f09c6707d 100644 --- a/cypress/support/plugins/accessibility/index.ts +++ b/cypress/support/plugins/accessibility/index.ts @@ -1,11 +1,115 @@ /* eslint-disable no-console */ import * as fs from 'fs'; import * as path from 'path'; +import * as sha from 'sha.js'; import { createHtmlReport } from 'axe-html-reporter'; +const calcHash = function(str) { + return sha.default('sha256').update(str).digest('hex'); +} + +function createPath(testPath: string[]) { + const currentSpec = chain[chain.length - 1]; + + let found = currentSpec; + + for (const p of testPath) { + const f = found.children.filter((item) => item.name === p); + + if (f.length === 1) { + found = f[0]; + } else { + const c = { + name: p, + children: [], + violations: [], + leaf: false, + }; + + found.children.push(c); + found = c; + } + } + + return found; +} + +export type TestViolation = { + name: string; + children: TestViolation[]; + violations: any[]; + leaf: boolean; + screenshot?: string; +}; + +export type Options = { + violations: any[]; + titlePath: string[]; +}; + +// Root chain +const chain: TestViolation[] = [{ + name: 'Root', + children: [], + violations: [], + leaf: false, +}]; + const allViolations = [] as any[]; let folder; +// Tidy up the chain +function tidy(item: TestViolation) { + item.children.forEach((i) => tidy(i)); + + if (item.violations.length === 0 && item.children.length === 1) { + if (item.children[0].leaf) { + // Collapse up + item.violations = item.children[0].violations; + item.children = []; + } + } +} + +export function a11yScreenshot(options: any) { + const { titlePath, props} = options; + const found = createPath(titlePath); + + found.screenshot = props.path; +} + +function deDuplicate(violations: any[]) { + const result: any[] = []; + const seen: {[key: string]: any} = {}; + + violations.forEach((item) => { + const copy = JSON.parse(JSON.stringify(item)); + + delete copy.nodes; + + const hash = calcHash(JSON.stringify(copy)); + + if (!seen[hash]) { + seen[hash] = item; + result.push(item); + } else { + // Merge the nodes + const existing = seen[hash]; + + item.nodes.forEach((node) => { + const str = JSON.stringify(node); + const exists = existing.nodes.find((n) => JSON.stringify(n) === str); + + if (!exists) { + existing.nodes.push(node); + } + }); + } + }); + + return result; +} + function registerHooks(on, config) { // Get the folder to write the reports into folder = config.env.a11yFolder; @@ -16,10 +120,25 @@ function registerHooks(on, config) { } on('task', { - a11y(violations: any[]) { + a11y(options: Options) { + const { violations, titlePath} = options; + const found = createPath(titlePath); + allViolations.push(...violations); - console.log(this); + found.violations.push(...violations); + found.leaf = true; + + return null; + }, + + a11yScreenshot(options: any ) { + const { titlePath, name} = options; + const found = createPath(titlePath); + + console.log('>>>>>>>>>>>>>>>>>>> SCREEN SHOT'); + + found.screenshot = name; return null; } @@ -39,15 +158,33 @@ function registerHooks(on, config) { }); on('before:spec', (spec) => { - console.log('Before spec'); - console.log(spec); + const newSpec = { + name: spec.baseName, + children: [], + violations: [], + leaf: false, + }; + + chain[0].children.push(newSpec); + + // Push the spec onto the chain + chain.push(newSpec); }); + on('after:spec', (spec) => { + // Pop the spec off of the chain + chain.pop(); + }); + on('after:run', () => { - fs.writeFileSync(path.join(folder, 'accessibility.json'), JSON.stringify(allViolations, null, 2)); + const root = chain[0]; + + tidy(root); + + fs.writeFileSync(path.join(folder, 'accessibility.json'), JSON.stringify(root.children, null, 2)); const reportHTML = createHtmlReport({ - results: { violations: allViolations }, + results: { violations: deDuplicate(allViolations) }, options: { projectKey: 'Rancher Manager', doNotCreateReportFile: true, From 561fe974ed6c8a5e097a9ef0f50e139553a55827 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 16:25:55 +0000 Subject: [PATCH 03/15] Fix a11y tests --- .github/workflows/test.yaml | 2 +- cypress/e2e/tests/setup/rancher-setup.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 508d7afcac6..42c63e05abd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -190,7 +190,7 @@ jobs: env: TEST_A11Y: true TEST_SKIP: setup - GREP_TAGS: "@admin+@accessibility --@jenkins" + GREP_TAGS: "@adminUser+@accessibility --@jenkins" TEST_USERNAME: admin # - name: Upload screenshots diff --git a/cypress/e2e/tests/setup/rancher-setup.spec.ts b/cypress/e2e/tests/setup/rancher-setup.spec.ts index 8e584e895a7..cb84e2874ae 100644 --- a/cypress/e2e/tests/setup/rancher-setup.spec.ts +++ b/cypress/e2e/tests/setup/rancher-setup.spec.ts @@ -6,7 +6,7 @@ import { serverUrlLocalhostCases, urlWithTrailingForwardSlash, httpUrl, nonUrlCa // Cypress or the GrepTags avoid to run multiples times the same test for each tag used. // This is a temporary solution till initialization is not handled as a test -describe('Rancher setup', { tags: ['@adminUserSetup', '@standardUserSetup', '@setup', '@components', '@navigation', '@charts', '@explorer', '@explorer2', '@extensions', '@fleet', '@generic', '@globalSettings', '@manager', '@userMenu', '@usersAndAuths', '@elemental', '@vai'] }, () => { +describe('Rancher setup', { tags: ['@adminUserSetup', '@standardUserSetup', '@setup', '@components', '@navigation', '@charts', '@explorer', '@explorer2', '@extensions', '@fleet', '@generic', '@globalSettings', '@manager', '@userMenu', '@usersAndAuths', '@elemental', '@vai', '@accessibility'] }, () => { const rancherSetupLoginPage = new RancherSetupLoginPagePo(); const rancherSetupConfigurePage = new RancherSetupConfigurePage(); const homePage = new HomePagePo(); From dca4de198d8610e4ed8bb1ed588e5ff972be8c94 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 17:04:34 +0000 Subject: [PATCH 04/15] Remove test --- cypress/e2e/tests/accessibility/login.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts index b809d4c98e4..d1ce465f174 100644 --- a/cypress/e2e/tests/accessibility/login.spec.ts +++ b/cypress/e2e/tests/accessibility/login.spec.ts @@ -11,13 +11,13 @@ describe('Login page a11y testing', { tags: ['@adminUser', '@accessibility'] }, cy.checkPageAccessibility(); }); - it('locale selector', () => { - loginPage.goTo(); - loginPage.waitForPage(); + // it('locale selector', () => { + // loginPage.goTo(); + // loginPage.waitForPage(); - cy.injectAxe(); - cy.get('[data-testid="locale-selector"]').click(); - cy.checkPageAccessibility(); - cy.checkElementAccessibility('#username', 'Username field checks'); - }); + // cy.injectAxe(); + // cy.get('[data-testid="locale-selector"]').click(); + // cy.checkPageAccessibility(); + // cy.checkElementAccessibility('#username', 'Username field checks'); + // }); }); From 98f8e8aa4207360dcc1c170216587450bcdf28e5 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 17:11:03 +0000 Subject: [PATCH 05/15] Fix lint issues --- cypress/support/commands/accessiblity.ts | 4 +-- .../support/plugins/accessibility/index.ts | 32 +++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/cypress/support/commands/accessiblity.ts b/cypress/support/commands/accessiblity.ts index a67ed479cb9..f61fee53adc 100644 --- a/cypress/support/commands/accessiblity.ts +++ b/cypress/support/commands/accessiblity.ts @@ -1,5 +1,3 @@ -import { a11yScreenshot } from '../plugins/accessibility'; - // Custom violation callback function that prints a list of violations // Used when logging to the Cypress log const severityIndicators = { @@ -134,7 +132,7 @@ function getAccessibilityViolationsCallback(description?: string) { }); }); }); - } + }; } /** diff --git a/cypress/support/plugins/accessibility/index.ts b/cypress/support/plugins/accessibility/index.ts index a3f09c6707d..10b8f418756 100644 --- a/cypress/support/plugins/accessibility/index.ts +++ b/cypress/support/plugins/accessibility/index.ts @@ -6,7 +6,7 @@ import { createHtmlReport } from 'axe-html-reporter'; const calcHash = function(str) { return sha.default('sha256').update(str).digest('hex'); -} +}; function createPath(testPath: string[]) { const currentSpec = chain[chain.length - 1]; @@ -20,10 +20,10 @@ function createPath(testPath: string[]) { found = f[0]; } else { const c = { - name: p, - children: [], + name: p, + children: [], violations: [], - leaf: false, + leaf: false, }; found.children.push(c); @@ -49,10 +49,10 @@ export type Options = { // Root chain const chain: TestViolation[] = [{ - name: 'Root', - children: [], + name: 'Root', + children: [], violations: [], - leaf: false, + leaf: false, }]; const allViolations = [] as any[]; @@ -72,7 +72,7 @@ function tidy(item: TestViolation) { } export function a11yScreenshot(options: any) { - const { titlePath, props} = options; + const { titlePath, props } = options; const found = createPath(titlePath); found.screenshot = props.path; @@ -121,7 +121,7 @@ function registerHooks(on, config) { on('task', { a11y(options: Options) { - const { violations, titlePath} = options; + const { violations, titlePath } = options; const found = createPath(titlePath); allViolations.push(...violations); @@ -133,11 +133,9 @@ function registerHooks(on, config) { }, a11yScreenshot(options: any ) { - const { titlePath, name} = options; + const { titlePath, name } = options; const found = createPath(titlePath); - console.log('>>>>>>>>>>>>>>>>>>> SCREEN SHOT'); - found.screenshot = name; return null; @@ -159,10 +157,10 @@ function registerHooks(on, config) { on('before:spec', (spec) => { const newSpec = { - name: spec.baseName, - children: [], + name: spec.baseName, + children: [], violations: [], - leaf: false, + leaf: false, }; chain[0].children.push(newSpec); @@ -174,13 +172,13 @@ function registerHooks(on, config) { on('after:spec', (spec) => { // Pop the spec off of the chain chain.pop(); - }); + }); on('after:run', () => { const root = chain[0]; tidy(root); - + fs.writeFileSync(path.join(folder, 'accessibility.json'), JSON.stringify(root.children, null, 2)); const reportHTML = createHtmlReport({ From 67e201d2c909ea74507f8fb6fe2a61ea35908000 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 17:21:37 +0000 Subject: [PATCH 06/15] Add report upload --- .github/workflows/test.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 42c63e05abd..944d6a4bd9c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -192,6 +192,12 @@ jobs: TEST_SKIP: setup GREP_TAGS: "@adminUser+@accessibility --@jenkins" TEST_USERNAME: admin + + - name: Upload html report + uses: actions/upload-artifact@v4 + with: + name: accessibility-report + path: cypress/accessibility/accessibility.html # - name: Upload screenshots # uses: actions/upload-artifact@v4 From 6a7c8c748c350272f3347e2ad088137bf586451b Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 17:35:51 +0000 Subject: [PATCH 07/15] Upload a11y screenshots --- .github/workflows/test.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 944d6a4bd9c..a501c960f0f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -199,12 +199,12 @@ jobs: name: accessibility-report path: cypress/accessibility/accessibility.html - # - name: Upload screenshots - # uses: actions/upload-artifact@v4 - # if: ${{ failure() }} - # with: - # name: ${{github.run_number}}-${{github.run_attempt}}-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} - # path: cypress/screenshots + - name: Upload screenshots + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: ${{github.run_number}}-${{github.run_attempt}}-a11y-screenshots + path: cypress/screenshots unit-test: runs-on: ubuntu-latest From 45422692e8ebb1e98b61546e0087628620619eef Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Tue, 17 Dec 2024 17:52:34 +0000 Subject: [PATCH 08/15] Remove failure gate on a11y screenshots --- .github/workflows/test.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a501c960f0f..7d7209ddece 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -201,7 +201,6 @@ jobs: - name: Upload screenshots uses: actions/upload-artifact@v4 - if: ${{ failure() }} with: name: ${{github.run_number}}-${{github.run_attempt}}-a11y-screenshots path: cypress/screenshots From 353a315e8015a1dc1fbb6950b49b7925ece10015 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 18 Dec 2024 08:47:44 +0000 Subject: [PATCH 09/15] Test debugging --- .github/workflows/test.yaml | 2 +- cypress/e2e/tests/accessibility/login.spec.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7d7209ddece..eaf12dcfcee 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -188,10 +188,10 @@ jobs: yarn e2e:prod [ "$BUILD_DASHBOARD" != "false" ] || exit 0 env: - TEST_A11Y: true TEST_SKIP: setup GREP_TAGS: "@adminUser+@accessibility --@jenkins" TEST_USERNAME: admin + TEST_A11Y: true - name: Upload html report uses: actions/upload-artifact@v4 diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts index d1ce465f174..17b05f51154 100644 --- a/cypress/e2e/tests/accessibility/login.spec.ts +++ b/cypress/e2e/tests/accessibility/login.spec.ts @@ -1,13 +1,19 @@ import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; describe('Login page a11y testing', { tags: ['@adminUser', '@accessibility'] }, () => { - const loginPage = new LoginPagePo(); it('wcag21aa test', () => { + const loginPage = new LoginPagePo(); + loginPage.goTo(); loginPage.waitForPage(); - cy.injectAxe(); + + cy.checkPageAccessibility(); + + loginPage.username().set('test'); + loginPage.submit(); + cy.checkPageAccessibility(); }); From 8cdcc274b535d0b008560b2943fcd59946fb854d Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 18 Dec 2024 08:54:14 +0000 Subject: [PATCH 10/15] Fix lint --- cypress.config.ts | 1 + cypress/e2e/tests/accessibility/login.spec.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.config.ts b/cypress.config.ts index 65f22a84ce6..9d27822df89 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -101,6 +101,7 @@ export default defineConfig({ azureClientSecret: process.env.AZURE_CLIENT_SECRET, customNodeIp: process.env.CUSTOM_NODE_IP, customNodeKey: process.env.CUSTOM_NODE_KEY, + accessibility: !!process.env.TEST_A11Y, // Are we running accessibility tests? a11yFolder: path.join('.', 'cypress', 'accessibility'), }, e2e: { diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts index 17b05f51154..cce3545b0e0 100644 --- a/cypress/e2e/tests/accessibility/login.spec.ts +++ b/cypress/e2e/tests/accessibility/login.spec.ts @@ -1,7 +1,6 @@ import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; describe('Login page a11y testing', { tags: ['@adminUser', '@accessibility'] }, () => { - it('wcag21aa test', () => { const loginPage = new LoginPagePo(); From 374b48778e675685ee570b46bee508f21cde7a3a Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 18 Dec 2024 09:20:03 +0000 Subject: [PATCH 11/15] Fix setup issue --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index eaf12dcfcee..4344144eea7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -180,7 +180,7 @@ jobs: run: | yarn e2e:prod env: - GREP_TAGS: "@adminSetup+@accessibility --@jenkins" + GREP_TAGS: "@adminUserSetup+@accessibility --@jenkins" TEST_USERNAME: admin TEST_ONLY: setup - name: Run user tests From f5b68c37bf37bf9fc9005fb0c3ec2d3ffa4d5311 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 18 Dec 2024 09:30:25 +0000 Subject: [PATCH 12/15] Tweak tests --- cypress/e2e/tests/accessibility/login.spec.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts index cce3545b0e0..d364bc46103 100644 --- a/cypress/e2e/tests/accessibility/login.spec.ts +++ b/cypress/e2e/tests/accessibility/login.spec.ts @@ -7,22 +7,19 @@ describe('Login page a11y testing', { tags: ['@adminUser', '@accessibility'] }, loginPage.goTo(); loginPage.waitForPage(); cy.injectAxe(); + loginPage.username().set('test user'); cy.checkPageAccessibility(); + }); - loginPage.username().set('test'); - loginPage.submit(); + it('locale selector', () => { + const loginPage = new LoginPagePo(); + loginPage.goTo(); + loginPage.waitForPage(); + cy.injectAxe(); + cy.get('[data-testid="locale-selector"]').click(); cy.checkPageAccessibility(); + cy.checkElementAccessibility('#username', 'Username field checks'); }); - - // it('locale selector', () => { - // loginPage.goTo(); - // loginPage.waitForPage(); - - // cy.injectAxe(); - // cy.get('[data-testid="locale-selector"]').click(); - // cy.checkPageAccessibility(); - // cy.checkElementAccessibility('#username', 'Username field checks'); - // }); }); From 217441591e2fe6d7d3274eb77c328690dbe328f8 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 18 Dec 2024 10:15:32 +0000 Subject: [PATCH 13/15] Create a11y report --- .github/workflows/test.yaml | 10 ++---- cypress.config.ts | 5 ++- cypress/support/commands/accessiblity.ts | 11 ++---- .../support/plugins/accessibility/index.ts | 35 ++++++++++++++++--- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4344144eea7..9f147ac34fa 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -193,17 +193,11 @@ jobs: TEST_USERNAME: admin TEST_A11Y: true - - name: Upload html report + - name: Upload report uses: actions/upload-artifact@v4 with: name: accessibility-report - path: cypress/accessibility/accessibility.html - - - name: Upload screenshots - uses: actions/upload-artifact@v4 - with: - name: ${{github.run_number}}-${{github.run_attempt}}-a11y-screenshots - path: cypress/screenshots + path: cypress/accessibility unit-test: runs-on: ubuntu-latest diff --git a/cypress.config.ts b/cypress.config.ts index 9d27822df89..9ad6aa5d974 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -112,7 +112,10 @@ export default defineConfig({ require('@cypress/grep/src/plugin')(config); // For more info: https://www.npmjs.com/package/cypress-delete-downloads-folder - require('./cypress/support/plugins/accessibility').default(on, config); + // Load Accessibility plugin if configured + if (process.env.TEST_A11Y) { + require('./cypress/support/plugins/accessibility').default(on, config); + } on('task', { removeDirectory }); diff --git a/cypress/support/commands/accessiblity.ts b/cypress/support/commands/accessiblity.ts index f61fee53adc..b58a46de655 100644 --- a/cypress/support/commands/accessiblity.ts +++ b/cypress/support/commands/accessiblity.ts @@ -98,17 +98,10 @@ function getAccessibilityViolationsCallback(description?: string) { cy.screenshot(`a11y_${ Cypress.currentTest.title }_${ index }`); - // cy.screenshot(`a11y_${ Cypress.currentTest.title }_${ index }`, { - // onAfterScreenshot($el, props) { - // a11yScreenshot({ - // titlePath: testPath, - // props, - // }); - // }, - // }); - + // Record the screenshot against the test and move it into the a11y folder cy.task('a11yScreenshot', { titlePath: testPath, + test: Cypress.currentTest, name: `a11y_${ Cypress.currentTest.title }_${ index }` }); diff --git a/cypress/support/plugins/accessibility/index.ts b/cypress/support/plugins/accessibility/index.ts index 10b8f418756..8a399196dd6 100644 --- a/cypress/support/plugins/accessibility/index.ts +++ b/cypress/support/plugins/accessibility/index.ts @@ -47,6 +47,12 @@ export type Options = { titlePath: string[]; }; +type Screenshot = { + name: string; + specName: string; + path: string; +}; + // Root chain const chain: TestViolation[] = [{ name: 'Root', @@ -56,6 +62,7 @@ const chain: TestViolation[] = [{ }]; const allViolations = [] as any[]; +const screenshots = [] as Screenshot[]; let folder; // Tidy up the chain @@ -136,7 +143,19 @@ function registerHooks(on, config) { const { titlePath, name } = options; const found = createPath(titlePath); - found.screenshot = name; + // Move the screenshot to the accessibility folder + const details = screenshots.find((s) => s.name === name); + + if (details) { + found.screenshot = path.join(details.specName, `${ name }.png`); + const screenFolder = path.join(folder, found.screenshot); + const destFile = path.join(screenFolder, `${ name }.png`); + + if (!fs.existsSync(screenFolder)) { + fs.mkdirSync(screenFolder); + } + fs.renameSync(details.path, destFile); + } return null; } @@ -169,11 +188,21 @@ function registerHooks(on, config) { chain.push(newSpec); }); - on('after:spec', (spec) => { + on('after:spec', () => { // Pop the spec off of the chain chain.pop(); }); + on('after:screenshot', (details) => { + const { name, specName, path } = details; + + screenshots.push({ + name, + specName, + path + }); + }); + on('after:run', () => { const root = chain[0]; @@ -191,8 +220,6 @@ function registerHooks(on, config) { fs.writeFileSync(path.join(folder, 'accessibility.html'), reportHTML); - // Write the validation data to disk and transform to HTML - return null; }); From b7a17466527dac72e4fce6e5ce027d2307617695 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Wed, 18 Dec 2024 10:41:12 +0000 Subject: [PATCH 14/15] Fix screenshot folder --- cypress/support/plugins/accessibility/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cypress/support/plugins/accessibility/index.ts b/cypress/support/plugins/accessibility/index.ts index 8a399196dd6..073cc5ab8cd 100644 --- a/cypress/support/plugins/accessibility/index.ts +++ b/cypress/support/plugins/accessibility/index.ts @@ -147,10 +147,11 @@ function registerHooks(on, config) { const details = screenshots.find((s) => s.name === name); if (details) { - found.screenshot = path.join(details.specName, `${ name }.png`); - const screenFolder = path.join(folder, found.screenshot); + const screenFolder = path.join(folder, details.specName); const destFile = path.join(screenFolder, `${ name }.png`); + found.screenshot = path.join(details.specName, `${ name }.png`); + if (!fs.existsSync(screenFolder)) { fs.mkdirSync(screenFolder); } From 6f982962211f903be057c9b00031d840c14e7162 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Fri, 10 Jan 2025 11:13:04 +0000 Subject: [PATCH 15/15] Build out more tests to cover a11y --- cypress/e2e/po/pages/account-api-keys.po.ts | 4 + cypress/e2e/tests/accessibility/login.spec.ts | 25 ----- cypress/e2e/tests/accessibility/shell.spec.ts | 95 +++++++++++++++++++ 3 files changed, 99 insertions(+), 25 deletions(-) delete mode 100644 cypress/e2e/tests/accessibility/login.spec.ts create mode 100644 cypress/e2e/tests/accessibility/shell.spec.ts diff --git a/cypress/e2e/po/pages/account-api-keys.po.ts b/cypress/e2e/po/pages/account-api-keys.po.ts index 280073e19df..0c3ffd31440 100644 --- a/cypress/e2e/po/pages/account-api-keys.po.ts +++ b/cypress/e2e/po/pages/account-api-keys.po.ts @@ -56,6 +56,10 @@ export default class AccountPagePo extends PagePo { return this.applyButton().click(); } + cancel(): Cypress.Chainable { + return this.self().get('button[type="reset"]').click(); + } + currentPassword(): PasswordPo { return new PasswordPo('[data-testid="account__current_password"]'); } diff --git a/cypress/e2e/tests/accessibility/login.spec.ts b/cypress/e2e/tests/accessibility/login.spec.ts deleted file mode 100644 index d364bc46103..00000000000 --- a/cypress/e2e/tests/accessibility/login.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; - -describe('Login page a11y testing', { tags: ['@adminUser', '@accessibility'] }, () => { - it('wcag21aa test', () => { - const loginPage = new LoginPagePo(); - - loginPage.goTo(); - loginPage.waitForPage(); - cy.injectAxe(); - loginPage.username().set('test user'); - - cy.checkPageAccessibility(); - }); - - it('locale selector', () => { - const loginPage = new LoginPagePo(); - - loginPage.goTo(); - loginPage.waitForPage(); - cy.injectAxe(); - cy.get('[data-testid="locale-selector"]').click(); - cy.checkPageAccessibility(); - cy.checkElementAccessibility('#username', 'Username field checks'); - }); -}); diff --git a/cypress/e2e/tests/accessibility/shell.spec.ts b/cypress/e2e/tests/accessibility/shell.spec.ts new file mode 100644 index 00000000000..d84dc492564 --- /dev/null +++ b/cypress/e2e/tests/accessibility/shell.spec.ts @@ -0,0 +1,95 @@ +import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; +import HomePagePo from '@/cypress/e2e/po/pages/home.po'; +import AboutPagePo from '@/cypress/e2e/po/pages/about.po'; +import PreferencesPagePo from '@/cypress/e2e/po/pages/preferences.po'; +import UserMenuPo from '@/cypress/e2e/po/side-bars/user-menu.po'; +import AccountPagePo from '@/cypress/e2e/po/pages/account-api-keys.po'; +import CreateKeyPagePo from '@/cypress/e2e/po/pages/account-api-keys-create_key.po'; + +describe('Shell a11y testing', { tags: ['@adminUser', '@accessibility'] }, () => { + it('login page', () => { + const loginPage = new LoginPagePo(); + + loginPage.goTo(); + loginPage.waitForPage(); + cy.injectAxe(); + loginPage.username().set('test user'); + + cy.checkPageAccessibility(); + }); + + it('locale selector', () => { + const loginPage = new LoginPagePo(); + + loginPage.goTo(); + loginPage.waitForPage(); + cy.injectAxe(); + cy.get('[data-testid="locale-selector"]').click(); + cy.checkPageAccessibility(); + }); + + describe('Logged in', { testIsolation: 'off' }, () => { + const aboutPage = new AboutPagePo(); + const prefPage = new PreferencesPagePo(); + const userMenu = new UserMenuPo(); + + before(() => { + cy.login(); + }); + + it('home page', () => { + HomePagePo.goToAndWaitForGet(); + cy.injectAxe(); + + cy.checkPageAccessibility(); + }); + + it('about page', () => { + AboutPagePo.navTo(); + aboutPage.waitForPage(); + + cy.checkPageAccessibility(); + }); + + it('preferences page', () => { + userMenu.clickMenuItem('Preferences'); + userMenu.isClosed(); + prefPage.waitForPage(); + prefPage.checkIsCurrentPage(); + prefPage.title(); + cy.injectAxe(); + + cy.checkPageAccessibility(); + }); + + describe('account', () => { + const accountPage = new AccountPagePo(); + const createKeyPage = new CreateKeyPagePo(); + + it('account page', () => { + userMenu.clickMenuItem('Account & API Keys'); + accountPage.waitForPage(); + cy.injectAxe(); + accountPage.checkIsCurrentPage(); + + cy.checkPageAccessibility(); + }); + + it('change password dialog', () => { + accountPage.changePassword(); + + cy.checkElementAccessibility('.change-password-modal'); + + accountPage.cancel(); + }); + + it('create API key', () => { + accountPage.create(); + createKeyPage.waitForPage(); + createKeyPage.isCurrentPage(); + + cy.checkPageAccessibility(); + }); + }); + }); +});