From e6e31941d97febcd3f0cf3442abe7e2afdf3accb Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 26 Nov 2025 15:13:00 +0530 Subject: [PATCH] feat: adding GRR support to a11y tools --- src/lib/a11y-base-url.ts | 52 +++++++++++++++++++ src/tools/accessibility.ts | 8 +-- src/tools/accessiblity-utils/auth-config.ts | 10 +++- .../accessiblity-utils/report-fetcher.ts | 12 ++++- src/tools/accessiblity-utils/scanner.ts | 13 ++++- src/tools/sdk-utils/common/schema.ts | 49 +++++++++-------- 6 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 src/lib/a11y-base-url.ts diff --git a/src/lib/a11y-base-url.ts b/src/lib/a11y-base-url.ts new file mode 100644 index 00000000..d7fc3245 --- /dev/null +++ b/src/lib/a11y-base-url.ts @@ -0,0 +1,52 @@ +import { apiClient } from "./apiClient.js"; +import logger from "../logger.js"; +import { BrowserStackConfig } from "./types.js"; +import { getBrowserStackAuth } from "./get-auth.js"; + +const A11Y_BASE_URLS = [ + "https://api-accessibility.browserstack.com", + "https://api-accessibility-eu.browserstack.com", + "https://api-accessibility-in.browserstack.com", +] as const; + +let cachedBaseUrl: string | null = null; + +export async function getA11yBaseURL( + config: BrowserStackConfig, +): Promise { + if (cachedBaseUrl) { + logger.debug(`Using cached A11y base URL: ${cachedBaseUrl}`); + return cachedBaseUrl; + } + + logger.info( + "No cached A11y base URL found, testing available URLs with authentication", + ); + + const authString = getBrowserStackAuth(config); + const [username, password] = authString.split(":"); + const authHeader = + "Basic " + Buffer.from(`${username}:${password}`).toString("base64"); + + for (const baseUrl of A11Y_BASE_URLS) { + try { + const res = await apiClient.get({ + url: `${baseUrl}/api/automated-tests/v1/projects`, + headers: { Authorization: authHeader }, + raise_error: false, + }); + + if (res.ok) { + cachedBaseUrl = baseUrl; + logger.info(`Selected A11y base URL: ${baseUrl}`); + return baseUrl; + } + } catch (err) { + logger.debug(`Failed A11y base URL: ${baseUrl} (${err})`); + } + } + + throw new Error( + "Unable to connect to BrowserStack Accessibility API. Please check your credentials and network connection. Please open an issue on GitHub if the problem persists", + ); +} diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 5f881b12..b4fc6c76 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -104,7 +104,7 @@ async function notifyScanProgress( async function initializeScanner( config: BrowserStackConfig, ): Promise { - const scanner = new AccessibilityScanner(); + const scanner = new AccessibilityScanner(config); const auth = setupAuth(config); scanner.setAuth(auth); return scanner; @@ -113,7 +113,7 @@ async function initializeScanner( async function initializeReportFetcher( config: BrowserStackConfig, ): Promise { - const reportFetcher = new AccessibilityReportFetcher(); + const reportFetcher = new AccessibilityReportFetcher(config); const auth = setupAuth(config); reportFetcher.setAuth(auth); return reportFetcher; @@ -236,7 +236,7 @@ async function createAuthConfig( args: AuthConfigArgs, config: BrowserStackConfig, ): Promise { - const authConfig = new AccessibilityAuthConfig(); + const authConfig = new AccessibilityAuthConfig(config); const auth = setupAuth(config); authConfig.setAuth(auth); @@ -312,7 +312,7 @@ async function executeGetAuthConfig( config, ); - const authConfig = new AccessibilityAuthConfig(); + const authConfig = new AccessibilityAuthConfig(config); const auth = setupAuth(config); authConfig.setAuth(auth); diff --git a/src/tools/accessiblity-utils/auth-config.ts b/src/tools/accessiblity-utils/auth-config.ts index 9ac64fc7..1c0114f8 100644 --- a/src/tools/accessiblity-utils/auth-config.ts +++ b/src/tools/accessiblity-utils/auth-config.ts @@ -1,5 +1,7 @@ import { apiClient } from "../../lib/apiClient.js"; import logger from "../../logger.js"; +import { getA11yBaseURL } from "../../lib/a11y-base-url.js"; +import { BrowserStackConfig } from "../../lib/types.js"; export interface AuthConfigResponse { success: boolean; @@ -34,6 +36,11 @@ export interface BasicAuthData { export class AccessibilityAuthConfig { private auth: { username: string; password: string } | undefined; + private config: BrowserStackConfig; + + constructor(config: BrowserStackConfig) { + this.config = config; + } public setAuth(auth: { username: string; password: string }): void { this.auth = auth; @@ -171,8 +178,9 @@ export class AccessibilityAuthConfig { } try { + const baseUrl = await getA11yBaseURL(this.config); const response = await apiClient.get({ - url: `https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs/${configId}`, + url: `${baseUrl}/api/website-scanner/v1/auth_configs/${configId}`, headers: { Authorization: "Basic " + diff --git a/src/tools/accessiblity-utils/report-fetcher.ts b/src/tools/accessiblity-utils/report-fetcher.ts index b5ad0291..7e4b334c 100644 --- a/src/tools/accessiblity-utils/report-fetcher.ts +++ b/src/tools/accessiblity-utils/report-fetcher.ts @@ -1,4 +1,6 @@ import { apiClient } from "../../lib/apiClient.js"; +import { getA11yBaseURL } from "../../lib/a11y-base-url.js"; +import { BrowserStackConfig } from "../../lib/types.js"; interface ReportInitResponse { success: true; @@ -14,6 +16,11 @@ interface ReportResponse { export class AccessibilityReportFetcher { private auth: { username: string; password: string } | undefined; + private config: BrowserStackConfig; + + constructor(config: BrowserStackConfig) { + this.config = config; + } public setAuth(auth: { username: string; password: string }): void { this.auth = auth; @@ -21,7 +28,8 @@ export class AccessibilityReportFetcher { async getReportLink(scanId: string, scanRunId: string): Promise { // Initiate CSV link generation - const initUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?scan_run_id=${scanRunId}`; + const baseUrl = await getA11yBaseURL(this.config); + const initUrl = `${baseUrl}/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?scan_run_id=${scanRunId}`; let basicAuthHeader = undefined; if (this.auth) { @@ -42,7 +50,7 @@ export class AccessibilityReportFetcher { const taskId = initData.data.task_id; // Fetch the generated CSV link - const reportUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?task_id=${encodeURIComponent( + const reportUrl = `${baseUrl}/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?task_id=${encodeURIComponent( taskId, )}`; // Use apiClient for the report link request as well diff --git a/src/tools/accessiblity-utils/scanner.ts b/src/tools/accessiblity-utils/scanner.ts index f5e9c12f..e94de3e4 100644 --- a/src/tools/accessiblity-utils/scanner.ts +++ b/src/tools/accessiblity-utils/scanner.ts @@ -7,6 +7,8 @@ import { killExistingBrowserStackLocalProcesses, } from "../../lib/local.js"; import config from "../../config.js"; +import { getA11yBaseURL } from "../../lib/a11y-base-url.js"; +import { BrowserStackConfig } from "../../lib/types.js"; export interface AccessibilityScanResponse { success: boolean; @@ -22,6 +24,11 @@ export interface AccessibilityScanStatus { export class AccessibilityScanner { private auth: { username: string; password: string } | undefined; + private config: BrowserStackConfig; + + constructor(config: BrowserStackConfig) { + this.config = config; + } public setAuth(auth: { username: string; password: string }): void { this.auth = auth; @@ -98,8 +105,9 @@ export class AccessibilityScanner { } try { + const baseUrl = await getA11yBaseURL(this.config); const response = await apiClient.post({ - url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", + url: `${baseUrl}/api/website-scanner/v1/scans`, headers: { Authorization: "Basic " + @@ -135,8 +143,9 @@ export class AccessibilityScanner { scanRunId: string, ): Promise { try { + const baseUrl = await getA11yBaseURL(this.config); const response = await apiClient.get({ - url: `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, + url: `${baseUrl}/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, headers: { Authorization: "Basic " + diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 4815cc30..1cb6159b 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -49,31 +49,30 @@ export const SetUpPercyParamsShape = { }; // Device schema for BrowserStack Automate (supports desktop and mobile) -const DeviceSchema = z - .object({ - platform: z - .enum(["windows", "macos", "android", "ios"]) - .describe("Platform name, e.g. 'windows', 'macos', 'android', 'ios'"), - deviceName: z - .string() - .optional() - .describe( - "Device name for mobile platforms, e.g. 'iPhone 15', 'Samsung Galaxy S24'", - ), - osVersion: z - .string() - .describe("OS version, e.g. '11', 'Sequoia', '14', '17', 'latest'"), - browser: z - .string() - .optional() - .describe("Browser name, e.g. 'chrome', 'safari', 'edge', 'firefox'"), - browserVersion: z - .string() - .optional() - .describe( - "Browser version for desktop platforms only (windows, macos), e.g. '132', 'latest', 'oldest'. Not used for mobile devices (android, ios).", - ), - }); +const DeviceSchema = z.object({ + platform: z + .enum(["windows", "macos", "android", "ios"]) + .describe("Platform name, e.g. 'windows', 'macos', 'android', 'ios'"), + deviceName: z + .string() + .optional() + .describe( + "Device name for mobile platforms, e.g. 'iPhone 15', 'Samsung Galaxy S24'", + ), + osVersion: z + .string() + .describe("OS version, e.g. '11', 'Sequoia', '14', '17', 'latest'"), + browser: z + .string() + .optional() + .describe("Browser name, e.g. 'chrome', 'safari', 'edge', 'firefox'"), + browserVersion: z + .string() + .optional() + .describe( + "Browser version for desktop platforms only (windows, macos), e.g. '132', 'latest', 'oldest'. Not used for mobile devices (android, ios).", + ), +}); export const RunTestsOnBrowserStackParamsShape = { projectName: z