Skip to content

Commit

Permalink
Merge pull request #949 from browserstack/SDK-1884
Browse files Browse the repository at this point in the history
SDK-1884: Cypress SDK not wrapping A11Y commands appropriately
  • Loading branch information
pranavj1001 authored Dec 9, 2024
2 parents 9870e26 + 2148ba1 commit 6a74d96
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 48 deletions.
143 changes: 95 additions & 48 deletions bin/accessibility-automation/cypress/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,68 @@ 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'];

/*
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) => {
let customChaining = cy.wrap(null).performScan();
const changeSub = (args, stateType, newSubject) => {
if (stateType !== 'parent') {
return [newSubject, ...args.slice(1)];
}
return args;
}
const runCustomizedCommand = () => {
if (!Subject) {
let orgS1, orgS2, cypressCommandSubject = null;
if((orgS2 = (orgS1 = cy).subject) !==null && orgS2 !== void 0){
cypressCommandSubject = orgS2.call(orgS1);
}
customChaining.then(()=> cypressCommandSubject).then(() => {originalFn(...args)});
} else {
let orgSC1, orgSC2, timeO1, cypressCommandChain = null, setTimeout = null;
if((timeO1 = args.find(arg => arg !== null && arg !== void 0 ? arg.timeout : null)) !== null && timeO1 !== void 0) {
setTimeout = timeO1.timeout;
}
if((orgSC1 = (orgSC2 = cy).subjectChain) !== null && orgSC1 !== void 0){
cypressCommandChain = orgSC1.call(orgSC2);
}
customChaining.performScanSubjectQuery(cypressCommandChain, setTimeout).then({timeout: 30000}, (newSubject) => originalFn(...changeSub(args, stateType, newSubject)));
}
}
runCustomizedCommand();
}

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) {
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 +78,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 +92,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 +114,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 +132,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 +146,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 +168,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 +186,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 +200,8 @@ new Promise((resolve) => {
waitForScannerReadiness()
.then(getResults)
.catch((err) => {
resolve();
});
return resolve();
});
}
});

Expand All @@ -175,6 +211,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 +224,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 +241,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 +256,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,31 +292,29 @@ 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 = null;
if (!shouldScanTestForAccessibility || (stateName && stateName !== command)) {
return originalFn(...args);
}
if(state !== null && state !== void 0){
stateType = state.get('type');
}
performModifiedScan(originalFn, Subject, stateType, ...args);
});
});

afterEach(() => {
const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest;
Expand Down Expand Up @@ -322,6 +358,7 @@ afterEach(() => {
})

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

Cypress.Commands.add('getAccessibilityResultsSummary', () => {
Expand All @@ -355,7 +394,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 +417,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) {
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;
}
}

0 comments on commit 6a74d96

Please sign in to comment.