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

SDK-1884: Cypress SDK not wrapping A11Y commands appropriately #949

Merged
merged 9 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
142 changes: 92 additions & 50 deletions bin/accessibility-automation/cypress/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,60 @@ const browserStackLog = (message) => {
}

const commandsToWrap = ['visit', 'click', 'type', 'request', 'dblclick', 'rightclick', 'clear', 'check', 'uncheck', 'select', 'trigger', 'selectFile', 'scrollIntoView', 'scroll', 'scrollTo', 'blur', 'focus', 'go', 'reload', 'submit', 'viewport', 'origin'];
// scroll is not a default function in cypress.
const commandToOverwrite = ['visit', 'click', 'type', 'request', 'dblclick', 'rightclick', 'clear', 'check', 'uncheck', 'select', 'trigger', 'selectFile', 'scrollIntoView', 'scrollTo', 'blur', 'focus', 'go', 'reload', 'submit', 'viewport', 'origin'];
Copy link
Collaborator

Choose a reason for hiding this comment

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

any reason for removing scroll from the list ?

Copy link
Collaborator Author

@osho-20 osho-20 Dec 4, 2024

Choose a reason for hiding this comment

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

scroll is not a default cypress function. This error was occuring.

Copy link
Collaborator

@sauravdas1997 sauravdas1997 Dec 5, 2024

Choose a reason for hiding this comment

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

is this verified across all supported cy versions ?


/*
Overrriding the cypress commands to perform Accessibility Scan before Each command
- runCutomizedCommand is handling both the cases of subject available in cypress original command
and chaning available from original cypress command.
*/
const performModifiedScan = (originalFn, Subject, stateType, ...args) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

please add some comments explaining the below function for future ref

let customChaining = cy.wrap(null).performScan();
const changeSub = (args, stateType, newSubject) => {
if (stateType !== 'parent') {
return [newSubject, ...args.slice(1)];
}
return args;
}
const runCutomizedCommand = () => {
if (!Subject) {
let cypressCommandSubject = (cy.subject?.call(cy)) ?? null;
customChaining.then(() => cypressCommandSubject).then(() => { originalFn(...args); });
} else {
let setTimeout = args.find(arg => arg?.timeout)?.timeout ?? null;
let cypressCommandChain = (cy.subjectChain?.call(cy)) ?? null;
customChaining.performScanSubjectQuery(cypressCommandChain, setTimeout).then({ timeout: 10000 }, newSubject => originalFn(...changeSub(args, stateType, newSubject)));
}
}
runCutomizedCommand();
}

