@browserstack/mcp-server 2.2.5-beta.1 → 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.
- package/dist/lib/accessibility-routing.d.ts +5 -0
- package/dist/lib/accessibility-routing.js +5 -0
- package/dist/server-factory.js +6 -1
- package/dist/tools/accessibility.js +588 -67
- package/dist/tools/accessiblity-utils/app-report-fetcher.d.ts +341 -0
- package/dist/tools/accessiblity-utils/app-report-fetcher.js +731 -0
- package/dist/tools/accessiblity-utils/paginate.d.ts +21 -0
- package/dist/tools/accessiblity-utils/paginate.js +44 -0
- package/dist/tools/accessiblity-utils/report-fetcher.d.ts +116 -7
- package/dist/tools/accessiblity-utils/report-fetcher.js +273 -30
- package/dist/tools/accessiblity-utils/report-parser.d.ts +41 -1
- package/dist/tools/accessiblity-utils/report-parser.js +142 -43
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +2 -0
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +15 -5
- package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +2 -1
- package/dist/tools/appautomate-utils/appium-sdk/constants.js +6 -1
- package/dist/tools/appautomate-utils/appium-sdk/handler.js +11 -1
- package/dist/tools/appautomate-utils/appium-sdk/instructions.d.ts +3 -1
- package/dist/tools/appautomate-utils/appium-sdk/instructions.js +2 -2
- package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.d.ts +2 -0
- package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.js +32 -0
- package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.d.ts +3 -1
- package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.js +19 -6
- package/dist/tools/appautomate-utils/native-execution/appautomate.d.ts +7 -2
- package/dist/tools/appautomate-utils/native-execution/appautomate.js +15 -12
- package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
- package/dist/tools/appautomate-utils/native-execution/constants.js +6 -1
- package/dist/tools/appautomate.js +29 -16
- package/dist/tools/failurelogs-utils/app-automate.js +1 -1
- package/dist/tools/testmanagement-utils/create-testrun.d.ts +1 -1
- package/dist/tools/testmanagement-utils/update-testrun.d.ts +1 -1
- package/dist/tools/tool-handoff.d.ts +5 -4
- package/dist/tools/tool-handoff.js +5 -10
- package/package.json +1 -1
- package/dist/tools/accessiblity-utils/accessibility-rag.d.ts +0 -12
- 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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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)
|
|
@@ -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:
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
export
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|