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 5 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
131 changes: 102 additions & 29 deletions bin/accessibility-automation/cypress/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,59 @@ 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'];
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 ?

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();
function changeSub(args, stateType, newSubject) {
amaanbs marked this conversation as resolved.
Show resolved Hide resolved
if (stateType !== 'parent') {
return [newSubject, ...args.slice(1)];
}
return args;
}
function runCutomizedCommand() {
amaanbs marked this conversation as resolved.
Show resolved Hide resolved
if (!Subject) {
let orgS1, orgS2, cypressCommandSubject;
if((orgS2 = (orgS1 = cy).subject) !==null && orgS2 !== void 0){
cypressCommandSubject = orgS2.call(orgS1);
}
else{
cypressCommandSubject = null;
}
customChaining.then(()=> cypressCommandSubject).then(() => {originalFn(...args)});
}
else {
let orgSC1, orgSC2, timeO1, cypressCommandChain, setTimeout;
if((timeO1 = args.find(arg => arg !== null && arg !== void 0 ? arg.timeout : null)) !== null && timeO1 !== void 0) {
setTimeout = timeO1.timeout;
}
else {
setTimeout = null;
}
if((orgSC1 = (orgSC2 = cy).subjectChain) !== null && orgSC1 !== void 0){
cypressCommandChain = orgSC1.call(orgSC2);
}
else {
cypressCommandChain = null;
}
customChaining.performScanSubjectQuery(cypressCommandChain, setTimeout).then({timeout: 30000}, (newSubject) => originalFn(...changeSub(args, stateType, newSubject)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
function runCutomizedCommand() {
if (!Subject) {
let orgS1, orgS2, cypressCommandSubject;
if((orgS2 = (orgS1 = cy).subject) !==null && orgS2 !== void 0){
cypressCommandSubject = orgS2.call(orgS1);
}
else{
cypressCommandSubject = null;
}
customChaining.then(()=> cypressCommandSubject).then(() => {originalFn(...args)});
}
else {
let orgSC1, orgSC2, timeO1, cypressCommandChain, setTimeout;
if((timeO1 = args.find(arg => arg !== null && arg !== void 0 ? arg.timeout : null)) !== null && timeO1 !== void 0) {
setTimeout = timeO1.timeout;
}
else {
setTimeout = null;
}
if((orgSC1 = (orgSC2 = cy).subjectChain) !== null && orgSC1 !== void 0){
cypressCommandChain = orgSC1.call(orgSC2);
}
else {
cypressCommandChain = null;
}
customChaining.performScanSubjectQuery(cypressCommandChain, setTimeout).then({timeout: 30000}, (newSubject) => originalFn(...changeSub(args, stateType, newSubject)));
function 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: 30000 }, 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;
}

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 () => {
Expand All @@ -29,9 +69,11 @@ new Promise(async (resolve, reject) => {
"Accessibility Automation Scanner is not ready on the page."
)
);
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't promise be resolved?

} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return;
} else {
count += 1;
}
Expand All @@ -43,6 +85,7 @@ new Promise(async (resolve, reject) => {
function onScanComplete() {
win.removeEventListener("A11Y_SCAN_FINISHED", onScanComplete);
resolve();
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't promise be resolved?

}

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

Expand All @@ -66,6 +110,7 @@ new Promise((resolve) => {
const isHttpOrHttps = /^(http|https):$/.test(window.location.protocol);
if (!isHttpOrHttps) {
resolve();
return;
}

function findAccessibilityAutomationElement() {
Expand All @@ -83,9 +128,11 @@ new Promise((resolve) => {
"Accessibility Automation Scanner is not ready on the page."
)
);
return;
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return;
} else {
count += 1;
}
Expand All @@ -97,6 +144,7 @@ new Promise((resolve) => {
function onReceiveSummary(event) {
win.removeEventListener("A11Y_RESULTS_SUMMARY", onReceiveSummary);
resolve(event.detail);
return;
}

win.addEventListener("A11Y_RESULTS_SUMMARY", onReceiveSummary);
Expand All @@ -111,7 +159,8 @@ new Promise((resolve) => {
.then(getSummary)
.catch((err) => {
resolve();
});
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

resolving do we need return?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no

});
}
})

Expand All @@ -120,6 +169,7 @@ new Promise((resolve) => {
const isHttpOrHttps = /^(http|https):$/.test(window.location.protocol);
if (!isHttpOrHttps) {
resolve();
return;
}

function findAccessibilityAutomationElement() {
Expand All @@ -137,9 +187,11 @@ new Promise((resolve) => {
"Accessibility Automation Scanner is not ready on the page."
)
);
return;
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return;
} else {
count += 1;
}
Expand All @@ -151,6 +203,7 @@ new Promise((resolve) => {
function onReceivedResult(event) {
win.removeEventListener("A11Y_RESULTS_RESPONSE", onReceivedResult);
resolve(event.detail);
return;
}

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

Expand All @@ -175,6 +229,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 @@ -192,9 +247,11 @@ new Promise( (resolve, reject) => {
"Accessibility Automation Scanner is not ready on the page."
)
);
return;
} else if (findAccessibilityAutomationElement()) {
clearInterval(intervalID);
resolve("Scanner set");
return;
} else {
count += 1;
}
Expand All @@ -205,6 +262,7 @@ new Promise( (resolve, reject) => {
function saveResults() {
function onResultsSaved(event) {
resolve();
return;
}
win.addEventListener("A11Y_RESULTS_SAVED", onResultsSaved);
const e = new CustomEvent("A11Y_SAVE_RESULTS", {
Expand All @@ -220,10 +278,13 @@ new Promise( (resolve, reject) => {
.then(saveResults)
.catch(async (err) => {
resolve("Scanner is not ready on the page after multiple retries. after run");
return;
});
}
} catch(er) {
resolve()
} catch(error) {
browserStackLog(`Error in saving results with error: ${error.message}`);
resolve();
return;
}

})
Expand Down Expand Up @@ -254,31 +315,32 @@ 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;
Expand Down Expand Up @@ -322,6 +384,7 @@ afterEach(() => {
})

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

@sauravdas1997 sauravdas1997 Dec 4, 2024

Choose a reason for hiding this comment

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

This sets timeout, which can break the test if mochaTimeout is less than 40s. should we also handle mocha timeout ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if the response from performScan take less time no timeout will occur. Eventually if response does not come and mocha timeout occurs it should be considered as timeout.

});
} catch {}
} catch(error) {
browserStackLog(`Error in performing scan with error: ${error.message}`);
}
})

Cypress.Commands.add('getAccessibilityResultsSummary', () => {
Expand All @@ -355,7 +420,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 +443,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
21 changes: 21 additions & 0 deletions bin/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1775,3 +1775,24 @@ 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 header = JSON.parse(base64UrlDecode(parts[0]));
const payload = JSON.parse(base64UrlDecode(parts[1]));
return { header, payload };
Copy link
Collaborator

Choose a reason for hiding this comment

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

why to parse header if we are not using

Copy link
Collaborator

Choose a reason for hiding this comment

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

also let's not return it if not used

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i made it a generic function to get used in future if need headers

} catch (error) {
logger.err(error.message);
amaanbs marked this conversation as resolved.
Show resolved Hide resolved
return {undefined, undefined};
}
}
Loading