const performScan = (win, payloadToSend) =>
new Promise(async (resolve, reject) => {
const isHttpOrHttps = /^(http|https):$/.test(win.location.protocol);
if (!isHttpOrHttps) {
resolve();
return resolve();
}

function findAccessibilityAutomationElement() {
return win.document.querySelector("#accessibility-automation-element");
}

function waitForScannerReadiness(retryCount = 30, retryInterval = 100) {
function waitForScannerReadiness(retryCount = 100, retryInterval = 100) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is retryCount number intentional ? Why 100 ?

Copy link
Collaborator Author

@osho-20 osho-20 Dec 4, 2024

Choose a reason for hiding this comment

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

yes a11y team told to retry for 10 sec other framework have same count.

return new Promise(async (resolve, reject) => {
let count = 0;
const intervalID = setInterval(async () => {
if (count > retryCount) {
clearInterval(intervalID);
reject(
return reject(
new Error(
"Accessibility Automation Scanner is not ready on the page."
)
);
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return resolve("Scanner set");
} else {
count += 1;
}
Expand All @@ -42,7 +70,7 @@ new Promise(async (resolve, reject) => {
function startScan() {
function onScanComplete() {
win.removeEventListener("A11Y_SCAN_FINISHED", onScanComplete);
resolve();
return resolve();
}

win.addEventListener("A11Y_SCAN_FINISHED", onScanComplete);
Expand All @@ -56,16 +84,16 @@ new Promise(async (resolve, reject) => {
waitForScannerReadiness()
.then(startScan)
.catch(async (err) => {
resolve("Scanner is not ready on the page after multiple retries. performscan");
});
return resolve("Scanner is not ready on the page after multiple retries. performscan");
});
}
})

const getAccessibilityResultsSummary = (win) =>
new Promise((resolve) => {
const isHttpOrHttps = /^(http|https):$/.test(window.location.protocol);
if (!isHttpOrHttps) {
resolve();
return resolve();
}

function findAccessibilityAutomationElement() {
Expand All @@ -78,14 +106,14 @@ new Promise((resolve) => {
const intervalID = setInterval(() => {
if (count > retryCount) {
clearInterval(intervalID);
reject(
return reject(
new Error(
"Accessibility Automation Scanner is not ready on the page."
)
);
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return resolve("Scanner set");
} else {
count += 1;
}
Expand All @@ -96,7 +124,7 @@ new Promise((resolve) => {
function getSummary() {
function onReceiveSummary(event) {
win.removeEventListener("A11Y_RESULTS_SUMMARY", onReceiveSummary);
resolve(event.detail);
return resolve(event.detail);
}

win.addEventListener("A11Y_RESULTS_SUMMARY", onReceiveSummary);
Expand All @@ -110,16 +138,16 @@ new Promise((resolve) => {
waitForScannerReadiness()
.then(getSummary)
.catch((err) => {
resolve();
});
return resolve();
});
}
})

const getAccessibilityResults = (win) =>
new Promise((resolve) => {
const isHttpOrHttps = /^(http|https):$/.test(window.location.protocol);
if (!isHttpOrHttps) {
resolve();
return resolve();
}

function findAccessibilityAutomationElement() {
Expand All @@ -132,14 +160,14 @@ new Promise((resolve) => {
const intervalID = setInterval(() => {
if (count > retryCount) {
clearInterval(intervalID);
reject(
return reject(
new Error(
"Accessibility Automation Scanner is not ready on the page."
)
);
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return resolve("Scanner set");
} else {
count += 1;
}
Expand All @@ -150,7 +178,7 @@ new Promise((resolve) => {
function getResults() {
function onReceivedResult(event) {
win.removeEventListener("A11Y_RESULTS_RESPONSE", onReceivedResult);
resolve(event.detail);
return resolve(event.detail);
}

win.addEventListener("A11Y_RESULTS_RESPONSE", onReceivedResult);
Expand All @@ -164,8 +192,8 @@ new Promise((resolve) => {
waitForScannerReadiness()
.then(getResults)
.catch((err) => {
resolve();
});
return resolve();
});
}
});

Expand All @@ -175,6 +203,7 @@ new Promise( (resolve, reject) => {
const isHttpOrHttps = /^(http|https):$/.test(win.location.protocol);
if (!isHttpOrHttps) {
resolve("Unable to save accessibility results, Invalid URL.");
return;
}

function findAccessibilityAutomationElement() {
Expand All @@ -187,14 +216,14 @@ new Promise( (resolve, reject) => {
const intervalID = setInterval(async () => {
if (count > retryCount) {
clearInterval(intervalID);
reject(
return reject(
new Error(
"Accessibility Automation Scanner is not ready on the page."
)
);
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return resolve("Scanner set");
} else {
count += 1;
}
Expand All @@ -204,7 +233,7 @@ new Promise( (resolve, reject) => {

function saveResults() {
function onResultsSaved(event) {
resolve();
return resolve();
}
win.addEventListener("A11Y_RESULTS_SAVED", onResultsSaved);
const e = new CustomEvent("A11Y_SAVE_RESULTS", {
Expand All @@ -219,11 +248,12 @@ new Promise( (resolve, reject) => {
waitForScannerReadiness()
.then(saveResults)
.catch(async (err) => {
resolve("Scanner is not ready on the page after multiple retries. after run");
return resolve("Scanner is not ready on the page after multiple retries. after run");
});
}
} catch(er) {
resolve()
} catch(error) {
browserStackLog(`Error in saving results with error: ${error.message}`);
return resolve();
}

})
Expand Down Expand Up @@ -254,39 +284,40 @@ const shouldScanForAccessibility = (attributes) => {
const included = includeTagArray.length === 0 || includeTags.some((include) => fullTestName.includes(include));
shouldScanTestForAccessibility = !excluded && included;
} catch (error) {
browserStackLog("Error while validating test case for accessibility before scanning. Error : ", error);
browserStackLog(`Error while validating test case for accessibility before scanning. Error : ${error.message}`);
}
}

return shouldScanTestForAccessibility;
}

Cypress.on('command:start', async (command) => {
if(!command || !command.attributes) return;
if(command.attributes.name == 'window' || command.attributes.name == 'then' || command.attributes.name == 'wrap') {
return;
}

if (!commandsToWrap.includes(command.attributes.name)) return;

const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest || Cypress.mocha.getRunner().suite.ctx._runnable;

let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
if (!shouldScanTestForAccessibility) return;

cy.window().then((win) => {
browserStackLog('Performing scan form command ' + command.attributes.name);
cy.wrap(performScan(win, {method: command.attributes.name}), {timeout: 30000});
})
})
commandToOverwrite.forEach((command) => {
Cypress.Commands.overwrite(command, (originalFn, ...args) => {
const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest || Cypress.mocha.getRunner().suite.ctx._runnable;
const shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
const state = cy.state('current'), Subject = 'getSubjectFromChain' in cy;
const stateName = state === null || state === void 0 ? void 0 : state.get('name');
let stateType;
if (!shouldScanTestForAccessibility || (stateName && stateName !== command)) {
return originalFn(...args);
}
if(state !== null && state !== void 0){
stateType = state.get('type');
}
else {
stateType = null;
}
performModifiedScan(originalFn, Subject, stateType, ...args);
});
});

afterEach(() => {
const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest;
cy.window().then(async (win) => {
let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
if (!shouldScanTestForAccessibility) return cy.wrap({});

cy.wrap(performScan(win), {timeout: 30000}).then(() => {
cy.wrap(performScan(win), {timeout: 10000}).then(() => {
try {
let os_data;
if (Cypress.env("OS")) {
Expand Down Expand Up @@ -317,11 +348,12 @@ afterEach(() => {
}
};
browserStackLog(`Saving accessibility test results`);
cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}).then(() => {
cy.wrap(saveTestResults(win, payloadToSend), {timeout: 10000}).then(() => {
browserStackLog(`Saved accessibility test results`);
})

} catch (er) {
browserStackLog(`Error in saving results with error: ${er.message}`);
}
})
});
Expand All @@ -337,9 +369,11 @@ Cypress.Commands.add('performScan', () => {
}
cy.window().then(async (win) => {
browserStackLog(`Performing accessibility scan`);
await performScan(win);
cy.wrap(performScan(win), {timeout:10000});
});
} catch {}
} catch(error) {
browserStackLog(`Error in performing scan with error: ${error.message}`);
}
})

Cypress.Commands.add('getAccessibilityResultsSummary', () => {
Expand All @@ -355,7 +389,9 @@ Cypress.Commands.add('getAccessibilityResultsSummary', () => {
browserStackLog('Getting accessibility results summary');
return await getAccessibilityResultsSummary(win);
});
} catch {}
} catch(error) {
browserStackLog(`Error in getting accessibilty results summary with error: ${error.message}`);
}

});

Expand All @@ -376,6 +412,12 @@ Cypress.Commands.add('getAccessibilityResults', () => {
return await getAccessibilityResults(win);
});

} catch {}
} catch(error) {
browserStackLog(`Error in getting accessibilty results with error: ${error.message}`);
}
});

Cypress.Commands.addQuery('performScanSubjectQuery', function (chaining, setTimeout) {
this.set('timeout', setTimeout);
return () => cy.getSubjectFromChain(chaining);
});
11 changes: 11 additions & 0 deletions bin/accessibility-automation/plugin/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const path = require("node:path");
const { decodeJWTToken } = require("../../helpers/utils");
const utils = require('../../helpers/utils');

const browserstackAccessibility = (on, config) => {
let browser_validation = true;
Expand Down Expand Up @@ -30,7 +32,16 @@ const browserstackAccessibility = (on, config) => {
}
if (browser_validation) {
const ally_path = path.dirname(process.env.ACCESSIBILITY_EXTENSION_PATH)
const payload = decodeJWTToken(process.env.ACCESSIBILITY_AUTH);
launchOptions.extensions.push(ally_path);
if(!utils.isUndefined(payload) && !utils.isUndefined(payload.a11y_core_config) && payload.a11y_core_config.domForge === true) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

significance of these?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

other ticket SDK-1907

launchOptions.args.push("--auto-open-devtools-for-tabs");
launchOptions.preferences.default["devtools"] = launchOptions.preferences.default["devtools"] || {};
launchOptions.preferences.default["devtools"]["preferences"] = launchOptions.preferences.default["devtools"]["preferences"] || {};
launchOptions.preferences.default["devtools"]["preferences"][
"currentDockState"
] = '"undocked"';
}
return launchOptions
}
}
Expand Down
20 changes: 20 additions & 0 deletions bin/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1775,3 +1775,23 @@ exports.getMajorVersion = (version) => {
return null;
}
}

const base64UrlDecode = (str) => {
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
const buffer = Buffer.from(base64, 'base64');
return buffer.toString('utf-8');
};

exports.decodeJWTToken = (token) => {
try{
const parts = token.split('.');
if (parts.length < 2) {
throw new Error('Invalid JWT token');
}
const payload = JSON.parse(base64UrlDecode(parts[1]));
return payload
} catch (error) {
logger.err("Error in token decoding with error:", error.message);
return undefined;
}
}
Loading