Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
71e28b7
TRA changes
Bhargavi-BS Nov 9, 2025
94fbd89
changes for testMap implementation
Bhargavi-BS Nov 11, 2025
c9e6862
added EOF lines
Bhargavi-BS Nov 11, 2025
8ab63b8
TRA changes
Bhargavi-BS Nov 14, 2025
506dd0b
TRA changes pt.3
Bhargavi-BS Nov 17, 2025
c876a42
fix: lint error
Bhargavi-BS Nov 17, 2025
c107fa1
accessibility changes
Bhargavi-BS Nov 17, 2025
9ada09f
fix: lint error
Bhargavi-BS Nov 17, 2025
ff58a6a
minor change
Bhargavi-BS Nov 18, 2025
7344585
minor change
Bhargavi-BS Nov 19, 2025
3a3910a
fix:lint errors
Bhargavi-BS Nov 19, 2025
04a4ab0
added null checks
Bhargavi-BS Nov 19, 2025
04f5edb
review changes pt.1
Bhargavi-BS Nov 19, 2025
dc47ae8
minor change
Bhargavi-BS Nov 19, 2025
1d9431f
fix: static testMap added
Bhargavi-BS Nov 20, 2025
2f0befb
minor change
Bhargavi-BS Nov 20, 2025
b31cdc9
minor change in try-catch
Bhargavi-BS Nov 20, 2025
bc916ed
temp: fallback for old core version
Bhargavi-BS Nov 20, 2025
4893c7a
eslintrc change
Bhargavi-BS Nov 20, 2025
1f5138f
fix for double test events in cucumber runner
Bhargavi-BS Nov 20, 2025
e932fab
env var name changed
Bhargavi-BS Nov 20, 2025
6e0a90a
minor changes
Bhargavi-BS Nov 24, 2025
724033d
removed the fallback and added alternative
Bhargavi-BS Nov 24, 2025
a25e71e
review changes pt.2
Bhargavi-BS Nov 25, 2025
e4aac46
Update src/utils/testMap.js
Bhargavi-BS Nov 26, 2025
bb86129
review changes pt.3
Bhargavi-BS Nov 26, 2025
8539ae8
review changes pt.4
Bhargavi-BS Nov 26, 2025
75a5ec9
minor change
Bhargavi-BS Nov 26, 2025
6934bf7
app accessibility changes
Bhargavi-BS Nov 26, 2025
9ca5ff5
fix: lint issues
Bhargavi-BS Nov 27, 2025
73d96ba
fixed the UTs
Bhargavi-BS Nov 27, 2025
b42efb4
minor change
Bhargavi-BS Nov 27, 2025
5627070
review changes pt.5
Bhargavi-BS Nov 27, 2025
5220e0a
fixed lint issues
Bhargavi-BS Nov 27, 2025
adda0be
minor log change
Bhargavi-BS Nov 28, 2025
e22d920
Merge branch 'nightwatch-bu-TRA' into nightwatch-app-accessibility
Bhargavi-BS Nov 28, 2025
990102c
minor change
Bhargavi-BS Nov 28, 2025
bb7870a
Merge branch 'nightwatch-bu-TRA' into nightwatch-app-accessibility
Bhargavi-BS Dec 1, 2025
7d99495
fix for wrong product map
Bhargavi-BS Dec 1, 2025
ba9b3be
Merge branch 'nightwatch-bu-TRA' into nightwatch-app-accessibility
Bhargavi-BS Dec 1, 2025
1825803
minor changes
Bhargavi-BS Dec 1, 2025
e3d9c59
minor change
Bhargavi-BS Dec 1, 2025
01aab9a
Merge branch 'nightwatch-bu-TRA' into nightwatch-app-accessibility
Bhargavi-BS Dec 1, 2025
c52b4b8
review changes pt.1 & lint fixes
Bhargavi-BS Dec 1, 2025
3b795f6
minor change
Bhargavi-BS Dec 2, 2025
143d072
lint fix
Bhargavi-BS Dec 2, 2025
6956b30
added handling of a edge case
Bhargavi-BS Dec 3, 2025
d36ada5
fixed the polling logic
Bhargavi-BS Dec 4, 2025
c68ae0b
fix for handling a edge case
Bhargavi-BS Dec 5, 2025
835daf6
Merge branch 'nightwatch-bu-TRA' into nightwatch-app-accessibility
Bhargavi-BS Dec 5, 2025
a7f66fd
fix for the timeout issue
Bhargavi-BS Dec 8, 2025
5338d1e
Merge branch 'nightwatch-bu-TRA' into nightwatch-app-accessibility
Bhargavi-BS Dec 8, 2025
c709edf
Merge branch 'main' into nightwatch-app-accessibility
Bhargavi-BS Dec 10, 2025
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
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
"by": "readonly",
"expect": "readonly",
"browser": "readonly",
"Key": "readonly"
"Key": "readonly",
"URLSearchParams": "readonly"

}
}
20 changes: 15 additions & 5 deletions nightwatch/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ module.exports = {
});

