Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: integrate cypress-axe for accessibility testing #11128

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ dist
coverage
.nyc_output

# Cypress internal logs
cypress-logs

# scoping feature generated entry points for vite consumption
packages/compat/test/pages/scoped
packages/main/test/pages/scoped
Expand Down
4 changes: 3 additions & 1 deletion packages/cypress-internal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"typescript": "^5.6.2",
"rimraf": "^3.0.2",
"cypress-real-events": "^1.12.0",
"@cypress/code-coverage": "^3.13.11"
"@cypress/code-coverage": "^3.13.11",
"axe-core": "^4.10.2",
"cypress-axe": "^1.6.0"
}
}
94 changes: 94 additions & 0 deletions packages/cypress-internal/src/acc_report/index
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cypress Test Report</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
padding: 20px;
}

ul {
list-style-type: none;
padding-left: 20px;
}

.violations ul li:not(:last-child) {
border-bottom: 1px solid black;
}

details {
margin-bottom: 10px;
}

summary {
cursor: pointer;
}

details>ul {
margin-top: 5px;
}
</style>
</head>

<body>

<h1>Cypress Test Report</h1>

<div>
<ul id="test-report">
</ul>
</div>

<script type="module">
const response = await fetch("./acc_logs.json")
const reportData = await response.json();

const printError = (error) => {
return `
<li>
<details class="violations">
<summary>[FAILED] ${error.testTitlePath.join(" > ")}</summary>
<ul>
${error.violations.map(printViolation).join("")}
</ul>
</details>
</li>`
};

const printViolation = (violation) => {
return `
<li>
<b>id:</b> ${violation.id}<br />
<b>impact:</b> ${violation.impact}<br />
<b>description:</b> ${violation.description}
</li>`;
};

const printFileLog = () => {
const testReport = document.querySelector("#test-report");

testReport.innerHTML = reportData.map(fileLog => {
return `
<li>
<details open>
<summary>Test file: ${fileLog.testFile}</summary>
<ul>
${fileLog.errors.map(printError).join("")}
</ul>
</details>
</li>`;
}).join("");
}

printFileLog();
</script>


</body>

</html>
63 changes: 63 additions & 0 deletions packages/cypress-internal/src/acc_report/support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'cypress-axe'
import type { AxeResults, ImpactValue } from "axe-core";
import { Options } from "cypress-axe";

type Vialotation = {
id: string,
impact: ImpactValue | undefined,
description: string
nodes: number,
}

type TestVialotation = {
testTitlePath: string[],
violations: Vialotation[]
}

type TestReport = {
testFile: string,
errors: TestVialotation[]
}

function checkA11TerminalLog(violations: typeof AxeResults.violations) {
const violationData = violations.map<Vialotation>(
({ id, impact, description, nodes }) => ({
id,
impact,
description,
nodes: nodes.length
})
)

const report: TestReport = {
testFile: Cypress.spec.relative,
errors: [{
testTitlePath: Cypress.currentTest.titlePath,
violations: violationData,
}]
}

cy.task('ui5ReportA11y', report)
}

declare global {
namespace Cypress {
interface Chainable {
ui5CheckA11y(context?: string | Node | undefined, options?: Options | undefined): Cypress.Chainable<void>;
}
}
}

Cypress.Commands.add("ui5CheckA11y", (context?: string | Node | undefined, options?: Options | undefined) => {
return cy.checkA11y(context || "[data-cy-root]", options, checkA11TerminalLog, false)
})

if (Cypress.env('ui5AccTasksRegistered') === true) {
before(() => {
cy.task('ui5ReportA11yReset', Cypress.spec.relative);
})
}

export type {
TestReport,
}
111 changes: 111 additions & 0 deletions packages/cypress-internal/src/acc_report/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { TestReport } from "./support.js";
// @ts-expect-error
import { readFileSync, writeFileSync, mkdirSync } from "fs";
// @ts-expect-error
import * as path from "path";
// @ts-expect-error
import { fileURLToPath } from 'node:url';

// @ts-expect-error
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const outputPath = path.resolve("./cypress-logs/acc_logs.json");
const outputPathIndex = path.resolve("./cypress-logs/index.html");

const findExistingReport = (reportData: TestReport[], testFile: string) => reportData.find(report => report.testFile === testFile);

const readReportFile = (): TestReport[] => {
try {
return JSON.parse(readFileSync(outputPath, { encoding: "utf-8" })) as TestReport[];
} catch (e) {
return [];
}
}

const log = (currentReport: TestReport) => {
let reportData = readReportFile();

const existingReport = findExistingReport(reportData, currentReport.testFile);

if (existingReport) {
existingReport.errors.push(...currentReport.errors)
} else {
reportData.push(currentReport);
}

reportData.forEach(file => {
const paths = file.errors.map(error => error.testTitlePath.join(" "));
file.errors = file.errors.filter((error, index) => paths.indexOf(error.testTitlePath.join(" ")) === index);
})

saveReportFile(reportData);
}

