Skip to content
Draft
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
23 changes: 22 additions & 1 deletion examples/oobee-cypress-integration-js/cypress.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { defineConfig } from "cypress";
import oobeeA11yInit from "@govtechsg/oobee";
import fs from 'fs-extra';
import { glob } from 'glob';
import path from 'path';

// viewport used in tests to optimise screenshots
const viewportSettings = { width: 1920, height: 1040 };
Expand All @@ -15,7 +18,7 @@ const oobeeA11y = await oobeeA11yInit({
testLabel: "Demo Cypress Scan", // label for test
name: "Your Name",
email: "email@domain.com",
includeScreenshots: true, // include screenshots of affected elements in the report
includeScreenshots: false, // include screenshots of affected elements in the report
viewportSettings,
thresholds,
scanAboutMetadata,
Expand Down Expand Up @@ -56,6 +59,24 @@ export default defineConfig({
async terminateOobeeA11y() {
return await oobeeA11y.terminate();
},
returnOobeeRandomTokenAndPage() {
return {
randomToken: oobeeA11y.randomToken,
// page: `${String(oobeeA11y.scanDetails.urlsCrawled.scanned.length).padStart(9, '0')}.json`,
};
},
copyFiles({fromPattern, toDir}) {
!fs.existsSync(toDir) && fs.mkdirSync(toDir, {recursive: true});

const files = glob.sync(fromPattern);

for (const file of files) {
const to = path.join(toDir, path.basename(file));
fs.copyFileSync(file, to);
}

return null;
},
});
},
},
Expand Down
36 changes: 22 additions & 14 deletions examples/oobee-cypress-integration-js/cypress/e2e/spec.cy.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
describe("template spec", () => {
it("should run oobee A11y", () => {
cy.visit(
"https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm"
);
describe('template spec', () => {
beforeEach(() => {
cy.visit('https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm');
cy.injectOobeeA11yScripts();
cy.runOobeeA11yScan();

cy.get("button[onclick=\"toggleSecondSection()\"]").click();
// Run a scan on <input> and <button> elements
cy.runOobeeA11yScan({
elementsToScan: ["input", "button"],
elementsToClick: ["button[onclick=\"toggleSecondSection()\"]"],
metadata: "Clicked button"
});
});

after(() => {
cy.terminateOobeeA11y();
});

it('should not have WCAG violations in first section', () => {
cy.runOobeeA11yScan({}, { mustFix: 10 });
});

it('should not have WCAG violations in second section', () => {
cy.get('button[onclick="toggleSecondSection()"]').click();
// Run a scan on <input> and <button> elements
cy.runOobeeA11yScan(
{
elementsToScan: ['input', 'button'],
elementsToClick: ['button[onclick="toggleSecondSection()"]'],
metadata: 'Clicked button',
},
{ mustFix: 1 },
);
});
});
214 changes: 155 additions & 59 deletions examples/oobee-cypress-integration-js/cypress/support/e2e.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,169 @@
Cypress.Commands.add("injectOobeeA11yScripts", () => {
cy.task("getAxeScript").then((s) => {
cy.window().then((win) => {
try {
win.eval(s);
}
catch (error) {
// If eval fails due to cross-origin issues, try alternative injection
if (error.message.includes('SecurityError') || error.message.includes('cross-origin')) {
cy.log('Cross-origin error detected, attempting alternative script injection');
// Create a script tag as fallback
const script = win.document.createElement('script');
script.textContent = s;
win.document.head.appendChild(script);
}
else {
throw error;
}
}
});
import 'cypress-if';

Cypress.Commands.add('injectOobeeA11yScripts', () => {
cy.task('getAxeScript').then(s => {
cy.window().then(win => {
try {
win.eval(s);
} catch (error) {
// If eval fails due to cross-origin issues, try alternative injection
if (error.message.includes('SecurityError') || error.message.includes('cross-origin')) {
cy.log('Cross-origin error detected, attempting alternative script injection');
// Create a script tag as fallback
const script = win.document.createElement('script');
script.textContent = s;
win.document.head.appendChild(script);
} else {
throw error;
}
}
});
cy.task("getOobeeA11yScripts").then((s) => {
cy.window().then((win) => {
try {
win.eval(s);
}
catch (error) {
// If eval fails due to cross-origin issues, try alternative injection
if (error.message.includes('SecurityError') || error.message.includes('cross-origin')) {
cy.log('Cross-origin error detected, attempting alternative script injection');
// Create a script tag as fallback
const script = win.document.createElement('script');
script.textContent = s;
win.document.head.appendChild(script);
}
else {
throw error;
}
}
});
});
cy.task('getOobeeA11yScripts').then(s => {
cy.window().then(win => {
try {
win.eval(s);
} catch (error) {
// If eval fails due to cross-origin issues, try alternative injection
if (error.message.includes('SecurityError') || error.message.includes('cross-origin')) {
cy.log('Cross-origin error detected, attempting alternative script injection');
// Create a script tag as fallback
const script = win.document.createElement('script');
script.textContent = s;
win.document.head.appendChild(script);
} else {
throw error;
}
}
});
});
});

Cypress.Commands.add("runOobeeA11yScan", (items = {}) => {
cy.window().then(async (win) => {
Cypress.Commands.add('runOobeeA11yScan', (items = {}, threshold = {}) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ash007ok with your Cypress experience, do you think it is possible to make Oobee functions like these exposed as cypress keywords instead?

E.g. Can we have an equivalent of or extendnpmIndex for Cypress? Then users don't have to copy/paste the examples.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add on, the screenshots keywords can be for Playwright too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@younglim That would be great! We can create a Cypress plugin like cypress-oobee to eliminate the copy/paste setup.

Guide to create plugins: https://docs.cypress.io/guides/tooling/plugins-guide
Plugin examples: https://docs.cypress.io/app/plugins/plugins-list

cy.window().then(async win => {
const { elementsToScan, elementsToClick, metadata } = items;

// extract text from the page for readability grading
const sentences = win.extractText();
// run readability grading separately as it cannot be done within the browser context
cy.task("gradeReadability", sentences).then(
async (gradingReadabilityFlag) => {
// passing the grading flag to runA11yScan to inject violation as needed
const res = await win.runA11yScan(
elementsToScan,
gradingReadabilityFlag,
);
cy.task("pushOobeeA11yScanResults", {
res,
metadata,
elementsToClick,
}).then((count) => {
return count;
cy.task('gradeReadability', sentences).then(async gradingReadabilityFlag => {
// passing the grading flag to runA11yScan to inject violation as needed
const res = await win.runA11yScan(elementsToScan, gradingReadabilityFlag);

const takeOobeeScreenshotsFromCypressForMustFix =
Cypress.env('takeOobeeScreenshotsFromCypressForMustFix') || true;
const takeOobeeScreenshotsFromCypressForGoodToFix =
Cypress.env('takeOobeeScreenshotsFromCypressForGoodToFix') || true;

let shouldTakeScreenshot;
let oobeeReportPath;

// take screenshot and move to report dir
cy.wrap()
.then(() => {
cy.task('returnOobeeRandomTokenAndPage').then(({ randomToken }) => {
oobeeReportPath = randomToken;
});
})
.then(() => {
// Take screenshots based on flags and violation severity
if (
(takeOobeeScreenshotsFromCypressForMustFix ||
takeOobeeScreenshotsFromCypressForGoodToFix) &&
res.axeScanResults.violations.length > 0
) {
const violations = res.axeScanResults.violations;
violations.forEach(violation => {
violation.nodes.forEach((node, nodeIndex) => {
const selector = node.target && node.target[0];
const timestamp = Date.now() * 1000000 + Math.floor(Math.random() * 1000000); // Epoch time in nanoseconds
const screenshotFileName = `${node.impact}_${violation.id}_node_${nodeIndex}_${timestamp}`;
const screenshotPath = `elemScreenshots/html/${screenshotFileName}`;
const fullScreenshotPath = `${oobeeReportPath}/${screenshotPath}`;

if (selector) {
// Determine if we should take screenshot based on impact level
shouldTakeScreenshot =
(takeOobeeScreenshotsFromCypressForMustFix &&
(node.impact === 'critical' || node.impact === 'serious')) ||
(takeOobeeScreenshotsFromCypressForGoodToFix &&
(node.impact === 'moderate' || node.impact === 'minor'));

if (shouldTakeScreenshot) {
takeScreenshotForHTMLElements(fullScreenshotPath, selector);
node.screenshotPath = screenshotPath + '.png';
}
}
});
});
}
})
.then(() => {
// move screenshots to report dir
if (
takeOobeeScreenshotsFromCypressForMustFix ||
takeOobeeScreenshotsFromCypressForGoodToFix
) {
cy.task('returnOobeeRandomTokenAndPage').then(({ randomToken }) => {
// const screenshotDir = `cypress/screenshots/${randomToken}/elemScreenshots/html`;
const screenshotPattern = `cypress/screenshots/**/elemScreenshots/html/*.png`;
const toReportDir = `results/${randomToken}/elemScreenshots/html`;
cy.task('copyFiles', {
fromPattern: screenshotPattern,
toDir: toReportDir,
});
});
}
})
.then(() => {
cy.task('pushOobeeA11yScanResults', {
res,
metadata,
elementsToClick,
}).then(count => {
// validate the count against the thresholds
handleViolation(count, threshold);
return count;
});
});
},
);
cy.task("finishOobeeA11yTestCase"); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
});
cy.task('finishOobeeA11yTestCase'); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
});
});

Cypress.Commands.add("terminateOobeeA11y", () => {
cy.task("terminateOobeeA11y");
Cypress.Commands.add('terminateOobeeA11y', () => {
cy.task('terminateOobeeA11y');
});

const handleViolation = (scanResults = {}, threshold = {}) => {
const assertIfConfigured = key => {
if (threshold && typeof threshold[key] === 'number') {
const actual = Number(scanResults[key] ?? 0);
const limit = Number(threshold[key]);
expect(
actual,
`The value of '${key}' (${actual}) should be less than or equal to the threshold (${limit}).`,
).to.be.at.most(limit);
}
};
assertIfConfigured('mustFix');
assertIfConfigured('goodToFix');
};

const takeScreenshotForHTMLElements = (screenshotPath, selector) => {
try {
cy.get(selector)
.if()
.then(el => {
cy.wrap(el).first().invoke('css', 'border', '3px solid red'); // Highlight element with red border
cy.wrap(el).first().parent();
cy.wrap(el).first().screenshot(screenshotPath, {
overwrite: true,
capture: 'viewport',
});
cy.wrap(el).first().invoke('css', 'border', 'none'); // Remove highlight after screenshot
});
} catch (e) {
cy.log('Error taking screenshot for element', selector, e);
}
};
5 changes: 4 additions & 1 deletion examples/oobee-cypress-integration-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"test": "cypress run"
},
"devDependencies": {
"@govtechsg/oobee": "^0.10.69",
"cypress": "^13.0.0",
"@govtechsg/oobee": "^0.10.69"
"cypress-if": "^1.13.2",
"fs-extra": "^11.3.2",
"glob": "^11.0.3"
}
}