eventBroadcaster.on('TestRunStarted', async (test) => {
process.env.VALID_ALLY_PLATFORM = accessibilityAutomation.validateA11yCaps(browser);
process.env.VALID_ALLY_PLATFORM = process.env.BROWSERSTACK_APP_AUTOMATE ? accessibilityAutomation.validateAppA11yCaps(test.metadata.sessionCapabilities) : accessibilityAutomation.validateA11yCaps(browser);
await accessibilityAutomation.beforeEachExecution(test);
if (testRunner !== 'cucumber'){
const uuid = TestMap.storeTestDetails(test);
Expand Down Expand Up @@ -357,7 +357,9 @@ module.exports = {
if (helper.isAccessibilitySession() && !settings.parallel_mode) {
accessibilityAutomation.setAccessibilityCapabilities(settings);
accessibilityAutomation.commandWrapper();
helper.patchBrowserTerminateCommand();
if (!process.env.BROWSERSTACK_APP_AUTOMATE){
helper.patchBrowserTerminateCommand();
};
}
} catch (err){
Logger.debug(`Exception while setting Accessibility Automation capabilities. Error ${err}`);
Expand Down Expand Up @@ -489,8 +491,14 @@ module.exports = {
},

async beforeEach(settings) {
browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() };
browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() };
if (helper.isAppAccessibilitySession()){
browser.getAccessibilityResults = () => { return accessibilityAutomation.getAppAccessibilityResults(browser) };
browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAppAccessibilityResultsSummary(browser) };
} else {
browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() };
browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() };
}
// await accessibilityAutomation.beforeEachExecution(browser);
},

