@browserstack/mcp-server 2.2.5-beta.2 → 2.2.5-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/lib/accessibility-routing.d.ts +5 -0
  2. package/dist/lib/accessibility-routing.js +5 -0
  3. package/dist/server-factory.js +6 -1
  4. package/dist/tools/accessibility.js +588 -67
  5. package/dist/tools/accessiblity-utils/app-report-fetcher.d.ts +341 -0
  6. package/dist/tools/accessiblity-utils/app-report-fetcher.js +731 -0
  7. package/dist/tools/accessiblity-utils/paginate.d.ts +21 -0
  8. package/dist/tools/accessiblity-utils/paginate.js +44 -0
  9. package/dist/tools/accessiblity-utils/report-fetcher.d.ts +116 -7
  10. package/dist/tools/accessiblity-utils/report-fetcher.js +273 -30
  11. package/dist/tools/accessiblity-utils/report-parser.d.ts +41 -1
  12. package/dist/tools/accessiblity-utils/report-parser.js +142 -43
  13. package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +2 -0
  14. package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +15 -5
  15. package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +2 -1
  16. package/dist/tools/appautomate-utils/appium-sdk/constants.js +6 -1
  17. package/dist/tools/appautomate-utils/appium-sdk/handler.js +11 -1
  18. package/dist/tools/appautomate-utils/appium-sdk/instructions.d.ts +3 -1
  19. package/dist/tools/appautomate-utils/appium-sdk/instructions.js +2 -2
  20. package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.d.ts +2 -0
  21. package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.js +32 -0
  22. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.d.ts +3 -1
  23. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.js +19 -6
  24. package/dist/tools/appautomate-utils/native-execution/appautomate.d.ts +7 -2
  25. package/dist/tools/appautomate-utils/native-execution/appautomate.js +15 -12
  26. package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
  27. package/dist/tools/appautomate-utils/native-execution/constants.js +6 -1
  28. package/dist/tools/appautomate.js +29 -16
  29. package/dist/tools/failurelogs-utils/app-automate.js +1 -1
  30. package/dist/tools/testmanagement-utils/create-testrun.d.ts +1 -1
  31. package/dist/tools/testmanagement-utils/update-testrun.d.ts +1 -1
  32. package/dist/tools/tool-handoff.d.ts +5 -4
  33. package/dist/tools/tool-handoff.js +5 -10
  34. package/package.json +1 -1
  35. package/dist/tools/accessiblity-utils/accessibility-rag.d.ts +0 -12
  36. package/dist/tools/accessiblity-utils/accessibility-rag.js +0 -59
@@ -11,12 +11,32 @@ type SimplifiedAccessibilityIssue = {
11
11
  source_test?: string;
12
12
  /** Grouped reports only: how many duplicate rows point at this issue. */
13
13
  duplicate_count?: number;
14
+ /** Read for one page: how many times the issue occurs on that page. */
15
+ occurrences_on_page?: number;
16
+ /** Read for one page: the page the issue was first found on, when another one. */
17
+ first_found_on?: string;
18
+ };
19
+ /** One page of a report, counted as the dashboard's page view counts it. */
20
+ type PageSummary = {
21
+ url: string;
22
+ /** Every occurrence on the page, repeats of issues first found elsewhere included. */
23
+ occurrences: number;
24
+ /** The issues those occurrences are of. */
25
+ distinct_issues: number;
26
+ /** Of those, the ones first found on this page. */
27
+ first_found_here: number;
28
+ /** Occurrences by severity. */
29
+ by_severity: Record<string, number>;
30
+ /** When nothing is on the page: report pages whose address looks like it. */
31
+ similar_pages?: string[];
14
32
  };
15
33
  type PaginationOptions = {
16
34
  /** How many JSON-chars max per “page” (default 10000) */
17
35
  maxCharacterLength?: number;
18
36
  /** Character offset to start from (default 0) */
19
37
  nextPage?: number;
38
+ /** Only the issues on this page. */
39
+ pageUrl?: string;
20
40
  };
