Skip to content

Commit 6a74d96

Browse files
authored
Merge pull request #949 from browserstack/SDK-1884
SDK-1884: Cypress SDK not wrapping A11Y commands appropriately
2 parents 9870e26 + 2148ba1 commit 6a74d96

File tree

3 files changed

+126
-48
lines changed

3 files changed

+126
-48
lines changed

bin/accessibility-automation/cypress/index.js

Lines changed: 95 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,68 @@ const browserStackLog = (message) => {
66
}
77

88
const commandsToWrap = ['visit', 'click', 'type', 'request', 'dblclick', 'rightclick', 'clear', 'check', 'uncheck', 'select', 'trigger', 'selectFile', 'scrollIntoView', 'scroll', 'scrollTo', 'blur', 'focus', 'go', 'reload', 'submit', 'viewport', 'origin'];
9+
// scroll is not a default function in cypress.
10+
const commandToOverwrite = ['visit', 'click', 'type', 'request', 'dblclick', 'rightclick', 'clear', 'check', 'uncheck', 'select', 'trigger', 'selectFile', 'scrollIntoView', 'scrollTo', 'blur', 'focus', 'go', 'reload', 'submit', 'viewport', 'origin'];
11+
12+
/*
13+
Overrriding the cypress commands to perform Accessibility Scan before Each command
14+
- runCutomizedCommand is handling both the cases of subject available in cypress original command
15+
and chaning available from original cypress command.
16+
*/
17+
const performModifiedScan = (originalFn, Subject, stateType, ...args) => {
18+
let customChaining = cy.wrap(null).performScan();
19+
const changeSub = (args, stateType, newSubject) => {
20+
if (stateType !== 'parent') {
21+
return [newSubject, ...args.slice(1)];
22+
}
23+
return args;
24+
}
25+
const runCustomizedCommand = () => {
26+
if (!Subject) {
27+
let orgS1, orgS2, cypressCommandSubject = null;
28+
if((orgS2 = (orgS1 = cy).subject) !==null && orgS2 !== void 0){
29+
cypressCommandSubject = orgS2.call(orgS1);
30+
}
31+
customChaining.then(()=> cypressCommandSubject).then(() => {originalFn(...args)});
32+
} else {
33+
let orgSC1, orgSC2, timeO1, cypressCommandChain = null, setTimeout = null;
34+
if((timeO1 = args.find(arg => arg !== null && arg !== void 0 ? arg.timeout : null)) !== null && timeO1 !== void 0) {
35+
setTimeout = timeO1.timeout;
36+
}
37+
if((orgSC1 = (orgSC2 = cy).subjectChain) !== null && orgSC1 !== void 0){
38+
cypressCommandChain = orgSC1.call(orgSC2);
39+
}
40+
customChaining.performScanSubjectQuery(cypressCommandChain, setTimeout).then({timeout: 30000}, (newSubject) => originalFn(...changeSub(args, stateType, newSubject)));
41+
}
42+
}
43+
runCustomizedCommand();
44+
}
945