// This will be run after each test suite is finished
Expand Down Expand Up @@ -531,7 +539,9 @@ module.exports = {
if (helper.isAccessibilitySession()) {
accessibilityAutomation.setAccessibilityCapabilities(settings);
accessibilityAutomation.commandWrapper();
helper.patchBrowserTerminateCommand();
if (!process.env.BROWSERSTACK_APP_AUTOMATE){
helper.patchBrowserTerminateCommand();
};
}
} catch (err){
Logger.debug(`Exception while setting Accessibility Automation capabilities. Error ${err}`);
Expand Down
139 changes: 135 additions & 4 deletions src/accessibilityAutomation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const path = require('path');
const helper = require('./utils/helper');
const Logger = require('./utils/logger');
const {APP_ALLY_ENDPOINT, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, APP_ALLY_ISSUES_ENDPOINT} = require('./utils/constants');
const util = require('util');
const AccessibilityScripts = require('./scripts/accessibilityScripts');

Expand Down Expand Up @@ -162,14 +163,38 @@ class AccessibilityAutomation {
return false;
}

validateAppA11yCaps(capabilities = {}) {
/* Check if the current driver platform is eligible for AppAccessibility scan */
if (
capabilities?.platformName &&
String(capabilities?.platformName).toLowerCase() === 'android' &&
capabilities?.platformVersion &&
parseInt(capabilities?.platformVersion?.toString()) < 11
) {
Logger.warn(
'App Accessibility Automation tests are supported on OS version 11 and above for Android devices.'
);

return false;
}

return true;
}

async beforeEachExecution(testMetaData) {
try {
this.currentTest = browser.currentTest;
this.currentTest.shouldScanTestForAccessibility = this.shouldScanTestForAccessibility(
testMetaData
);
this.currentTest.accessibilityScanStarted = true;
this._isAccessibilitySession = this.validateA11yCaps(browser);

this._isAppAccessibility = helper.isAppAccessibilitySession();
if (this._isAppAccessibility) {
this._isAccessibilitySession = this.validateAppA11yCaps(testMetaData.metadata.sessionCapabilities);
} else {
this._isAccessibilitySession = this.validateA11yCaps(browser);
}

if (this.isAccessibilityAutomationSession() && browser && this._isAccessibilitySession) {
try {
Expand Down Expand Up @@ -267,10 +292,9 @@ class AccessibilityAutomation {
}

if (this.currentTest.shouldScanTestForAccessibility === false) {
Logger.info('Skipping Accessibility scan for this test as it\'s disabled.');

return;
}

try {
const browser = browserInstance;

Expand All @@ -279,6 +303,16 @@ class AccessibilityAutomation {

return;
}

if (helper.isAppAccessibilitySession()){
const results = await browser.executeScript(
helper.formatString(AccessibilityScripts.performScan, JSON.stringify(this.getParamsForAppAccessibility(commandName))),
{}
);
Logger.debug(util.inspect(results));

return results;
}
AccessibilityAutomation.pendingAllyReq++;
const results = await browser.executeAsyncScript(AccessibilityScripts.performScan, {
method: commandName || ''
Expand All @@ -297,9 +331,79 @@ class AccessibilityAutomation {
}
}

async getAppAccessibilityResults(browser) {
if (!helper.isBrowserstackInfra()) {
return [];
}

if (!helper.isAppAccessibilitySession()) {
Logger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results.');

return [];
}

try {
const apiUrl = `${APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_ENDPOINT}`;
const apiRespone = await this.getAppA11yResultResponse(apiUrl, browser, browser.sessionId);
const result = apiRespone?.data?.data?.issues;
Logger.debug(`Results: ${JSON.stringify(result)}`);

return result;
} catch (error) {
Logger.error('No accessibility results were found.');
Logger.debug(`getAppAccessibilityResults Failed. Error: ${error}`);

return [];
}

}

async getAppAccessibilityResultsSummary(browser) {
if (!helper.isBrowserstackInfra()) {
return {};
}

if (!helper.isAppAccessibilitySession()) {
Logger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.');

return {};
}
try {
const apiUrl = `${APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_SUMMARY_ENDPOINT}`;
const apiRespone = await this.getAppA11yResultResponse(apiUrl, browser, browser.sessionId);
const result = apiRespone?.data?.data?.summary;
Logger.debug(`Results Summary: ${JSON.stringify(result)}`);

return result;
} catch (error) {
Logger.error('No accessibility result summary were found.');
Logger.debug(`getAppAccessibilityResultsSummary Failed. Error: ${error}`);

return {};
}
}

async getAppA11yResultResponse(apiUrl, browser, sessionId){
Logger.debug('Performing scan before getting results/results summary');
await this.performScan(browser);

const upperTimeLimit = process.env.BSTACK_A11Y_POLLING_TIMEOUT ? Date.now() + parseInt(process.env.BSTACK_A11Y_POLLING_TIMEOUT) * 1000 : Date.now() + 30000;
const params = {test_run_uuid: process.env.TEST_RUN_UUID, session_id: sessionId, timestamp: Date.now()}; // Query params to pass
const header = {Authorization: `Bearer ${process.env.BSTACK_A11Y_JWT}`};
const apiRespone = await helper.pollApi(apiUrl, params, header, upperTimeLimit);
Logger.debug(`Polling Result: ${JSON.stringify(apiRespone.message)}`);

return apiRespone;

}


async saveAccessibilityResults(browser, dataForExtension = {}) {
Logger.debug('Performing scan before saving results');
await this.performScan(browser);
if (helper.isAppAccessibilitySession()){
return;
}
const results = await browser.executeAsyncScript(AccessibilityScripts.saveTestResults, dataForExtension);

return results;
Expand Down Expand Up @@ -336,7 +440,12 @@ class AccessibilityAutomation {
const originalCommandFn = originalCommand.command;

originalCommand.command = async function(...args) {
await accessibilityInstance.performScan(browser, commandName);
if (
!commandName.includes('execute') ||
!accessibilityInstance.shouldPatchExecuteScript(args.length ? args[0] : null)
) {
await accessibilityInstance.performScan(browser, commandName);
}

return originalCommandFn.apply(this, args);
};
Expand All @@ -347,6 +456,28 @@ class AccessibilityAutomation {
}
}
}

shouldPatchExecuteScript(script) {
if (!script || typeof script !== 'string') {
return true;
}

return (
script.toLowerCase().indexOf('browserstack_executor') !== -1 ||
script.toLowerCase().indexOf('browserstack_accessibility_automation_script') !== -1
);
}

getParamsForAppAccessibility(commandName) {
return {
'thTestRunUuid': process.env.TEST_RUN_UUID,
'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID,
'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT,
'authHeader': process.env.BSTACK_A11Y_JWT,
'scanTimestamp': Date.now(),
'method': commandName
};
}
}

module.exports = AccessibilityAutomation;
1 change: 1 addition & 0 deletions src/testObservability.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ class TestObservability {
accessibilityScripts.store();
}
}
process.env.IS_APP_ACCESSIBILITY = accessibilityAutomation.isAccessibilityAutomationSession() && helper.isAppAutomate();

}

Expand Down
4 changes: 3 additions & 1 deletion src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ exports.EVENTS = {
SCREENSHOT: 'testObservability:screenshot'
};
exports.ACCESSIBILITY_URL= 'https://accessibility.browserstack.com/api';

exports.APP_ALLY_ENDPOINT = 'https://app-accessibility.browserstack.com/automate';
exports.APP_ALLY_ISSUES_SUMMARY_ENDPOINT ='api/v1/issues-summary';
exports.APP_ALLY_ISSUES_ENDPOINT = 'api/v1/issues';
// Maximum size of VCS info which is allowed
exports.MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024;

Expand Down
104 changes: 104 additions & 0 deletions src/utils/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const LogPatcher = require('./logPatcher');
const BSTestOpsPatcher = new LogPatcher({});
const sessions = {};
const {execSync} = require('child_process');
const request = require('@cypress/request');

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

};

exports.isAppAccessibilitySession = () => {
return process.env.IS_APP_ACCESSIBILITY === 'true';
};

exports.isAccessibilityEnabled = (settings) => {
if (process.argv.includes('--disable-accessibility')) {return false}

Expand Down Expand Up @@ -1305,3 +1310,102 @@ exports.patchBrowserTerminateCommand = () =>{
};
};

exports.formatString = (template, ...values) => {
let i = 0;
if (template === null) {
return '';
}

return template.replace(/%s/g, () => {
const value = values[i++];

return value !== null && value !== undefined ? value : '';
});
};

exports.pollApi = async (url, params, headers, upperLimit, startTime = Date.now()) => {
params.timestamp = Math.round(Date.now() / 1000);
Logger.debug(`current timestamp ${params.timestamp}`);

try {
const queryString = new URLSearchParams(params).toString();
const fullUrl = `${url}?${queryString}`;

const response = await new Promise((resolve, reject) => {
request({
method: 'GET',
url: fullUrl,
headers: headers,
json: false
}, (error, response, body) => {
if (error) {
reject(error);
} else {
resolve(response);
}
});
});

const responseData = JSON.parse(response.body);

if (response.statusCode === 404) {
const nextPollTime = parseInt(response.headers?.next_poll_time, 10) * 1000;
Logger.debug(`nextPollTime: ${nextPollTime}`);

if (isNaN(nextPollTime)) {
Logger.warn('Invalid or missing `nextPollTime` header. Stopping polling.');

return {
data: {},
headers: response.headers || {},
message: 'Invalid nextPollTime header value. Polling stopped.'
};
}

// Stop polling if the upper time limit is reached
if (nextPollTime > upperLimit) {
Logger.warn('Polling stopped due to upper time limit.');

return {
data: {},
headers: response.headers || {},
message: 'Polling stopped due to upper time limit.'
};
}

const elapsedTime = Math.max(0, nextPollTime - Date.now());
Logger.debug(
`elapsedTime ${elapsedTime} nextPollTimes ${nextPollTime} upperLimit ${upperLimit}`
);

Logger.debug(`Polling for results again in ${elapsedTime}ms`);

// Wait for the specified time and poll again
await new Promise((resolve) => setTimeout(resolve, elapsedTime));

return exports.pollApi(url, params, headers, upperLimit, startTime);
}

return {
data: responseData,
headers: response.headers,
message: 'Polling succeeded.'
};
} catch (error) {
if (error.response) {
throw {
data: {},
headers: {},
message: error.response.body ? JSON.parse(error.response.body).message : 'Unknown error'
};
} else {
Logger.error(`Unexpected error occurred: ${error}`);

return {data: {}, headers: {}, message: 'Unexpected error occurred.'};
}
}
};