Skip to content

Commit be745e1

Browse files
Bhargavi-BSamaanbs
andauthored
Support for Nightwatch App Accessibility (#47)
* TRA changes * changes for testMap implementation * added EOF lines * TRA changes * TRA changes pt.3 * fix: lint error * accessibility changes * fix: lint error * minor change * minor change * fix:lint errors * added null checks * review changes pt.1 * minor change * fix: static testMap added * minor change * minor change in try-catch * temp: fallback for old core version * eslintrc change * fix for double test events in cucumber runner * env var name changed * minor changes * removed the fallback and added alternative * review changes pt.2 * Update src/utils/testMap.js Co-authored-by: Amaan Hakim <[email protected]> * review changes pt.3 * review changes pt.4 * minor change * app accessibility changes * fix: lint issues * fixed the UTs * minor change * review changes pt.5 * fixed lint issues * minor log change * minor change * fix for wrong product map * minor changes * minor change * review changes pt.1 & lint fixes * minor change * lint fix * added handling of a edge case * fixed the polling logic * fix for handling a edge case * fix for the timeout issue --------- Co-authored-by: Amaan Hakim <[email protected]>
1 parent 680b8ce commit be745e1

File tree

6 files changed

+261
-11
lines changed

6 files changed

+261
-11
lines changed

.eslintrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@
108108
"by": "readonly",
109109
"expect": "readonly",
110110
"browser": "readonly",
111-
"Key": "readonly"
111+
"Key": "readonly",
112+
"URLSearchParams": "readonly"
113+
112114
}
113115
}

nightwatch/globals.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ module.exports = {
260260
});
261261

262262
eventBroadcaster.on('TestRunStarted', async (test) => {
263-
process.env.VALID_ALLY_PLATFORM = accessibilityAutomation.validateA11yCaps(browser);
263+
process.env.VALID_ALLY_PLATFORM = process.env.BROWSERSTACK_APP_AUTOMATE ? accessibilityAutomation.validateAppA11yCaps(test.metadata.sessionCapabilities) : accessibilityAutomation.validateA11yCaps(browser);
264264
await accessibilityAutomation.beforeEachExecution(test);
265265
if (testRunner !== 'cucumber'){
266266
const uuid = TestMap.storeTestDetails(test);
@@ -357,7 +357,9 @@ module.exports = {
357357
if (helper.isAccessibilitySession() && !settings.parallel_mode) {
358358
accessibilityAutomation.setAccessibilityCapabilities(settings);
359359
accessibilityAutomation.commandWrapper();
360-
helper.patchBrowserTerminateCommand();
360+
if (!process.env.BROWSERSTACK_APP_AUTOMATE){
361+
helper.patchBrowserTerminateCommand();
362+
};
361363
}
362364
} catch (err){
363365
Logger.debug(`Exception while setting Accessibility Automation capabilities. Error ${err}`);
@@ -489,8 +491,14 @@ module.exports = {
489491
},
490492

491493
async beforeEach(settings) {
492-
browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() };
493-
browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() };
494+
if (helper.isAppAccessibilitySession()){
495+
browser.getAccessibilityResults = () => { return accessibilityAutomation.getAppAccessibilityResults(browser) };
496+
browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAppAccessibilityResultsSummary(browser) };
497+
} else {
498+
browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() };
499+
browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() };
500+
}
501+
// await accessibilityAutomation.beforeEachExecution(browser);
494502
},
495503