1046
const performScan = (win, payloadToSend) =>
1147
new Promise(async (resolve, reject) => {
1248
const isHttpOrHttps = /^(http|https):$/.test(win.location.protocol);
1349
if (!isHttpOrHttps) {
14-
resolve();
50+
return resolve();
1551
}
1652

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

21-
function waitForScannerReadiness(retryCount = 30, retryInterval = 100) {
57+
function waitForScannerReadiness(retryCount = 100, retryInterval = 100) {
2258
return new Promise(async (resolve, reject) => {
2359
let count = 0;
2460
const intervalID = setInterval(async () => {
2561
if (count > retryCount) {
2662
clearInterval(intervalID);
27-
reject(
63+
return reject(
2864
new Error(
2965
"Accessibility Automation Scanner is not ready on the page."
3066
)
3167
);
3268
} else if (findAccessibilityAutomationElement()) {
3369
clearInterval(intervalID);
34-
resolve("Scanner set");
70+
return resolve("Scanner set");
3571
} else {
3672
count += 1;
3773
}
@@ -42,7 +78,7 @@ new Promise(async (resolve, reject) => {
4278
function startScan() {
4379
function onScanComplete() {
4480
win.removeEventListener("A11Y_SCAN_FINISHED", onScanComplete);
45-
resolve();
81+
return resolve();
4682
}
4783

4884
win.addEventListener("A11Y_SCAN_FINISHED", onScanComplete);
@@ -56,16 +92,16 @@ new Promise(async (resolve, reject) => {
5692
waitForScannerReadiness()
5793
.then(startScan)
5894
.catch(async (err) => {
59-
resolve("Scanner is not ready on the page after multiple retries. performscan");
60-
});
95+
return resolve("Scanner is not ready on the page after multiple retries. performscan");
96+
});
6197
}
6298
})
6399

64100
const getAccessibilityResultsSummary = (win) =>
65101
new Promise((resolve) => {
66102
const isHttpOrHttps = /^(http|https):$/.test(window.location.protocol);
67103
if (!isHttpOrHttps) {
68-
resolve();
104+
return resolve();
69105
}
70106

71107
function findAccessibilityAutomationElement() {
@@ -78,14 +114,14 @@ new Promise((resolve) => {
78114
const intervalID = setInterval(() => {
79115
if (count > retryCount) {
80116
clearInterval(intervalID);
81-
reject(
117+
return reject(
82118
new Error(
83119
"Accessibility Automation Scanner is not ready on the page."
84120
)
85121
);
86122
} else if (findAccessibilityAutomationElement()) {
87123
clearInterval(intervalID);
88-
resolve("Scanner set");
124+
return resolve("Scanner set");
89125
} else {
90126
count += 1;
91127
}
@@ -96,7 +132,7 @@ new Promise((resolve) => {
96132
function getSummary() {
97133
function onReceiveSummary(event) {
98134
win.removeEventListener("A11Y_RESULTS_SUMMARY", onReceiveSummary);
99-
resolve(event.detail);
135+
return resolve(event.detail);
100136
}
101137

102138
win.addEventListener("A11Y_RESULTS_SUMMARY", onReceiveSummary);
@@ -110,16 +146,16 @@ new Promise((resolve) => {
110146
waitForScannerReadiness()
111147
.then(getSummary)
112148
.catch((err) => {
113-
resolve();
114-
});
149+
return resolve();
150+
});
115151
}
116152
})
117153

118154
const getAccessibilityResults = (win) =>
119155
new Promise((resolve) => {
120156
const isHttpOrHttps = /^(http|https):$/.test(window.location.protocol);
121157
if (!isHttpOrHttps) {
122-
resolve();
158+
return resolve();
123159
}
124160

125161
function findAccessibilityAutomationElement() {
@@ -132,14 +168,14 @@ new Promise((resolve) => {
132168
const intervalID = setInterval(() => {
133169
if (count > retryCount) {
134170
clearInterval(intervalID);
135-
reject(
171+
return reject(
136172
new Error(
137173
"Accessibility Automation Scanner is not ready on the page."
138174
)
139175
);
140176
} else if (findAccessibilityAutomationElement()) {
141177
clearInterval(intervalID);
142-
resolve("Scanner set");
178+
return resolve("Scanner set");
143179
} else {
144180
count += 1;
145181
}
@@ -150,7 +186,7 @@ new Promise((resolve) => {
150186
function getResults() {
151187
function onReceivedResult(event) {
152188
win.removeEventListener("A11Y_RESULTS_RESPONSE", onReceivedResult);
153-
resolve(event.detail);
189+
return resolve(event.detail);
154190
}
155191

156192
win.addEventListener("A11Y_RESULTS_RESPONSE", onReceivedResult);
@@ -164,8 +200,8 @@ new Promise((resolve) => {
164200
waitForScannerReadiness()
165201
.then(getResults)
166202
.catch((err) => {
167-
resolve();
168-
});
203+
return resolve();
204+
});
169205
}
170206
});
171207

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

180217
function findAccessibilityAutomationElement() {
@@ -187,14 +224,14 @@ new Promise( (resolve, reject) => {
187224
const intervalID = setInterval(async () => {
188225
if (count > retryCount) {
189226
clearInterval(intervalID);
190-
reject(
227+
return reject(
191228
new Error(
192229
"Accessibility Automation Scanner is not ready on the page."
193230
)
194231
);
195232
} else if (findAccessibilityAutomationElement()) {
196233
clearInterval(intervalID);
197-
resolve("Scanner set");
234+
return resolve("Scanner set");
198235
} else {
199236
count += 1;
200237
}
@@ -204,7 +241,7 @@ new Promise( (resolve, reject) => {
204241

205242
function saveResults() {
206243
function onResultsSaved(event) {
207-
resolve();
244+
return resolve();
208245
}
209246
win.addEventListener("A11Y_RESULTS_SAVED", onResultsSaved);
210247
const e = new CustomEvent("A11Y_SAVE_RESULTS", {
@@ -219,11 +256,12 @@ new Promise( (resolve, reject) => {
219256
waitForScannerReadiness()
220257
.then(saveResults)
221258
.catch(async (err) => {
222-
resolve("Scanner is not ready on the page after multiple retries. after run");
259+
return resolve("Scanner is not ready on the page after multiple retries. after run");
223260
});
224261
}
225-
} catch(er) {
226-
resolve()
262+
} catch(error) {
263+
browserStackLog(`Error in saving results with error: ${error.message}`);
264+
return resolve();
227265
}
228266

229267
})
@@ -254,31 +292,29 @@ const shouldScanForAccessibility = (attributes) => {
254292
const included = includeTagArray.length === 0 || includeTags.some((include) => fullTestName.includes(include));
255293
shouldScanTestForAccessibility = !excluded && included;
256294
} catch (error) {
257-
browserStackLog("Error while validating test case for accessibility before scanning. Error : ", error);
295+
browserStackLog(`Error while validating test case for accessibility before scanning. Error : ${error.message}`);
258296
}
259297
}
260298

261299
return shouldScanTestForAccessibility;
262300
}
263301

264-
Cypress.on('command:start', async (command) => {
265-
if(!command || !command.attributes) return;
266-
if(command.attributes.name == 'window' || command.attributes.name == 'then' || command.attributes.name == 'wrap') {
267-
return;
268-
}
269-
270-
if (!commandsToWrap.includes(command.attributes.name)) return;
271-
272-
const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest || Cypress.mocha.getRunner().suite.ctx._runnable;
273-
274-
let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
275-
if (!shouldScanTestForAccessibility) return;
276-
277-
cy.window().then((win) => {
278-
browserStackLog('Performing scan form command ' + command.attributes.name);
279-
cy.wrap(performScan(win, {method: command.attributes.name}), {timeout: 30000});
280-
})
281-
})
302+
commandToOverwrite.forEach((command) => {
303+
Cypress.Commands.overwrite(command, (originalFn, ...args) => {
304+
const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest || Cypress.mocha.getRunner().suite.ctx._runnable;
305+
const shouldScanTestForAccessibility = shouldScanForAccessibility(attributes);
306+
const state = cy.state('current'), Subject = 'getSubjectFromChain' in cy;
307+
const stateName = state === null || state === void 0 ? void 0 : state.get('name');
308+
let stateType = null;
309+
if (!shouldScanTestForAccessibility || (stateName && stateName !== command)) {
310+
return originalFn(...args);
311+
}
312+
if(state !== null && state !== void 0){
313+
stateType = state.get('type');
314+
}
315+
performModifiedScan(originalFn, Subject, stateType, ...args);
316+
});
317+
});
282318

283319
afterEach(() => {
284320
const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest;
@@ -322,6 +358,7 @@ afterEach(() => {
322358
})
323359

324360
} catch (er) {
361+
browserStackLog(`Error in saving results with error: ${er.message}`);
325362
}
326363
})
327364
});
@@ -337,9 +374,11 @@ Cypress.Commands.add('performScan', () => {
337374
}
338375
cy.window().then(async (win) => {
339376
browserStackLog(`Performing accessibility scan`);
340-
await performScan(win);
377+
cy.wrap(performScan(win), {timeout:30000});
341378
});
342-
} catch {}
379+
} catch(error) {
380+
browserStackLog(`Error in performing scan with error: ${error.message}`);
381+
}
343382
})
344383

345384
Cypress.Commands.add('getAccessibilityResultsSummary', () => {
@@ -355,7 +394,9 @@ Cypress.Commands.add('getAccessibilityResultsSummary', () => {
355394
browserStackLog('Getting accessibility results summary');
356395
return await getAccessibilityResultsSummary(win);
357396
});
358-
} catch {}
397+
} catch(error) {
398+
browserStackLog(`Error in getting accessibilty results summary with error: ${error.message}`);
399+
}
359400

360401
});
361402

@@ -376,6 +417,12 @@ Cypress.Commands.add('getAccessibilityResults', () => {
376417
return await getAccessibilityResults(win);
377418
});
378419

379-
} catch {}
420+
} catch(error) {
421+
browserStackLog(`Error in getting accessibilty results with error: ${error.message}`);
422+
}
423+
});
380424

425+
Cypress.Commands.addQuery('performScanSubjectQuery', function (chaining, setTimeout) {
426+
this.set('timeout', setTimeout);
427+
return () => cy.getSubjectFromChain(chaining);
381428
});

bin/accessibility-automation/plugin/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const path = require("node:path");
2+
const { decodeJWTToken } = require("../../helpers/utils");
3+
const utils = require('../../helpers/utils');
24

35
const browserstackAccessibility = (on, config) => {
46
let browser_validation = true;
@@ -30,7 +32,16 @@ const browserstackAccessibility = (on, config) => {
3032
}
3133
if (browser_validation) {
3234
const ally_path = path.dirname(process.env.ACCESSIBILITY_EXTENSION_PATH)
35+
const payload = decodeJWTToken(process.env.ACCESSIBILITY_AUTH);
3336
launchOptions.extensions.push(ally_path);
37+
if(!utils.isUndefined(payload) && !utils.isUndefined(payload.a11y_core_config) && payload.a11y_core_config.domForge === true) {
38+
launchOptions.args.push("--auto-open-devtools-for-tabs");
39+
launchOptions.preferences.default["devtools"] = launchOptions.preferences.default["devtools"] || {};
40+
launchOptions.preferences.default["devtools"]["preferences"] = launchOptions.preferences.default["devtools"]["preferences"] || {};
41+
launchOptions.preferences.default["devtools"]["preferences"][
42+
"currentDockState"
43+
] = '"undocked"';
44+
}
3445
return launchOptions
3546
}
3647
}

bin/helpers/utils.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,3 +1775,23 @@ exports.getMajorVersion = (version) => {
17751775
return null;
17761776
}
17771777
}
1778+
1779+
const base64UrlDecode = (str) => {
1780+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
1781+
const buffer = Buffer.from(base64, 'base64');
1782+
return buffer.toString('utf-8');
1783+
};
1784+
1785+
exports.decodeJWTToken = (token) => {
1786+
try{
1787+
const parts = token.split('.');
1788+
if (parts.length < 2) {
1789+
throw new Error('Invalid JWT token');
1790+
}
1791+
const payload = JSON.parse(base64UrlDecode(parts[1]));
1792+
return payload
1793+
} catch (error) {
1794+
logger.err("Error in token decoding with error:", error.message);
1795+
return undefined;
1796+
}
1797+
}

0 commit comments

Comments
 (0)