21
41
  type PaginatedResult = {
22
42
  records: SimplifiedAccessibilityIssue[];
@@ -25,12 +45,32 @@ type PaginatedResult = {
25
45
  /** Main issues only when the report is grouped, so it matches the run's issue count. */
26
46
  total_issues: number;
27
47
  next_page: number | null;
48
+ /** How many issues the pages after this one hold. */
49
+ remaining: number;
28
50
  /** Duplicate rows left out of `records`; null when the report is not grouped. */
29
51
  duplicate_issues: number | null;
30
52
  /** Of those, the ones whose main issue could not be identified (counted only here). */
31
53
  unlinked_duplicates: number;
32
54
  /** Issues hidden in the dashboard and left out; null when the export has no Issue status. */
33
55
  hidden_issues: number | null;
56
+ /** Set when the report was read for one page. */
57
+ page?: PageSummary;
58
+ };
59
+ /** A main issue as two runs are matched on: the rule, the element and the page. */
60
+ export type RunIssue = {
61
+ key: string;
62
+ issue_type: string;
63
+ severity: string;
64
+ component: string;
65
+ affected_page: string;
66
+ };
67
+ /** A report's main issues that the dashboard counts (not hidden), keyed for comparing runs. */
68
+ export declare function loadMainIssues(reportLink: string): Promise<RunIssue[]>;
69
+ /** Which issues of `current` are new since `base`, which of `base` are gone, and how many remain. */
70
+ export declare function compareIssues(base: RunIssue[], current: RunIssue[]): {
71
+ added: RunIssue[];
72
+ resolved: RunIssue[];
73
+ retained: number;
34
74
  };
35
- export declare function parseAccessibilityReportFromCSV(reportLink: string, { maxCharacterLength, nextPage }?: PaginationOptions): Promise<PaginatedResult>;
75
+ export declare function parseAccessibilityReportFromCSV(reportLink: string, { maxCharacterLength, nextPage, pageUrl }?: PaginationOptions): Promise<PaginatedResult>;
36
76
  export {};
@@ -1,6 +1,7 @@
1
1
  import { apiClient } from "../../lib/apiClient.js";
2
2
  import { parse } from "csv-parse/sync";
3
3
  import logger from "../../logger.js";
4
+ import { pageOf, sortBySeverity } from "./paginate.js";
4
5
  const SOURCE_TEST_COLUMN = "Source test";
5
6
  const DUPLICATE_COLUMN = "Duplicate Issue of";
6
7
  const ISSUE_LINK_COLUMN = "Issue link";
@@ -14,8 +15,29 @@ function isMainIssue(row) {
14
15
  const value = (row[DUPLICATE_COLUMN] ?? "").trim();
15
16
  return value === "" || value.toUpperCase() === "NA";
16
17
  }
17
- export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 5000, nextPage = 0 } = {}) {
18
- // 1) Download & parse
18
+ /** A page address as the report writes it, give or take host case, trailing slash and hash. */
19
+ function samePage(url) {
20
+ try {
21
+ const u = new URL(url.trim());
22
+ return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}${u.search}`;
23
+ }
24
+ catch {
25
+ return url.trim().replace(/\/+$/, "");
26
+ }
27
+ }
28
+ function recordOfRow(row, hasSourceTest) {
29
+ return {
30
+ issue_type: row["Issue type"],
31
+ component: row["Component"],
32
+ issue_description: row["Issue description"],
33
+ HTML_snippet: row["HTML snippet"],
34
+ how_to_fix: row["How to fix this issue"],
35
+ severity: (row["Severity"] || "unknown").trim(),
36
+ affected_page: row["Affected page"],
37
+ ...(hasSourceTest ? { source_test: row[SOURCE_TEST_COLUMN] } : {}),
38
+ };
39
+ }
40
+ async function loadReport(reportLink) {
19
41
  const res = await apiClient.get({ url: reportLink });
20
42
  if (!res.ok)
21
43
  throw new Error(`Failed to download report: ${res.statusText}`);
@@ -36,6 +58,119 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
36
58
  logger.error(`[AccessibilityReport] CSV parse failed (${text.length} chars): ${err instanceof Error ? err.message : String(err)}`);
37
59
  throw err;
38
60
  }
61
+ return { parsed, columns };
62
+ }
63
+ /**
64
+ * One page of a report: every occurrence on it, as the dashboard's page view counts, with
65
+ * each issue listed once. A grouped report records a repeat under the page it occurs on but
66
+ * points it at its main issue, which may be on another page; those count here too.
67
+ */
68
+ function pageIssues(parsed, columns, pageUrl) {
69
+ const grouped = columns.includes(DUPLICATE_COLUMN);
70
+ const hasSourceTest = columns.includes(SOURCE_TEST_COLUMN);
71
+ const target = samePage(pageUrl);
72
+ const visible = parsed.filter((row) => !isHidden(row));
73
+ const onPage = visible.filter((row) => samePage(row["Affected page"] ?? "") === target);
74
+ const mainByLink = new Map();
75
+ for (const row of visible) {
76
+ if (!grouped || isMainIssue(row)) {
77
+ mainByLink.set((row[ISSUE_LINK_COLUMN] ?? "").trim(), row);
78
+ }
79
+ }
80
+ const issues = new Map();
81
+ const bySeverity = {};
82
+ onPage.forEach((row, i) => {
83
+ const severity = (row["Severity"] || "unknown").trim();
84
+ bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
85
+ const mainLink = grouped && !isMainIssue(row)
86
+ ? (row[DUPLICATE_COLUMN] ?? "").trim()
87
+ : (row[ISSUE_LINK_COLUMN] ?? "").trim();
88
+ const key = mainLink || `row-${i}`;
89
+ let issue = issues.get(key);
90
+ if (!issue) {
91
+ const main = mainByLink.get(mainLink) ?? row;
92
+ issue = {
93
+ ...recordOfRow(main, hasSourceTest),
94
+ affected_page: row["Affected page"],
95
+ occurrences_on_page: 0,
96
+ };
97
+ if (samePage(main["Affected page"] ?? "") !== target) {
98
+ issue.first_found_on = main["Affected page"];
99
+ }
100
+ issues.set(key, issue);
101
+ }
102
+ issue.occurrences_on_page = (issue.occurrences_on_page ?? 0) + 1;
103
+ });
104
+ const all = [...issues.values()];
105
+ const page = {
106
+ url: pageUrl,
107
+ occurrences: onPage.length,
108
+ distinct_issues: all.length,
109
+ first_found_here: all.filter((issue) => !issue.first_found_on).length,
110
+ by_severity: bySeverity,
111
+ };
112
+ if (onPage.length === 0) {
113
+ const tail = new URL(target, "https://x").pathname.split("/").filter(Boolean).pop() ??
114
+ "";
115
+ const stem = tail.split("-").slice(0, 2).join("-");
116
+ // A short row may have no Affected page at all.
117
+ const pages = [
118
+ ...new Set(visible
119
+ .map((row) => row["Affected page"])
120
+ .filter((p) => Boolean(p))),
121
+ ];
122
+ page.similar_pages = pages
123
+ .filter((p) => stem && samePage(p).includes(stem))
124
+ .slice(0, 5);
125
+ }
126
+ return { all, page };
127
+ }
128
+ /** A report's main issues that the dashboard counts (not hidden), keyed for comparing runs. */
129
+ export async function loadMainIssues(reportLink) {
130
+ const { parsed, columns } = await loadReport(reportLink);
131
+ const grouped = columns.includes(DUPLICATE_COLUMN);
132
+ return parsed
133
+ .filter((row) => (!grouped || isMainIssue(row)) && !isHidden(row))
134
+ .map((row) => {
135
+ const element = (row["CSS selector"] || row["Component"] || "").trim();
136
+ return {
137
+ key: [
138
+ row["Issue type"],
139
+ element,
140
+ samePage(row["Affected page"] ?? ""),
141
+ ].join("|"),
142
+ issue_type: row["Issue type"],
143
+ severity: (row["Severity"] || "unknown").trim(),
144
+ component: row["Component"],
145
+ affected_page: row["Affected page"],
146
+ };
147
+ });
148
+ }
149
+ /** Which issues of `current` are new since `base`, which of `base` are gone, and how many remain. */
150
+ export function compareIssues(base, current) {
151
+ const before = new Set(base.map((issue) => issue.key));
152
+ const after = new Set(current.map((issue) => issue.key));
153
+ return {
154
+ added: sortBySeverity(current.filter((issue) => !before.has(issue.key))),
155
+ resolved: sortBySeverity(base.filter((issue) => !after.has(issue.key))),
156
+ retained: current.filter((issue) => before.has(issue.key)).length,
157
+ };
158
+ }
159
+ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 5000, nextPage = 0, pageUrl } = {}) {
160
+ // 1) Download & parse
161
+ const { parsed, columns } = await loadReport(reportLink);
162
+ if (pageUrl) {
163
+ const { all, page } = pageIssues(parsed, columns, pageUrl);
164
+ const slice = pageOf(sortBySeverity(all), { maxCharacterLength, nextPage });
165
+ return {
166
+ ...slice,
167
+ total_issues: all.length,
168
+ duplicate_issues: null,
169
+ unlinked_duplicates: 0,
170
+ hidden_issues: null,
171
+ page,
172
+ };
173
+ }
39
174
  // 2) Leave out hidden issues and fold duplicates into their main issue, as the
40
175
  // dashboard's count does.
41
176
  //
@@ -55,14 +190,7 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
55
190
  const recordOf = new Map();
56
191
  const all = visibleMainRows.map((row) => {
57
192
  const record = {
58
- issue_type: row["Issue type"],
59
- component: row["Component"],
60
- issue_description: row["Issue description"],
61
- HTML_snippet: row["HTML snippet"],
62
- how_to_fix: row["How to fix this issue"],
63
- severity: (row["Severity"] || "unknown").trim(),
64
- affected_page: row["Affected page"],
65
- ...(hasSourceTest ? { source_test: row[SOURCE_TEST_COLUMN] } : {}),
193
+ ...recordOfRow(row, hasSourceTest),
66
194
  ...(linkable ? { duplicate_count: 0 } : {}),
67
195
  };
68
196
  recordOf.set(row, record);
@@ -91,40 +219,11 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
91
219
  else
92
220
  unlinkedDuplicates++;
93
221
  }
94
- const totalIssues = all.length;
95
- // 3) Sort by severity
96
- const order = {
97
- critical: 0,
98
- serious: 1,
99
- moderate: 2,
100
- minor: 3,
101
- };
102
- all.sort((a, b) => (order[a.severity] ?? 99) - (order[b.severity] ?? 99));
103
- // 4) Walk to the starting offset
104
- let charCursor = 0;
105
- let idx = 0;
106
- for (; idx < all.length; idx++) {
107
- const len = JSON.stringify(all[idx]).length;
108
- if (charCursor + len > nextPage)
109
- break;
110
- charCursor += len;
111
- }
112
- // 5) Collect up to maxCharacterLength
113
- const page = [];
114
- for (let i = idx; i < all.length; i++) {
115
- const recStr = JSON.stringify(all[i]);
116
- if (charCursor - nextPage + recStr.length > maxCharacterLength)
117
- break;
118
- page.push(all[i]);
119
- charCursor += recStr.length;
120
- }
121
- const pageLength = page.length;
122
- const hasMore = idx + pageLength < totalIssues;
222
+ // 3) Most severe first, in pages by JSON length
223
+ const page = pageOf(sortBySeverity(all), { maxCharacterLength, nextPage });
123
224
  return {
124
- records: page,
125
- next_page: hasMore ? charCursor : null,
126
- page_length: pageLength,
127
- total_issues: totalIssues,
225
+ ...page,
226
+ total_issues: all.length,
128
227
  duplicate_issues: grouped ? duplicates : null,
129
228
  unlinked_duplicates: unlinkedDuplicates,
130
229
  hidden_issues: columns.includes(STATUS_COLUMN)
@@ -4,4 +4,6 @@ export declare function generateAppBrowserStackYMLInstructions(config: {
4
4
  platforms?: string[];
5
5
  testingFramework?: string;
6
6
  projectName?: string;
7
+ /** Run App Accessibility checks during the tests. */
8
+ accessibility?: boolean;
7
9
  }, appPath?: string): string;
@@ -1,4 +1,12 @@
1
1
  import { APP_DEVICE_CONFIGS, AppSDKSupportedTestingFrameworkEnum, DEFAULT_APP_PATH, USERNAME_PLACEHOLDER, ACCESS_KEY_PLACEHOLDER, createStep, } from "./index.js";
2
+ /**
3
+ * The App Accessibility settings written when checks are on. Only the WCAG version is
4
+ * spelled out, as the one most often changed; the SDK's other defaults apply.
5
+ */
6
+ const ACCESSIBILITY_OPTIONS = `accessibilityOptions:
7
+ wcagVersion: wcag22aa
8
+ includeIssueType:
9
+ bestPractice: true`;
2
10
  export function generateAppBrowserStackYMLInstructions(config, appPath = DEFAULT_APP_PATH) {
3
11
  if (config.testingFramework ===
4
12
  AppSDKSupportedTestingFrameworkEnum.nightwatch ||
@@ -12,6 +20,7 @@ export function generateAppBrowserStackYMLInstructions(config, appPath = DEFAULT
12
20
  const buildName = config.projectName
13
21
  ? `${config.projectName}-AppAutomate-Build`
14
22
  : "bstack-demo";
23
+ // A YAML comment, not "//": YAML reads a "//" line as a key.
15
24
  const configContent = `\`\`\`yaml
16
25
  userName: ${USERNAME_PLACEHOLDER}
17
26
  accessKey: ${ACCESS_KEY_PLACEHOLDER}
@@ -19,21 +28,22 @@ app: ${appPath}
19
28
  platforms:
20
29
  ${platformConfigs}
21
30
  parallelsPerPlatform: 1
22
- browserstackLocal: true
23
- // TODO: replace projectName and buildName according to actual project
31
+ browserstackLocal: false
32
+ # Replace projectName and buildName according to the actual project
24
33
  projectName: ${projectName}
25
34
  buildName: ${buildName}
26
35
  debug: true
27
36
  networkLogs: true
28
37
  percy: false
29
38
  percyCaptureMode: auto
30
- accessibility: false
39
+ accessibility: ${config.accessibility ? "true" : "false"}${config.accessibility ? `\n${ACCESSIBILITY_OPTIONS}` : ""}
31
40
  \`\`\`
32
41
 
33
42
  **Important notes:**
34
43
  - Replace \`app: ${appPath}\` with the path to your actual app file (e.g., \`./SampleApp.apk\` for Android or \`./SampleApp.ipa\` for iOS)
35
44
  - You can upload your app using BrowserStack's App Upload API or manually through the dashboard
36
45
  - Set \`browserstackLocal: true\` if you need to test with local/staging servers
46
+ - Keep \`osVersion\` unquoted: the SDK parses it as a number, and its App Accessibility module reads that key (not \`platformVersion\`)
37
47
  - Adjust \`parallelsPerPlatform\` based on your subscription limits`;
38
48
  const stepTitle = "Update browserstack.yml file with App Automate configuration:";
39
49
  const stepDescription = `Create or update the browserstack.yml file in your project root with the following content:
@@ -47,7 +57,7 @@ function generatePlatformConfigs(config) {
47
57
  .map((env) => {
48
58
  return ` - platformName: ${env.platform}
49
59
  deviceName: "${env.deviceName}"
50
- platformVersion: "${env.osVersion}"`;
60
+ osVersion: ${env.osVersion}`;
51
61
  })
