Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions src/lib/a11y-base-url.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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",
);
}
8 changes: 4 additions & 4 deletions src/tools/accessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async function notifyScanProgress(
async function initializeScanner(
config: BrowserStackConfig,
): Promise<AccessibilityScanner> {
const scanner = new AccessibilityScanner();
const scanner = new AccessibilityScanner(config);
const auth = setupAuth(config);
scanner.setAuth(auth);
return scanner;
Expand All @@ -113,7 +113,7 @@ async function initializeScanner(
async function initializeReportFetcher(
config: BrowserStackConfig,
): Promise<AccessibilityReportFetcher> {
const reportFetcher = new AccessibilityReportFetcher();
const reportFetcher = new AccessibilityReportFetcher(config);
const auth = setupAuth(config);
reportFetcher.setAuth(auth);
return reportFetcher;
Expand Down Expand Up @@ -236,7 +236,7 @@ async function createAuthConfig(
args: AuthConfigArgs,
config: BrowserStackConfig,
): Promise<any> {
const authConfig = new AccessibilityAuthConfig();
const authConfig = new AccessibilityAuthConfig(config);
const auth = setupAuth(config);
authConfig.setAuth(auth);

Expand Down Expand Up @@ -312,7 +312,7 @@ async function executeGetAuthConfig(
config,
);

const authConfig = new AccessibilityAuthConfig();
const authConfig = new AccessibilityAuthConfig(config);
const auth = setupAuth(config);
authConfig.setAuth(auth);

Expand Down
10 changes: 9 additions & 1 deletion src/tools/accessiblity-utils/auth-config.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -171,8 +178,9 @@ export class AccessibilityAuthConfig {
}

try {
const baseUrl = await getA11yBaseURL(this.config);
const response = await apiClient.get<AuthConfigResponse>({
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 " +
Expand Down
12 changes: 10 additions & 2 deletions src/tools/accessiblity-utils/report-fetcher.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,14 +16,20 @@ 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;
}

async getReportLink(scanId: string, scanRunId: string): Promise<string> {
// 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) {
Expand All @@ -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
Expand Down
13 changes: 11 additions & 2 deletions src/tools/accessiblity-utils/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -98,8 +105,9 @@ export class AccessibilityScanner {
}

try {
const baseUrl = await getA11yBaseURL(this.config);
const response = await apiClient.post<AccessibilityScanResponse>({
url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/scans",
url: `${baseUrl}/api/website-scanner/v1/scans`,
headers: {
Authorization:
"Basic " +
Expand Down Expand Up @@ -135,8 +143,9 @@ export class AccessibilityScanner {
scanRunId: string,
): Promise<AccessibilityScanStatus> {
try {
const baseUrl = await getA11yBaseURL(this.config);
const response = await apiClient.get<AccessibilityScanStatus>({
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 " +
Expand Down
49 changes: 24 additions & 25 deletions src/tools/sdk-utils/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down