const saveReportFile = (reportData: TestReport[]) => {
mkdirSync(path.dirname(outputPath), { recursive: true });

writeFileSync(outputPath, JSON.stringify(reportData, undefined, 4));
};

const reset = (testFile: string) => {
let reportData = readReportFile();
const existingReport = findExistingReport(reportData, testFile);

if (existingReport) {
reportData.splice(reportData.indexOf(existingReport), 1)

saveReportFile(reportData);
}
}

const prepare = () => {
const indexTemplate = readFileSync(path.join(__dirname, "index"), { encoding: "utf-8" });
writeFileSync(outputPathIndex, indexTemplate);

saveReportFile([]);

}

function accTask(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {
if (config.env.UI5_ACC === true) {
on('before:run', () => {
// Reset the report file when tests are run with the `cypress run` command.
// This event is triggered when running tests with the `cypress open` command (behind an experimental flag).
// `config.isInteractive` helps us determine whether the tests are running in interactive mode (`cypress open`) or non-interactive mode (`cypress run`).
if (!config.isInteractive) {
prepare();
}
});

on('before:browser:launch', () => {
// Reset the report file when tests are run with the `cypress open` command.
// `config.isInteractive` helps us determine whether the tests are running in interactive mode (`cypress open`) or non-interactive mode (`cypress run`).
if (config.isInteractive) {
prepare();
}
});

on('task', {
// Adds the accessibility report for the current test to the spec file logs
ui5ReportA11y(report: TestReport) {
log(report);

return null;
},

// Removes all existing logs for the current test file when the spec file is loaded
ui5ReportA11yReset(testFile: string) {
reset(testFile);

return null;
}
})

config.env.ui5AccTasksRegistered = true
}

return config
}

export default accTask;
4 changes: 3 additions & 1 deletion packages/cypress-internal/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import "cypress-real-events";
import '@cypress/code-coverage/support'
import "./acc_report/support.js";
import "./helpers.js"

const realEventCmdCallback = (originalFn: any, element: any, ...args: any) => {
cy.get(element)
Expand All @@ -26,4 +28,4 @@ const commands = [

commands.forEach(cmd => {
Cypress.Commands.overwrite(cmd as any, realEventCmdCallback)
});
});
9 changes: 7 additions & 2 deletions packages/cypress-internal/src/copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import path from "path";

const dirname = import.meta.dirname;

await copyFile(path.join(dirname, "./eslint.cjs"), path.join(dirname, "../dist/eslint.cjs"))
const files = [

];

console.log("eslint.cjs copied successfully")
await copyFile(path.join(dirname, "./eslint.cjs"), path.join(dirname, "../dist/eslint.cjs"))
console.log("eslint.cjs copied successfully")
await copyFile(path.join(dirname, "./acc_report/index"), path.join(dirname, "../dist/acc_report/index"))
console.log("acc_report/index copied successfully")
3 changes: 3 additions & 0 deletions packages/cypress-internal/src/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { defineConfig } from "cypress";
// @ts-ignore
import viteConfig from "../../../vite.config.js";
import coverageTask from "@cypress/code-coverage/task.js";
import accTask from "./acc_report/task.js";


export default defineConfig({
component: {
setupNodeEvents(on, config) {
coverageTask(on, config);
accTask(on, config)

return config
},
Expand Down
16 changes: 16 additions & 0 deletions packages/cypress-internal/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare global {
function ui5AccDescribe(title: string, fn: (this: Mocha.Suite) => void): Mocha.Suite | void;
}

globalThis.ui5AccDescribe = (title: string, fn: (this: Mocha.Suite) => void): Mocha.Suite | void => {
if (Cypress.env('ui5AccTasksRegistered') === true) {
return describe.only(`${title}`, function (this: Mocha.Suite) {
before(() => {
cy.injectAxe({ axeCorePath: "../../node_modules/axe-core/axe.min.js" });
});
fn.call(this);
});
}
};

export { }
3 changes: 2 additions & 1 deletion packages/cypress-internal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"esModuleInterop": true,
"strict": true,
"types": [
"cypress"
"cypress",
"cypress-axe",
]
},
}
9 changes: 9 additions & 0 deletions packages/main/cypress/specs/Button.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe("Button general interaction", () => {
.find(".ui5-button-icon")
.should("not.exist", "icon is not present");
});

it("tests button's endIon rendering", () => {
cy.mount(<Button>Action Bar Button</Button>);

Expand Down Expand Up @@ -387,3 +388,11 @@ describe("Accessibility", () => {
.should("have.text", "999+");
});
});

ui5AccDescribe("Automated accessibility tests", () => {
it("Icon only", () => {
cy.mount(<Button icon="message-information"></Button>);

cy.ui5CheckA11y();
})
});
Loading