52
62
  .join("\n");
53
63
  }
@@ -60,7 +70,7 @@ function generatePlatformConfigs(config) {
60
70
  return devices
61
71
  .map((device) => ` - platformName: ${platform}
62
72
  deviceName: ${device.deviceName}
63
- platformVersion: "${device.platformVersion}"`)
73
+ osVersion: ${device.platformVersion}`)
64
74
  .join("\n");
65
75
  })
66
76
  .filter(Boolean)
@@ -21,7 +21,7 @@ export declare const MobileDeviceSchema: z.ZodObject<{
21
21
  deviceName: z.ZodString;
22
22
  osVersion: z.ZodString;
23
23
  }, z.core.$strip>;
24
- export declare const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead.";
24
+ export declare const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead. Set accessibility: true to also run App Accessibility (WCAG) checks on the screens the tests interact with; the issues are then read with fetchAccessibilityIssues.";
25
25
  export declare const SETUP_APP_AUTOMATE_SCHEMA: {
26
26
  detectedFramework: z.ZodEnum<typeof AppSDKSupportedFrameworkEnum>;
27
27
  detectedTestingFramework: z.ZodEnum<typeof AppSDKSupportedTestingFrameworkEnum>;
@@ -36,4 +36,5 @@ export declare const SETUP_APP_AUTOMATE_SCHEMA: {
36
36
  }, z.core.$strip>>>;
37
37
  appPath: z.ZodString;
38
38
  project: z.ZodDefault<z.ZodOptional<z.ZodString>>;
39
+ accessibility: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
39
40
  };
@@ -31,7 +31,7 @@ export const MobileDeviceSchema = z.object({
31
31
  osVersion: z.string().describe("OS version, e.g. '14', '16', '17', 'latest'"),
32
32
  });
33
33
  // Tool description and schema for setupBrowserStackAppAutomateTests
34
- export const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead.";
34
+ export const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead. Set accessibility: true to also run App Accessibility (WCAG) checks on the screens the tests interact with; the issues are then read with fetchAccessibilityIssues.";
35
35
  export const SETUP_APP_AUTOMATE_SCHEMA = {
36
36
  detectedFramework: z
37
37
  .nativeEnum(AppSDKSupportedFrameworkEnum)
@@ -55,4 +55,9 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
55
55
  .optional()
56
56
  .default("BStack-AppAutomate-Suite")
57
57
  .describe("Project name for organizing test runs on BrowserStack."),
58
+ accessibility: z
59
+ .boolean()
60
+ .optional()
61
+ .default(false)
62
+ .describe("Run App Accessibility (WCAG) checks on the screens the tests touch; Android 11+ or iOS 14+."),
58
63
  };
@@ -4,6 +4,7 @@ import { validateAppAutomateDevices } from "../../sdk-utils/common/device-valida
4
4
  import { CREDENTIALS_SUBSTITUTION_NOTE } from "../../sdk-utils/common/credentials.js";
5
5
  import { getAppUploadInstruction, validateSupportforAppAutomate, } from "./utils.js";
6
6
  import { getAppSDKPrefixCommand, generateAppBrowserStackYMLInstructions, } from "./index.js";
7
+ import { getAppAccessibilityChecksInstructions } from "./languages/accessibility.js";
7
8
  import { createStep, formatAppInstructionsWithNumbers, getAppInstructionsForProjectConfiguration, SETUP_APP_AUTOMATE_SCHEMA, } from "./index.js";
8
9
  export async function setupAppAutomateHandler(rawInput, config) {
9
10
  const input = z.object(SETUP_APP_AUTOMATE_SCHEMA).parse(rawInput);
@@ -25,6 +26,7 @@ export async function setupAppAutomateHandler(rawInput, config) {
25
26
  ]) ?? [];
26
27
  const appPath = input.appPath;
27
28
  const framework = input.detectedFramework;
29
+ const accessibility = input.accessibility === true;
28
30
  //Validating if supported framework or not
29
31
  validateSupportforAppAutomate(framework, language, testingFramework);
30
32
  // Use default mobile devices when array is empty
@@ -36,7 +38,7 @@ export async function setupAppAutomateHandler(rawInput, config) {
36
38
  // Extract platforms for backward compatibility (if needed)
37
39
  const platforms = validatedEnvironments.map((env) => env.platform);
38
40
  // Step 1: Generate SDK setup command
39
- const sdkCommand = getAppSDKPrefixCommand(language, testingFramework, appPath);
41
+ const sdkCommand = getAppSDKPrefixCommand(language, testingFramework, appPath, { accessibility });
40
42
  if (sdkCommand) {
41
43
  instructions.push({ content: sdkCommand, type: "setup" });
42
44
  }
@@ -46,6 +48,7 @@ export async function setupAppAutomateHandler(rawInput, config) {
46
48
  platforms,
47
49
  testingFramework,
48
50
  projectName: input.project,
51
+ accessibility,
49
52
  }, appPath);
50
53
  if (configInstructions) {
51
54
  instructions.push({ content: configInstructions, type: "config" });
@@ -60,6 +63,13 @@ export async function setupAppAutomateHandler(rawInput, config) {
60
63
  if (projectInstructions) {
61
64
  instructions.push({ content: projectInstructions, type: "run" });
62
65
  }
66
+ // Step 5: What makes an accessibility scan happen, and how to read the issues
67
+ if (accessibility) {
68
+ instructions.push({
69
+ content: getAppAccessibilityChecksInstructions(language, testingFramework),
70
+ type: "run",
71
+ });
72
+ }
63
73
  const combinedInstructions = instructions
64
74
  .map((instruction) => instruction.content)
65
75
  .join("\n\n");
@@ -1,3 +1,5 @@
1
1
  import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework } from "./index.js";
2
2
  export declare function getAppInstructionsForProjectConfiguration(framework: string, testingFramework: AppSDKSupportedTestingFramework, language: AppSDKSupportedLanguage): string;
3
- export declare function getAppSDKPrefixCommand(language: AppSDKSupportedLanguage, testingFramework: string, appPath?: string): string;
3
+ export declare function getAppSDKPrefixCommand(language: AppSDKSupportedLanguage, testingFramework: string, appPath?: string, options?: {
4
+ accessibility?: boolean;
5
+ }): string;
@@ -29,14 +29,14 @@ export function getAppInstructionsForProjectConfiguration(framework, testingFram
29
29
  return "";
30
30
  }
31
31
  }
32
- export function getAppSDKPrefixCommand(language, testingFramework, appPath) {
32
+ export function getAppSDKPrefixCommand(language, testingFramework, appPath, options = {}) {
33
33
  switch (language) {
34
34
  case "csharp":
35
35
  return getCSharpSDKCommand();
36
36
  case "java":
37
37
  return getJavaSDKCommand(testingFramework, appPath);
38
38
  case "nodejs":
39
- return getNodejsSDKCommand(testingFramework);
39
+ return getNodejsSDKCommand(testingFramework, options);
40
40
  case "python":
41
41
  return getPythonSDKCommand(testingFramework);
42
42
  case "ruby":
@@ -0,0 +1,2 @@
1
+ import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework } from "../index.js";
2
+ export declare function getAppAccessibilityChecksInstructions(language: AppSDKSupportedLanguage, testingFramework: AppSDKSupportedTestingFramework): string;
@@ -0,0 +1,32 @@
1
+ // What an App Automate run needs for App Accessibility checks to happen, and how the
2
+ // issues are read afterwards. Each point here cost a run that finished green with no
3
+ // scans and no warning; the SDK says nothing when it decides not to scan.
4
+ import { AppSDKSupportedTestingFrameworkEnum, createStep, } from "../index.js";
5
+ const PYTEST_FIXTURE = `For pytest, create the driver in a fixture (\`conftest.py\`):
6
+ \`\`\`python
7
+ import pytest
8
+ from appium import webdriver
9
+ from appium.options.android import UiAutomator2Options # XCUITestOptions for iOS
10
+
11
+
12
+ @pytest.fixture
13
+ def driver():
14
+ options = UiAutomator2Options()
15
+ drv = webdriver.Remote("https://hub.browserstack.com/wd/hub", options=options)
16
+ yield drv
17
+ drv.quit()
18
+ \`\`\``;
19
+ function driverAdvice(language, testingFramework) {
20
+ if (language === "python" &&
21
+ testingFramework === AppSDKSupportedTestingFrameworkEnum.pytest) {
22
+ return PYTEST_FIXTURE;
23
+ }
24
+ return "Create the driver in your framework's setup hook (a fixture, `@BeforeMethod`, `@BeforeEach`, `SetUp`, `before`), not inside the test body.";
25
+ }
26
+ export function getAppAccessibilityChecksInstructions(language, testingFramework) {
27
+ return createStep("App Accessibility checks: what makes a scan happen, and how to read the issues", `- The SDK decides per test, at the test's start, whether to scan, from the drivers that exist at that moment. A driver created inside the test body is never scanned. ${driverAdvice(language, testingFramework)}
28
+ - Scans run on interaction commands: click/tap, send keys, actions and navigation (such as back). Reads like \`find_elements\` or \`page_source\` do not scan. Interact with every screen state you want scanned.
29
+ - Android 11 or newer, or iOS 14 or newer, is required; older devices are skipped without an error.
30
+ - Under \`platforms\` write the OS version as \`osVersion\`, unquoted (\`osVersion: 14.0\`; not \`platformVersion\`, not \`"14.0"\`): with any other spelling the SDK still runs the tests but App Accessibility records no scans, without a message.
31
+ - When the run ends, the SDK prints the build id as \`testhub id: <id>\` (also the id in its automation.browserstack.com/builds/<id> link). Read the issues with \`fetchAccessibilityIssues\`, \`buildId: <id>\`; it waits while the report is still processing and links the report on the App Accessibility dashboard (https://app-accessibility.browserstack.com/automated-tests). If it reports no scans, it lists the likely causes.`);
32
+ }
@@ -1,3 +1,5 @@
1
1
  import { AppSDKSupportedTestingFramework } from "../index.js";
2
- export declare function getNodejsSDKCommand(testingFramework: string): string;
2
+ export declare function getNodejsSDKCommand(testingFramework: string, options?: {
3
+ accessibility?: boolean;
4
+ }): string;
3
5
  export declare function getNodejsAppInstructions(testingFramework: AppSDKSupportedTestingFramework): string;
@@ -1,9 +1,9 @@
1
1
  // Node.js instructions and commands for App SDK utilities
2
2
  import { AppSDKSupportedTestingFrameworkEnum, createStep, combineInstructions, } from "../index.js";
3
- export function getNodejsSDKCommand(testingFramework) {
3
+ export function getNodejsSDKCommand(testingFramework, options = {}) {
4
4
  switch (testingFramework) {
5
5
  case "webdriverio":
6
- return getWebDriverIOCommand();
6
+ return getWebDriverIOCommand(options);
7
7
  case "nightwatch":
8
8
  return getNightwatchCommand();
9
9
  case "jest":
@@ -39,7 +39,14 @@ export function getNodejsAppInstructions(testingFramework) {
39
39
  return "";
40
40
  }
41
41
  }
42
- function getWebDriverIOCommand() {
42
+ function getWebDriverIOCommand({ accessibility = false, } = {}) {
43
+ const accessibilityConfig = accessibility
44
+ ? `accessibility: true,
45
+ accessibilityOptions: {
46
+ wcagVersion: 'wcag22aa',
47
+ includeIssueType: { bestPractice: true },
48
+ },`
49
+ : "accessibility: false,";
43
50
  const prerequisiteStep = createStep("Prerequisite Setup:", `a. Ensure you do not modify or replace any existing local driver code,
44
51
  as it will be automatically managed and overwritten by the BrowserStack SDK/Driver.
45
52
  b. Do not create any YML file in this integration as it is not required.
@@ -48,7 +55,13 @@ function getWebDriverIOCommand() {
48
55
  export BROWSERSTACK_USERNAME="<your_browserstack_username>"
49
56
  export BROWSERSTACK_ACCESS_KEY="<your_browserstack_access_key>"
50
57
  \`\`\``);
51
- const installStep = createStep("Install BrowserStack WDIO service:", `\`\`\`bash
58
+ // App Accessibility needs @wdio/browserstack-service 8.42.0+ on WDIO v8, or 9.6.0+ on v9.
59
+ const installStep = createStep("Install BrowserStack WDIO service:", accessibility
60
+ ? `\`\`\`bash
61
+ npm install @wdio/browserstack-service@^9.6.0 --save-dev
62
+ \`\`\`
63
+ App Accessibility needs WebdriverIO v8 or v9: on WebdriverIO v8, install @wdio/browserstack-service@^8.42.0 instead.`
64
+ : `\`\`\`bash
52
65
  npm install @wdio/browserstack-service@^7 --save-dev
53
66
  \`\`\``);
54
67
  const configStep = createStep("Update your WebdriverIO config file (e.g., \\`wdio.conf.js\\`) to add the BrowserStack service and capabilities:", `\`\`\`js
@@ -62,7 +75,7 @@ exports.config = {
62
75
  {
63
76
  app: 'bs://sample.app',
64
77
  browserstackLocal: true,
65
- accessibility: false,
78
+ ${accessibilityConfig}
66
79
  testObservabilityOptions: {
67
80
  buildName: "bstack-demo",
68
81
  projectName: "BrowserStack Sample",
@@ -74,7 +87,7 @@ exports.config = {
74
87
  capabilities: [{
75
88
  'bstack:options': {
76
89
  deviceName: 'Samsung Galaxy S22 Ultra',
77
- platformVersion: '12.0',
90
+ ${accessibility ? "osVersion" : "platformVersion"}: '12.0',
78
91
  platformName: 'android',
79
92
  }
80
93
  }],
@@ -38,6 +38,11 @@ export declare function uploadEspressoApp(appPath: string, config: BrowserStackC
38
38
  export declare function uploadEspressoTestSuite(testSuitePath: string, config: BrowserStackConfig): Promise<string>;
39
39
  export declare function uploadXcuiApp(appPath: string, config: BrowserStackConfig): Promise<string>;
40
40
  export declare function uploadXcuiTestSuite(testSuitePath: string, config: BrowserStackConfig): Promise<string>;
41
- export declare function triggerEspressoBuild(app_url: string, test_suite_url: string, devices: string[], project: string, config: BrowserStackConfig): Promise<string>;
42
- export declare function triggerXcuiBuild(app_url: string, test_suite_url: string, devices: string[], project: string, config: BrowserStackConfig): Promise<string>;
41
+ /** What a native build run can be asked for beyond app, suite, devices and project. */
42
+ export interface NativeBuildOptions {
43
+ /** Run App Accessibility checks during the tests (the build API's `accessibility` flag). */
44
+ accessibility?: boolean;
45
+ }
46
+ export declare function triggerEspressoBuild(app_url: string, test_suite_url: string, devices: string[], project: string, config: BrowserStackConfig, options?: NativeBuildOptions): Promise<string>;
47
+ export declare function triggerXcuiBuild(app_url: string, test_suite_url: string, devices: string[], project: string, config: BrowserStackConfig, options?: NativeBuildOptions): Promise<string>;
43
48
  export {};
@@ -138,8 +138,17 @@ export async function uploadXcuiApp(appPath, config) {
138
138
  export async function uploadXcuiTestSuite(testSuitePath, config) {
139
139
  return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/test-suite", "test_suite_url", config, { allowedExtensions: [".zip"] });
140
140
  }
141
+ function buildRequestBody(app_url, test_suite_url, devices, project, options) {
142
+ return {
143
+ app: app_url,
144
+ testSuite: test_suite_url,
145
+ devices,
146
+ project,
147
+ ...(options.accessibility ? { accessibility: true } : {}),
148
+ };
149
+ }
141
150
  // Triggers an Espresso test run on BrowserStack and returns the build_id
142
- export async function triggerEspressoBuild(app_url, test_suite_url, devices, project, config) {
151
+ export async function triggerEspressoBuild(app_url, test_suite_url, devices, project, config, options = {}) {
143
152
  const authHeader = "Basic " + Buffer.from(getBrowserStackAuth(config)).toString("base64");
144
153
  const response = await apiClient.post({
145
154
  url: "https://api-cloud.browserstack.com/app-automate/espresso/v2/build",
@@ -148,10 +157,9 @@ export async function triggerEspressoBuild(app_url, test_suite_url, devices, pro
148
157
  "Content-Type": "application/json",
149
158
  },
150
159
  body: {
151
- app: app_url,
152
- testSuite: test_suite_url,
153
- devices,
154
- project,
160
+ ...buildRequestBody(app_url, test_suite_url, devices, project, options),
161
+ // An Espresso build's App Accessibility report is generated through Test Observability.
162
+ ...(options.accessibility ? { testObservability: true } : {}),
155
163
  },
156
164
  });
157
165
  if (response.data.build_id) {
@@ -160,7 +168,7 @@ export async function triggerEspressoBuild(app_url, test_suite_url, devices, pro
160
168
  throw new Error(`Failed to trigger Espresso build: ${JSON.stringify(response.data)}`);
161
169
  }
162
170
  // Triggers an XCUITest run on BrowserStack and returns the build_id
163
- export async function triggerXcuiBuild(app_url, test_suite_url, devices, project, config) {
171
+ export async function triggerXcuiBuild(app_url, test_suite_url, devices, project, config, options = {}) {
164
172
  const auth = {
165
173
  username: config["browserstack-username"],
166
174
  password: config["browserstack-access-key"],
@@ -172,12 +180,7 @@ export async function triggerXcuiBuild(app_url, test_suite_url, devices, project
172
180
  Buffer.from(`${auth.username}:${auth.password}`).toString("base64"),
173
181
  "Content-Type": "application/json",
174
182
  },
175
- body: {
176
- app: app_url,
177
- testSuite: test_suite_url,
178
- devices,
179
- project,
180
- },
183
+ body: buildRequestBody(app_url, test_suite_url, devices, project, options),
181
184
  });
182
185
  if (response.data.build_id) {
183
186
  return response.data.build_id;