496504
// This will be run after each test suite is finished
@@ -531,7 +539,9 @@ module.exports = {
531539
if (helper.isAccessibilitySession()) {
532540
accessibilityAutomation.setAccessibilityCapabilities(settings);
533541
accessibilityAutomation.commandWrapper();
534-
helper.patchBrowserTerminateCommand();
542+
if (!process.env.BROWSERSTACK_APP_AUTOMATE){
543+
helper.patchBrowserTerminateCommand();
544+
};
535545
}
536546
} catch (err){
537547
Logger.debug(`Exception while setting Accessibility Automation capabilities. Error ${err}`);

src/accessibilityAutomation.js

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const path = require('path');
22
const helper = require('./utils/helper');
33
const Logger = require('./utils/logger');
4+
const {APP_ALLY_ENDPOINT, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, APP_ALLY_ISSUES_ENDPOINT} = require('./utils/constants');
45
const util = require('util');
56
const AccessibilityScripts = require('./scripts/accessibilityScripts');
67

@@ -162,14 +163,38 @@ class AccessibilityAutomation {
162163
return false;
163164
}
164165

166+
validateAppA11yCaps(capabilities = {}) {
167+
/* Check if the current driver platform is eligible for AppAccessibility scan */
168+
if (
169+
capabilities?.platformName &&
170+
String(capabilities?.platformName).toLowerCase() === 'android' &&
171+
capabilities?.platformVersion &&
172+
parseInt(capabilities?.platformVersion?.toString()) < 11
173+
) {
174+
Logger.warn(
175+
'App Accessibility Automation tests are supported on OS version 11 and above for Android devices.'
176+
);
177+
178+
return false;
179+
}
180+
181+
return true;
182+
}
183+
165184
async beforeEachExecution(testMetaData) {
166185
try {
167186
this.currentTest = browser.currentTest;
168187
this.currentTest.shouldScanTestForAccessibility = this.shouldScanTestForAccessibility(
169188
testMetaData
170189
);
171190
this.currentTest.accessibilityScanStarted = true;
172-
this._isAccessibilitySession = this.validateA11yCaps(browser);
191+
192+
this._isAppAccessibility = helper.isAppAccessibilitySession();
193+
if (this._isAppAccessibility) {
194+
this._isAccessibilitySession = this.validateAppA11yCaps(testMetaData.metadata.sessionCapabilities);
195+
} else {
196+
this._isAccessibilitySession = this.validateA11yCaps(browser);
197+
}
173198

174199
if (this.isAccessibilityAutomationSession() && browser && this._isAccessibilitySession) {
175200
try {
@@ -267,10 +292,9 @@ class AccessibilityAutomation {
267292
}
268293

269294
if (this.currentTest.shouldScanTestForAccessibility === false) {
270-
Logger.info('Skipping Accessibility scan for this test as it\'s disabled.');
271-
272295
return;
273296
}
297+
274298
try {
275299
const browser = browserInstance;
276300

@@ -279,6 +303,16 @@ class AccessibilityAutomation {
279303

280304
return;
281305
}
306+
307+
if (helper.isAppAccessibilitySession()){
308+
const results = await browser.executeScript(
309+
helper.formatString(AccessibilityScripts.performScan, JSON.stringify(this.getParamsForAppAccessibility(commandName))),
310+
{}
311+
);
312+
Logger.debug(util.inspect(results));
313+
314+
return results;
315+
}
282316
AccessibilityAutomation.pendingAllyReq++;
283317
const results = await browser.executeAsyncScript(AccessibilityScripts.performScan, {
284318
method: commandName || ''
@@ -297,9 +331,79 @@ class AccessibilityAutomation {
297331
}
298332
}
299333

334+
async getAppAccessibilityResults(browser) {
335+
if (!helper.isBrowserstackInfra()) {
336+
return [];
337+
}
338+
339+
if (!helper.isAppAccessibilitySession()) {
340+
Logger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results.');
341+
342+
return [];
343+
}
344+
345+
try {
346+
const apiUrl = `${APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_ENDPOINT}`;
347+
const apiRespone = await this.getAppA11yResultResponse(apiUrl, browser, browser.sessionId);
348+
const result = apiRespone?.data?.data?.issues;
349+
Logger.debug(`Results: ${JSON.stringify(result)}`);
350+
351+
return result;
352+
} catch (error) {
353+
Logger.error('No accessibility results were found.');
354+
Logger.debug(`getAppAccessibilityResults Failed. Error: ${error}`);
355+
356+
return [];
357+
}
358+
359+
}
360+
361+
async getAppAccessibilityResultsSummary(browser) {
362+
if (!helper.isBrowserstackInfra()) {
363+
return {};
364+
}
365+
366+
if (!helper.isAppAccessibilitySession()) {
367+
Logger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.');
368+
369+
return {};
370+
}
371+
try {
372+
const apiUrl = `${APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_SUMMARY_ENDPOINT}`;
373+
const apiRespone = await this.getAppA11yResultResponse(apiUrl, browser, browser.sessionId);
374+
const result = apiRespone?.data?.data?.summary;
375+
Logger.debug(`Results Summary: ${JSON.stringify(result)}`);
376+
377+
return result;
378+
} catch (error) {
379+
Logger.error('No accessibility result summary were found.');
380+
Logger.debug(`getAppAccessibilityResultsSummary Failed. Error: ${error}`);
381+
382+
return {};
383+
}
384+
}
385+
386+
async getAppA11yResultResponse(apiUrl, browser, sessionId){
387+
Logger.debug('Performing scan before getting results/results summary');
388+
await this.performScan(browser);
389+
390+
const upperTimeLimit = process.env.BSTACK_A11Y_POLLING_TIMEOUT ? Date.now() + parseInt(process.env.BSTACK_A11Y_POLLING_TIMEOUT) * 1000 : Date.now() + 30000;
391+
const params = {test_run_uuid: process.env.TEST_RUN_UUID, session_id: sessionId, timestamp: Date.now()}; // Query params to pass
392+
const header = {Authorization: `Bearer ${process.env.BSTACK_A11Y_JWT}`};
393+
const apiRespone = await helper.pollApi(apiUrl, params, header, upperTimeLimit);
394+
Logger.debug(`Polling Result: ${JSON.stringify(apiRespone.message)}`);
395+
396+
return apiRespone;
397+
398+
}
399+
400+
300401
async saveAccessibilityResults(browser, dataForExtension = {}) {
301402
Logger.debug('Performing scan before saving results');
302403
await this.performScan(browser);
404+
if (helper.isAppAccessibilitySession()){
405+
return;
406+
}
303407
const results = await browser.executeAsyncScript(AccessibilityScripts.saveTestResults, dataForExtension);
304408

305409
return results;
@@ -336,7 +440,12 @@ class AccessibilityAutomation {
336440
const originalCommandFn = originalCommand.command;
337441

338442
originalCommand.command = async function(...args) {
339-
await accessibilityInstance.performScan(browser, commandName);
443+
if (
444+
!commandName.includes('execute') ||
445+
!accessibilityInstance.shouldPatchExecuteScript(args.length ? args[0] : null)
446+
) {
447+
await accessibilityInstance.performScan(browser, commandName);
448+
}
340449

341450
return originalCommandFn.apply(this, args);
342451
};
@@ -347,6 +456,28 @@ class AccessibilityAutomation {
347456
}
348457
}
349458
}
459+
460+
shouldPatchExecuteScript(script) {
461+
if (!script || typeof script !== 'string') {
462+
return true;
463+
}
464+
465+
return (
466+
script.toLowerCase().indexOf('browserstack_executor') !== -1 ||
467+
script.toLowerCase().indexOf('browserstack_accessibility_automation_script') !== -1
468+
);
469+
}
470+
471+
getParamsForAppAccessibility(commandName) {
472+
return {
473+
'thTestRunUuid': process.env.TEST_RUN_UUID,
474+
'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID,
475+
'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT,
476+
'authHeader': process.env.BSTACK_A11Y_JWT,
477+
'scanTimestamp': Date.now(),
478+
'method': commandName
479+
};
480+
}
350481
}
351482

352483
module.exports = AccessibilityAutomation;

src/testObservability.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ class TestObservability {
235235
accessibilityScripts.store();
236236
}
237237
}
238+
process.env.IS_APP_ACCESSIBILITY = accessibilityAutomation.isAccessibilityAutomationSession() && helper.isAppAutomate();
238239

239240
}
240241

src/utils/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ exports.EVENTS = {
1818
SCREENSHOT: 'testObservability:screenshot'
1919
};
2020
exports.ACCESSIBILITY_URL= 'https://accessibility.browserstack.com/api';
21-
21+
exports.APP_ALLY_ENDPOINT = 'https://app-accessibility.browserstack.com/automate';
22+
exports.APP_ALLY_ISSUES_SUMMARY_ENDPOINT ='api/v1/issues-summary';
23+
exports.APP_ALLY_ISSUES_ENDPOINT = 'api/v1/issues';
2224
// Maximum size of VCS info which is allowed
2325
exports.MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024;
2426

src/utils/helper.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const LogPatcher = require('./logPatcher');
1717
const BSTestOpsPatcher = new LogPatcher({});
1818
const sessions = {};
1919
const {execSync} = require('child_process');
20+
const request = require('@cypress/request');
2021

2122
console = {};
2223
Object.keys(consoleHolder).forEach(method => {
@@ -101,6 +102,10 @@ exports.isTestHubBuild = (pluginSettings = {}, isBuildStart = false) => {
101102

102103
};
103104

105+
exports.isAppAccessibilitySession = () => {
106+
return process.env.IS_APP_ACCESSIBILITY === 'true';
107+
};
108+
104109
exports.isAccessibilityEnabled = (settings) => {
105110
if (process.argv.includes('--disable-accessibility')) {return false}
106111

@@ -1305,3 +1310,102 @@ exports.patchBrowserTerminateCommand = () =>{
13051310
};
13061311
};
13071312

1313+
exports.formatString = (template, ...values) => {
1314+
let i = 0;
1315+
if (template === null) {
1316+
return '';
1317+
}
1318+
1319+
return template.replace(/%s/g, () => {
1320+
const value = values[i++];
1321+
1322+
return value !== null && value !== undefined ? value : '';
1323+
});
1324+
};
1325+
1326+
exports.pollApi = async (url, params, headers, upperLimit, startTime = Date.now()) => {
1327+
params.timestamp = Math.round(Date.now() / 1000);
1328+
Logger.debug(`current timestamp ${params.timestamp}`);
1329+
1330+
try {
1331+
const queryString = new URLSearchParams(params).toString();
1332+
const fullUrl = `${url}?${queryString}`;
1333+
1334+
const response = await new Promise((resolve, reject) => {
1335+
request({
1336+
method: 'GET',
1337+
url: fullUrl,
1338+
headers: headers,
1339+
json: false
1340+
}, (error, response, body) => {
1341+
if (error) {
1342+
reject(error);
1343+
} else {
1344+
resolve(response);
1345+
}
1346+
});
1347+
});
1348+
1349+
const responseData = JSON.parse(response.body);
1350+
1351+
if (response.statusCode === 404) {
1352+
const nextPollTime = parseInt(response.headers?.next_poll_time, 10) * 1000;
1353+
Logger.debug(`nextPollTime: ${nextPollTime}`);
1354+
1355+
if (isNaN(nextPollTime)) {
1356+
Logger.warn('Invalid or missing `nextPollTime` header. Stopping polling.');
1357+
1358+
return {
1359+
data: {},
1360+
headers: response.headers || {},
1361+
message: 'Invalid nextPollTime header value. Polling stopped.'
1362+
};
1363+
}
1364+
1365+
// Stop polling if the upper time limit is reached
1366+
if (nextPollTime > upperLimit) {
1367+
Logger.warn('Polling stopped due to upper time limit.');
1368+
1369+
return {
1370+
data: {},
1371+
headers: response.headers || {},
1372+
message: 'Polling stopped due to upper time limit.'
1373+
};
1374+
}
1375+
1376+
const elapsedTime = Math.max(0, nextPollTime - Date.now());
1377+
Logger.debug(
1378+
`elapsedTime ${elapsedTime} nextPollTimes ${nextPollTime} upperLimit ${upperLimit}`
1379+
);
1380+
1381+
Logger.debug(`Polling for results again in ${elapsedTime}ms`);
1382+
1383+
// Wait for the specified time and poll again
1384+
await new Promise((resolve) => setTimeout(resolve, elapsedTime));
1385+
1386+
return exports.pollApi(url, params, headers, upperLimit, startTime);
1387+
}
1388+
1389+
return {
1390+
data: responseData,
1391+
headers: response.headers,
1392+
message: 'Polling succeeded.'
1393+
};
1394+
} catch (error) {
1395+
if (error.response) {
1396+
throw {
1397+
data: {},
1398+
headers: {},
1399+
message: error.response.body ? JSON.parse(error.response.body).message : 'Unknown error'
1400+
};
1401+
} else {
1402+
Logger.error(`Unexpected error occurred: ${error}`);
1403+
1404+
return {data: {}, headers: {}, message: 'Unexpected error occurred.'};
1405+
}
1406+
}
1407+
};
1408+
1409+
1410+
1411+

0 commit comments

Comments
 (0)