@browserstack/mcp-server 2.2.6-beta.1 → 2.2.6-beta.2
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 +6 -0
- package/dist/lib/accessibility-routing.js +6 -0
- package/dist/server-factory.js +6 -1
- package/dist/tools/accessibility.js +680 -69
- package/dist/tools/accessiblity-utils/app-report-fetcher.d.ts +412 -0
- package/dist/tools/accessiblity-utils/app-report-fetcher.js +926 -0
- package/dist/tools/accessiblity-utils/paginate.d.ts +27 -0
- package/dist/tools/accessiblity-utils/paginate.js +50 -0
- package/dist/tools/accessiblity-utils/report-fetcher.d.ts +116 -7
- package/dist/tools/accessiblity-utils/report-fetcher.js +268 -29
- package/dist/tools/accessiblity-utils/report-parser.d.ts +41 -1
- package/dist/tools/accessiblity-utils/report-parser.js +142 -43
- package/dist/tools/accessiblity-utils/scanner.d.ts +15 -1
- package/dist/tools/accessiblity-utils/scanner.js +3 -4
- 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 +12 -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 +4 -0
- package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.js +47 -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/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 +3 -2
- package/dist/tools/tool-handoff.js +3 -2
- 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 { PAGE_CHARACTERS, 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 = PAGE_CHARACTERS, 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)
|
|
@@ -13,13 +13,27 @@ export interface AccessibilityScanStatus {
|
|
|
13
13
|
};
|
|
14
14
|
errors?: string[];
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* What a Website Scanner scan checks for, as the create-scan API takes it under
|
|
18
|
+
* scanSettings. A setting left out takes the API's default.
|
|
19
|
+
*/
|
|
20
|
+
export interface ScanSettings {
|
|
21
|
+
/** The WCAG version and level, such as "wcag22aa", or "rgaa41" (API default "wcag21aa"). */
|
|
22
|
+
wcagVersion?: string;
|
|
23
|
+
/** Report best-practice issues too (API default false). */
|
|
24
|
+
bestPractices?: boolean;
|
|
25
|
+
/** Report issues that need a manual review (API default true). */
|
|
26
|
+
needsReview?: boolean;
|
|
27
|
+
/** Run the advanced rules (API default true). */
|
|
28
|
+
advancedRules?: boolean;
|
|
29
|
+
}
|
|
16
30
|
export declare class AccessibilityScanner {
|
|
17
31
|
private auth;
|
|
18
32
|
setAuth(auth: {
|
|
19
33
|
username: string;
|
|
20
34
|
password: string;
|
|
21
35
|
}): void;
|
|
22
|
-
startScan(name: string, urlList: string[], authConfigId?: number,
|
|
36
|
+
startScan(name: string, urlList: string[], authConfigId?: number, settings?: ScanSettings): Promise<AccessibilityScanResponse>;
|
|
23
37
|
pollStatus(scanId: string, scanRunId: string): Promise<AccessibilityScanStatus>;
|
|
24
38
|
waitUntilComplete(scanId: string, scanRunId: string, context: any): Promise<string>;
|
|
25
39
|
}
|
|
@@ -8,7 +8,7 @@ export class AccessibilityScanner {
|
|
|
8
8
|
setAuth(auth) {
|
|
9
9
|
this.auth = auth;
|
|
10
10
|
}
|
|
11
|
-
async startScan(name, urlList, authConfigId,
|
|
11
|
+
async startScan(name, urlList, authConfigId, settings = {}) {
|
|
12
12
|
if (!this.auth?.username || !this.auth?.password) {
|
|
13
13
|
throw new Error("BrowserStack credentials are not set for AccessibilityScanner.");
|
|
14
14
|
}
|
|
@@ -43,14 +43,13 @@ export class AccessibilityScanner {
|
|
|
43
43
|
return url;
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
|
+
const scanSettings = Object.fromEntries(Object.entries(settings).filter(([, value]) => value !== undefined));
|
|
46
47
|
const baseRequestBody = {
|
|
47
48
|
name,
|
|
48
49
|
urlList: transformedUrlList,
|
|
49
50
|
recurring: false,
|
|
50
51
|
...(authConfigId && { authConfigId }),
|
|
51
|
-
...(
|
|
52
|
-
scanSettings: { advancedRules },
|
|
53
|
-
}),
|
|
52
|
+
...(Object.keys(scanSettings).length > 0 && { scanSettings }),
|
|
54
53
|
};
|
|
55
54
|
let requestBody = baseRequestBody;
|
|
56
55
|
if (hasLocal) {
|
|
@@ -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, getAppAccessibilityNotSetUpInstructions, } 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,14 @@ 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: getAppAccessibilityNotSetUpInstructions(testingFramework) ??
|
|
70
|
+
getAppAccessibilityChecksInstructions(language, testingFramework),
|
|
71
|
+
type: "run",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
63
74
|
const combinedInstructions = instructions
|
|
64
75
|
.map((instruction) => instruction.content)
|
|
65
76
|
.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,4 @@
|
|
|
1
|
+
import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework } from "../index.js";
|
|
2
|
+
/** Says the checks were asked for but not set up, for a framework this setup leaves them off. */
|
|
3
|
+
export declare function getAppAccessibilityNotSetUpInstructions(testingFramework: AppSDKSupportedTestingFramework): string | undefined;
|
|
4
|
+
export declare function getAppAccessibilityChecksInstructions(language: AppSDKSupportedLanguage, testingFramework: AppSDKSupportedTestingFramework): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* The frameworks set up without a browserstack.yml whose setup here does not turn App
|
|
21
|
+
* Accessibility on; guidance for a config that was never written would only mislead.
|
|
22
|
+
*/
|
|
23
|
+
const NOT_SET_UP = {
|
|
24
|
+
[AppSDKSupportedTestingFrameworkEnum.nightwatch]: "Nightwatch",
|
|
25
|
+
[AppSDKSupportedTestingFrameworkEnum.cucumberRuby]: "Cucumber (Ruby)",
|
|
26
|
+
};
|
|
27
|
+
/** Says the checks were asked for but not set up, for a framework this setup leaves them off. */
|
|
28
|
+
export function getAppAccessibilityNotSetUpInstructions(testingFramework) {
|
|
29
|
+
const label = NOT_SET_UP[testingFramework];
|
|
30
|
+
if (!label)
|
|
31
|
+
return undefined;
|
|
32
|
+
return createStep("App Accessibility checks", `App Accessibility checks are not set up for ${label}: this setup does not turn them on for it, so these tests run without them. To check the app, set the tests up in another framework this tool supports, such as WebdriverIO, pytest, TestNG or JUnit, with accessibility: true.`);
|
|
33
|
+
}
|
|
34
|
+
function driverAdvice(language, testingFramework) {
|
|
35
|
+
if (language === "python" &&
|
|
36
|
+
testingFramework === AppSDKSupportedTestingFrameworkEnum.pytest) {
|
|
37
|
+
return PYTEST_FIXTURE;
|
|
38
|
+
}
|
|
39
|
+
return "Create the driver in your framework's setup hook (a fixture, `@BeforeMethod`, `@BeforeEach`, `SetUp`, `before`), not inside the test body.";
|
|
40
|
+
}
|
|
41
|
+
export function getAppAccessibilityChecksInstructions(language, testingFramework) {
|
|
42
|
+
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)}
|
|
43
|
+
- 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.
|
|
44
|
+
- Android 11 or newer, or iOS 14 or newer, is required; older devices are skipped without an error.
|
|
45
|
+
- 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.
|
|
46
|
+
- 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.`);
|
|
47
|
+
}
|
|
@@ -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
|
}],
|