@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.
- 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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AccessibilityScanner } from "./accessiblity-utils/scanner.js";
|
|
3
|
-
import { AccessibilityReportFetcher, } from "./accessiblity-utils/report-fetcher.js";
|
|
3
|
+
import { AccessibilityReportFetcher, EXPORT_WAIT_MS, ExportPendingError, } from "./accessiblity-utils/report-fetcher.js";
|
|
4
|
+
import { compareAppIssues, } from "./accessiblity-utils/app-report-fetcher.js";
|
|
4
5
|
import { AccessibilityAuthConfig, safeAuthConfigData, } from "./accessiblity-utils/auth-config.js";
|
|
5
6
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
|
-
import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js";
|
|
7
|
+
import { compareIssues, loadMainIssues, parseAccessibilityReportFromCSV, } from "./accessiblity-utils/report-parser.js";
|
|
7
8
|
import { wrapUntrusted } from "../lib/untrusted-content.js";
|
|
8
|
-
import { queryAccessibilityRAG } from "./accessiblity-utils/accessibility-rag.js";
|
|
9
9
|
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
10
10
|
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
11
11
|
import logger from "../logger.js";
|
|
@@ -66,25 +66,17 @@ async function initializeReportFetcher(config) {
|
|
|
66
66
|
reportFetcher.setAuth(auth);
|
|
67
67
|
return reportFetcher;
|
|
68
68
|
}
|
|
69
|
-
async function executeAccessibilityRAG(args, server, config) {
|
|
70
|
-
try {
|
|
71
|
-
trackMCP("accessibilityExpert", server.server.getClientVersion(), undefined, config);
|
|
72
|
-
return await queryAccessibilityRAG(args.query, config);
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
return handleMCPError("accessibilityExpert", server, config, error);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
69
|
/** The one source the arguments name, or why they do not name exactly one. */
|
|
79
70
|
function issueSourceFrom(args) {
|
|
80
71
|
const named = [
|
|
81
72
|
Boolean(args.scanId || args.scanRunId),
|
|
82
73
|
Boolean(args.buildId),
|
|
83
74
|
Boolean(args.workflowReportId),
|
|
75
|
+
Boolean(args.appReportId),
|
|
84
76
|
Boolean(args.reportUrl),
|
|
85
77
|
].filter(Boolean).length;
|
|
86
78
|
if (named !== 1) {
|
|
87
|
-
return "Pass exactly one source: scanId with scanRunId (a Website Scanner run), buildId (an Automated Tests build), workflowReportId (a Workflow Analyzer report) or reportUrl (a report's dashboard link).";
|
|
79
|
+
return "Pass exactly one source: scanId with scanRunId (a Website Scanner run), buildId (an Automated Tests or App Accessibility build), workflowReportId (a Workflow Analyzer report), appReportId (an App Accessibility manual report) or reportUrl (a report's dashboard link).";
|
|
88
80
|
}
|
|
89
81
|
if (args.reportUrl)
|
|
90
82
|
return { kind: "link", reportUrl: args.reportUrl };
|
|
@@ -93,6 +85,9 @@ function issueSourceFrom(args) {
|
|
|
93
85
|
if (args.workflowReportId) {
|
|
94
86
|
return { kind: "workflow", reportId: args.workflowReportId };
|
|
95
87
|
}
|
|
88
|
+
if (args.appReportId) {
|
|
89
|
+
return { kind: "appReport", reportId: args.appReportId };
|
|
90
|
+
}
|
|
96
91
|
if (!args.scanId || !args.scanRunId) {
|
|
97
92
|
return "A Website Scanner run needs both scanId and scanRunId.";
|
|
98
93
|
}
|
|
@@ -104,9 +99,12 @@ function sourceArgs(source) {
|
|
|
104
99
|
case "scan":
|
|
105
100
|
return `scanId: "${source.scanId}", scanRunId: "${source.scanRunId}"`;
|
|
106
101
|
case "build":
|
|
102
|
+
case "appBuild":
|
|
107
103
|
return `buildId: "${source.buildId}"`;
|
|
108
104
|
case "workflow":
|
|
109
105
|
return `workflowReportId: "${source.reportId}"`;
|
|
106
|
+
case "appReport":
|
|
107
|
+
return `appReportId: "${source.reportId}"`;
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
110
|
/** What a dashboard link resolved to, in words. */
|
|
@@ -118,6 +116,10 @@ function describeSource(source) {
|
|
|
118
116
|
return `Automated Tests build ${source.buildId}`;
|
|
119
117
|
case "workflow":
|
|
120
118
|
return `Workflow Analyzer report ${source.reportId}`;
|
|
119
|
+
case "appBuild":
|
|
120
|
+
return `App Accessibility build ${source.buildId}`;
|
|
121
|
+
case "appReport":
|
|
122
|
+
return `App Accessibility manual report ${source.reportId}`;
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
async function executeFetchAccessibilityIssues(args, context, server, config) {
|
|
@@ -126,7 +128,11 @@ async function executeFetchAccessibilityIssues(args, context, server, config) {
|
|
|
126
128
|
const source = issueSourceFrom(args);
|
|
127
129
|
if (typeof source === "string")
|
|
128
130
|
return createErrorResponse(source);
|
|
129
|
-
return await fetchAccessibilityIssues(source, config, args.cursor, context
|
|
131
|
+
return await fetchAccessibilityIssues(source, config, args.cursor, context, {
|
|
132
|
+
compareWith: args.compareWith,
|
|
133
|
+
pageUrl: args.pageUrl,
|
|
134
|
+
exportTaskId: args.exportTaskId,
|
|
135
|
+
});
|
|
130
136
|
}
|
|
131
137
|
catch (error) {
|
|
132
138
|
return handleMCPError("fetchAccessibilityIssues", server, config, error);
|
|
@@ -155,19 +161,27 @@ function excludedIssuesNote(duplicates, unlinked, hidden) {
|
|
|
155
161
|
/** How often, and for how long, a first read waits on a report that is still processing. */
|
|
156
162
|
export const REPORT_STATUS_POLL_MS = 10_000;
|
|
157
163
|
export const REPORT_STATUS_WAIT_MS = 5 * 60 * 1000;
|
|
158
|
-
/** Statuses during which a report's issues are still being added. */
|
|
164
|
+
/** Statuses during which a report's issues are still being added (`rule_processing`: an app build's). */
|
|
159
165
|
const PROCESSING_STATUSES = new Set([
|
|
160
166
|
"queued",
|
|
161
167
|
"running",
|
|
162
168
|
"processing",
|
|
163
169
|
"finalizing",
|
|
170
|
+
"rule_processing",
|
|
171
|
+
"in_progress",
|
|
164
172
|
]);
|
|
173
|
+
/**
|
|
174
|
+
* Of those, the ones worth waiting on: the processing after the run (a web scan's advanced
|
|
175
|
+
* issues, an app build's rules), which ends within minutes. A report whose scan or tests are still running is read as it is,
|
|
176
|
+
* with a note, since that can last as long as the suite does.
|
|
177
|
+
*/
|
|
178
|
+
const WAIT_STATUSES = new Set(["finalizing", "rule_processing"]);
|
|
165
179
|
/**
|
|
166
180
|
* Where a report is in its processing before its issues are read. Issues keep being added
|
|
167
|
-
* until it completes
|
|
168
|
-
* minutes after the tests end
|
|
169
|
-
* REPORT_STATUS_WAIT_MS. A
|
|
170
|
-
* is
|
|
181
|
+
* until it completes; an advanced scan adds its advanced-rule issues while "finalizing", for
|
|
182
|
+
* minutes after the tests end, so a first read waits for that, reporting progress, for up to
|
|
183
|
+
* REPORT_STATUS_WAIT_MS. A run still in progress is not waited on (it can take as long as
|
|
184
|
+
* the suite), nor is a later page, nor a status that cannot be read.
|
|
171
185
|
*/
|
|
172
186
|
async function waitForProcessing(reportFetcher, source, wait, context) {
|
|
173
187
|
const started = Date.now();
|
|
@@ -175,7 +189,7 @@ async function waitForProcessing(reportFetcher, source, wait, context) {
|
|
|
175
189
|
let status = firstStatus;
|
|
176
190
|
while (wait &&
|
|
177
191
|
status &&
|
|
178
|
-
|
|
192
|
+
WAIT_STATUSES.has(status) &&
|
|
179
193
|
Date.now() - started < REPORT_STATUS_WAIT_MS) {
|
|
180
194
|
const waited = Date.now() - started;
|
|
181
195
|
if (context) {
|
|
@@ -209,41 +223,498 @@ function processingNote({ status, firstStatus, waitedMs, }) {
|
|
|
209
223
|
}
|
|
210
224
|
return [];
|
|
211
225
|
}
|
|
212
|
-
|
|
226
|
+
/** Progress while a report's CSV export is generated, so clients see the call is alive. */
|
|
227
|
+
function exportProgress(context) {
|
|
228
|
+
return async (waitedMs) => {
|
|
229
|
+
if (!context)
|
|
230
|
+
return;
|
|
231
|
+
await notifyScanProgress(context, `Generating the report's CSV export (large reports take longer): ${formatDuration(waitedMs)} so far.`, Math.min(99, Math.round((waitedMs / EXPORT_WAIT_MS) * 100)));
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** What to say of an export still being generated: how to continue it, not an error. */
|
|
235
|
+
function exportPendingMessages(source, error, extraArgs) {
|
|
236
|
+
return [
|
|
237
|
+
`This report's CSV export is still being generated (waited ${formatDuration(error.waitedMs)}; large reports can take a few minutes). No issues have been read yet.`,
|
|
238
|
+
`Call fetchAccessibilityIssues again with ${sourceArgs(source)}${extraArgs} and exportTaskId: "${error.taskId}" to continue this export instead of starting a new one.`,
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
/** The last line: how to get the next page, or that there is none. */
|
|
242
|
+
function closingMessage(next_page, remaining, source, stillProcessing, extraArgs = "") {
|
|
243
|
+
if (next_page !== null) {
|
|
244
|
+
return `${remaining} more issues available. Use fetchAccessibilityIssues with ${sourceArgs(source)}${extraArgs} and cursor: ${next_page} to get the next batch.`;
|
|
245
|
+
}
|
|
246
|
+
if (stillProcessing) {
|
|
247
|
+
return `All issues found so far retrieved; the report is still processing.`;
|
|
248
|
+
}
|
|
249
|
+
return `✅ All issues retrieved.`;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* An app build's name and dashboard link, as data: the name is the user's own (from their
|
|
253
|
+
* CI or browserstack.yml), so it is quoted in an untrusted block rather than in our prose.
|
|
254
|
+
*/
|
|
255
|
+
function appBuildDetails(report) {
|
|
256
|
+
return `Build: ${wrapUntrusted("App Accessibility build", JSON.stringify({
|
|
257
|
+
name: report.build_name,
|
|
258
|
+
...(report.dashboard_link
|
|
259
|
+
? { dashboard_link: report.dashboard_link }
|
|
260
|
+
: {}),
|
|
261
|
+
}, null, 2))}`;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Why a native (Espresso, XCUITest) build recorded no scans. The build request's flag only
|
|
265
|
+
* turns the checks on: the suite itself must carry BrowserStack's instrumentation.
|
|
266
|
+
*/
|
|
267
|
+
const NATIVE_NO_SCAN_CAUSES = {
|
|
268
|
+
espresso: [
|
|
269
|
+
"The test suite was not built with BrowserStack's Gradle plugin and SDK: add classpath 'com.browserstack:gradle-tool:latest.release' to the root build.gradle, id 'com.browserstack.gradle-tool' to the app's plugins, and androidTestImplementation 'com.browserstack:browserstack-java-sdk:latest.release' (1.31.0 or newer), then rebuild the test APK.",
|
|
270
|
+
"Test Observability was off for the build: Espresso's App Accessibility report needs `testObservability: true` in the build request alongside `accessibility: true` (runAppTestsOnBrowserStack sends both).",
|
|
271
|
+
"The app cannot reach BrowserStack from the device: it needs internet permission, and cleartext traffic allowed (usesCleartextTraffic) if the network calls for it.",
|
|
272
|
+
"No tests ran: check the build's sessions and test results on App Automate.",
|
|
273
|
+
],
|
|
274
|
+
xcuitest: [
|
|
275
|
+
"The UI test target does not embed and initialize the BrowserStack Swift SDK: add BrowserstackSwiftSDK.xcframework (or the Swift package) to the test target, and set NSPrincipalClass to BrowserstackSwiftSDK.BrowserstackSwiftSDK in its Info.plist or call BrowserstackSwiftSDK.init() in the test class's init().",
|
|
276
|
+
"The device runs iOS older than 14, or the suite was built with Swift older than 5.4.",
|
|
277
|
+
"No tests ran: check the build's sessions and test results on App Automate.",
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
/** Which native framework ran a build, when its details name one. */
|
|
281
|
+
function nativeFramework(framework) {
|
|
282
|
+
if (!framework)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (/espresso/i.test(framework))
|
|
285
|
+
return "espresso";
|
|
286
|
+
if (/xcui|xctest/i.test(framework))
|
|
287
|
+
return "xcuitest";
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Why a finished app build has no scans. Each cause here produced a run that finished
|
|
292
|
+
* green with nothing scanned and no warning; they are listed most likely first, with what
|
|
293
|
+
* the build's own details say ahead of the rest. A native build gets the native causes.
|
|
294
|
+
*/
|
|
295
|
+
function noScansNote(report) {
|
|
296
|
+
const native = nativeFramework(report.framework);
|
|
297
|
+
if (native) {
|
|
298
|
+
const causes = [
|
|
299
|
+
...(report.fup_capped
|
|
300
|
+
? [
|
|
301
|
+
"This build hit the fair-usage cap (fup in the build details), which drops tests or scans.",
|
|
302
|
+
]
|
|
303
|
+
: []),
|
|
304
|
+
...NATIVE_NO_SCAN_CAUSES[native],
|
|
305
|
+
];
|
|
306
|
+
return [
|
|
307
|
+
`No accessibility scans were recorded for the ${native === "espresso" ? "Espresso" : "XCUITest"} build below (status: ${report.status}), so there are no issues to list.`,
|
|
308
|
+
appBuildDetails(report),
|
|
309
|
+
"The usual causes for a native build, most likely first:",
|
|
310
|
+
...causes.map((cause, i) => `${i + 1}. ${cause}`),
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
const causes = [
|
|
314
|
+
...(report.fup_capped
|
|
315
|
+
? [
|
|
316
|
+
"This build hit the fair-usage cap (fup in the build details), which drops tests or scans.",
|
|
317
|
+
]
|
|
318
|
+
: []),
|
|
319
|
+
...(report.status === "timeout"
|
|
320
|
+
? [
|
|
321
|
+
'The session timed out during initialization (status "timeout"): check the script and the config.',
|
|
322
|
+
]
|
|
323
|
+
: []),
|
|
324
|
+
"`accessibility: true` is not set in browserstack.yml (or the capabilities); the SDK defaults it to false.",
|
|
325
|
+
'The platform entry in browserstack.yml does not use `osVersion`, unquoted: with `platformVersion` the SDK selects the device but App Accessibility records no scans, and a quoted "14.0" fails its number check and turns the checks off; neither says so. Write `osVersion: 14.0`.',
|
|
326
|
+
'The device runs an OS App Accessibility does not support: Android 11 or newer, or iOS 14 or newer, is needed (an older Android reads "update_os_version").',
|
|
327
|
+
"The driver was created inside the test body: the SDK decides per test, at its start, from the drivers that already exist. Create it in a fixture or setup hook.",
|
|
328
|
+
"The test only read the screen (find_elements, page_source): scans run on interaction commands such as click/tap, send keys, actions and navigation. Interact with each screen you want scanned.",
|
|
329
|
+
...(report.status === "timeout"
|
|
330
|
+
? []
|
|
331
|
+
: [
|
|
332
|
+
'The session timed out during initialization (status "timeout"): check the script and the config.',
|
|
333
|
+
]),
|
|
334
|
+
];
|
|
335
|
+
return [
|
|
336
|
+
`No accessibility scans were recorded for the App Accessibility build below (status: ${report.status}), so there are no issues to list.`,
|
|
337
|
+
appBuildDetails(report),
|
|
338
|
+
"The usual causes, most likely first:",
|
|
339
|
+
...causes.map((cause, i) => `${i + 1}. ${cause}`),
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
const SEVERITY_ORDER = ["critical", "serious", "moderate", "minor"];
|
|
343
|
+
/** How many screens the summary names; the rest are counted. */
|
|
344
|
+
const SUMMARY_SCREENS = 10;
|
|
345
|
+
/** An app report's issues at a glance: by severity, by rule, and by screen. */
|
|
346
|
+
function summaryNote(summary, total) {
|
|
347
|
+
if (total === 0)
|
|
348
|
+
return [];
|
|
349
|
+
const rank = (severity) => {
|
|
350
|
+
const i = SEVERITY_ORDER.indexOf(severity);
|
|
351
|
+
return i === -1 ? SEVERITY_ORDER.length : i;
|
|
352
|
+
};
|
|
353
|
+
const severities = Object.entries(summary.by_severity)
|
|
354
|
+
.sort(([a], [b]) => rank(a) - rank(b))
|
|
355
|
+
.map(([severity, n]) => `${n} ${severity}`)
|
|
356
|
+
.join(", ");
|
|
357
|
+
const rules = summary.by_rule
|
|
358
|
+
.map((r) => `${r.issue_type} ${r.count}`)
|
|
359
|
+
.join(", ");
|
|
360
|
+
const shown = summary.by_screen.slice(0, SUMMARY_SCREENS);
|
|
361
|
+
const more = summary.by_screen.length - shown.length;
|
|
362
|
+
const screens = `${shown.map((s) => `${s.screen} ${s.count}`).join(", ")}${more > 0 ? `, and ${more} more screens` : ""}`;
|
|
363
|
+
return [
|
|
364
|
+
`Summary of the ${total} issues: ${severities}.`,
|
|
365
|
+
wrapUntrusted("accessibility issue counts", `By rule: ${rules}.\nBy screen: ${screens}.`),
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
/** How many new and resolved issues a comparison lists per page. */
|
|
369
|
+
const COMPARE_LIST_MAX = 50;
|
|
370
|
+
/** An issue as a comparison lists it: what and where, without the fix text. */
|
|
371
|
+
function compareRow(row) {
|
|
372
|
+
const { issue_type, rule_id, severity, screen, component, element } = row;
|
|
373
|
+
return { issue_type, rule_id, severity, screen, component, element };
|
|
374
|
+
}
|
|
375
|
+
/** One page of a comparison's list: the cursor counts the issues already listed. */
|
|
376
|
+
function issueList(label, rows, cursor) {
|
|
377
|
+
if (cursor > 0 && cursor >= rows.length) {
|
|
378
|
+
return `${label} (${rows.length}): all listed on earlier pages.`;
|
|
379
|
+
}
|
|
380
|
+
const shown = rows.slice(cursor, cursor + COMPARE_LIST_MAX).map(compareRow);
|
|
381
|
+
const range = rows.length > COMPARE_LIST_MAX
|
|
382
|
+
? `, ${cursor + 1} to ${cursor + shown.length}`
|
|
383
|
+
: "";
|
|
384
|
+
return `${label} (${rows.length})${range}: ${wrapUntrusted("accessibility scan results", JSON.stringify(shown, null, 2))}`;
|
|
385
|
+
}
|
|
386
|
+
/** The build to compare with, or why there is none. */
|
|
387
|
+
async function baselineAppBuild(reportFetcher, buildId, compareWith) {
|
|
388
|
+
let baseline;
|
|
389
|
+
if (compareWith === "previous") {
|
|
390
|
+
const previous = await reportFetcher.previousAppBuild(buildId);
|
|
391
|
+
if (!previous) {
|
|
392
|
+
return "This is its project's first build, so there is nothing earlier to compare it with. Pass an earlier build's id or link as compareWith instead.";
|
|
393
|
+
}
|
|
394
|
+
baseline = {
|
|
395
|
+
buildId: previous.buildId,
|
|
396
|
+
sameName: previous.same_name,
|
|
397
|
+
searchedAll: previous.searched_all,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
const named = /^https?:\/\//i.test(compareWith)
|
|
402
|
+
? await reportFetcher.sourceFromReportUrl(compareWith)
|
|
403
|
+
: { kind: "build", buildId: compareWith };
|
|
404
|
+
const source = await reportFetcher.resolveBuild(named);
|
|
405
|
+
if (source.kind !== "appBuild") {
|
|
406
|
+
return `compareWith names ${describeSource(source)}, which is not an App Accessibility build. Pass an earlier app build's id or link, or "previous".`;
|
|
407
|
+
}
|
|
408
|
+
baseline = { buildId: source.buildId };
|
|
409
|
+
}
|
|
410
|
+
if (baseline.buildId === buildId) {
|
|
411
|
+
return `compareWith names this build itself. Pass an earlier build's id or link, or "previous".`;
|
|
412
|
+
}
|
|
413
|
+
return baseline;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* What changed between an earlier app build and this one: issues new in this build,
|
|
417
|
+
* issues the earlier one had that this one does not, and how many it kept. An issue on a
|
|
418
|
+
* screen this build did not reach also reads as resolved, so a build that scanned other
|
|
419
|
+
* screens or ran other tests is called out.
|
|
420
|
+
*/
|
|
421
|
+
async function compareAppBuilds(reportFetcher, source, compareWith, cursor, preface, processing) {
|
|
422
|
+
const baseline = await baselineAppBuild(reportFetcher, source.buildId, compareWith);
|
|
423
|
+
if (typeof baseline === "string")
|
|
424
|
+
return createErrorResponse(baseline);
|
|
425
|
+
const [named, current] = await Promise.all([
|
|
426
|
+
reportFetcher.appBuildIssues(baseline.buildId),
|
|
427
|
+
reportFetcher.appBuildIssues(source.buildId),
|
|
428
|
+
]);
|
|
429
|
+
// A comparison runs from the earlier build to the later; named the other way round, the
|
|
430
|
+
// new issues would read as resolved and the resolved as new.
|
|
431
|
+
const reversed = named.created_at !== undefined &&
|
|
432
|
+
current.created_at !== undefined &&
|
|
433
|
+
Date.parse(named.created_at) > Date.parse(current.created_at);
|
|
434
|
+
const [before, after] = reversed ? [current, named] : [named, current];
|
|
435
|
+
const { added, resolved, retained } = compareAppIssues(before.issues, after.issues);
|
|
436
|
+
const which = baseline.sameName === undefined
|
|
437
|
+
? ""
|
|
438
|
+
: baseline.sameName
|
|
439
|
+
? ", the build before it"
|
|
440
|
+
: baseline.searchedAll
|
|
441
|
+
? ", the build just before it in its project (no earlier build has this build's name)"
|
|
442
|
+
: ", the build just before it in its project (none of the newest builds before it has this build's name)";
|
|
443
|
+
const warnings = [];
|
|
444
|
+
if (before.scan_count !== after.scan_count) {
|
|
445
|
+
warnings.push(`⚠️ The builds scanned a different number of screens (${after.scan_count} in the later build, ${before.scan_count} in the earlier one): an issue on a screen the later build did not reach reads as resolved without having been fixed. Check the resolved issues' screens before calling them fixed.`);
|
|
446
|
+
}
|
|
447
|
+
const notRun = before.tests.filter((t) => !after.tests.includes(t));
|
|
448
|
+
if (notRun.length > 0) {
|
|
449
|
+
warnings.push(`⚠️ Some tests had screens scanned in the earlier build but not in the later one; their issues read as resolved without having been checked again. ${wrapUntrusted("test names", JSON.stringify(notRun))}`);
|
|
450
|
+
}
|
|
451
|
+
// Build names are the user's own (from their CI or browserstack.yml), so they are data.
|
|
452
|
+
const builds = wrapUntrusted("App Accessibility builds", JSON.stringify({
|
|
453
|
+
later: { build_number: after.build_number, name: after.build_name },
|
|
454
|
+
earlier: { build_number: before.build_number, name: before.build_name },
|
|
455
|
+
}, null, 2));
|
|
456
|
+
return createSuccessResponse([
|
|
457
|
+
...preface,
|
|
458
|
+
...processingNote(processing),
|
|
459
|
+
...(reversed
|
|
460
|
+
? [
|
|
461
|
+
"compareWith names a build newer than this one, so the comparison runs from this build (the earlier) to that one (the later).",
|
|
462
|
+
]
|
|
463
|
+
: []),
|
|
464
|
+
`Compared App Accessibility build #${after.build_number} with build #${before.build_number}${which}: ${added.length} new, ${resolved.length} resolved, ${retained} still open.`,
|
|
465
|
+
`Builds: ${builds}`,
|
|
466
|
+
`Issues are matched one for one by rule and element: its id, or else its class and its text or label. New and still-open issues add up to the later build's ${after.issues.length}; resolved and still-open ones to the earlier build's ${before.issues.length}.`,
|
|
467
|
+
...warnings,
|
|
468
|
+
issueList("New issues", added, cursor),
|
|
469
|
+
issueList("Resolved issues", resolved, cursor),
|
|
470
|
+
Math.max(added.length, resolved.length) > cursor + COMPARE_LIST_MAX
|
|
471
|
+
? `More to list: call fetchAccessibilityIssues with buildId: "${source.buildId}", compareWith: "${compareWith}" and cursor: ${cursor + COMPARE_LIST_MAX}.`
|
|
472
|
+
: `Read the later build's open issues with fetchAccessibilityIssues with buildId: "${after.buildId}".`,
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
/** An app build's issues come inline from its scanned screens; there is no CSV to link. */
|
|
476
|
+
async function appBuildMessages(reportFetcher, source, cursor, preface, processing, stillProcessing) {
|
|
477
|
+
const report = await reportFetcher.appBuildReport(source.buildId, cursor);
|
|
478
|
+
if (report.scan_count === 0 && !stillProcessing) {
|
|
479
|
+
return [...preface, ...processingNote(processing), ...noScansNote(report)];
|
|
480
|
+
}
|
|
481
|
+
const { records, page_length, total_issues, next_page } = report;
|
|
482
|
+
const screens = `${report.scan_count} screen${report.scan_count === 1 ? "" : "s"} scanned`;
|
|
483
|
+
return [
|
|
484
|
+
...preface,
|
|
485
|
+
...processingNote(processing),
|
|
486
|
+
`Retrieved ${page_length} accessibility issues (Total: ${total_issues}) from the App Accessibility build below (${screens})`,
|
|
487
|
+
appBuildDetails(report),
|
|
488
|
+
...(cursor === 0 ? summaryNote(report.summary, total_issues) : []),
|
|
489
|
+
...excludedIssuesNote(null, 0, report.hidden_issues),
|
|
490
|
+
`Issues: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
|
|
491
|
+
closingMessage(next_page, report.remaining, source, stillProcessing),
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* A manual report's issues come inline from its scanned screens. The API lists every
|
|
496
|
+
* occurrence and does not mark the duplicates the dashboard folds, so the dashboard's
|
|
497
|
+
* own count is stated next to the total rather than reproduced.
|
|
498
|
+
*/
|
|
499
|
+
async function appReportMessages(reportFetcher, source, cursor, preface, processing, stillProcessing) {
|
|
500
|
+
const report = await reportFetcher.appReportIssues(source.reportId, cursor);
|
|
501
|
+
// The report's name is whatever its author typed, so it is quoted as data.
|
|
502
|
+
const details = `Report: ${wrapUntrusted("App Accessibility manual report", JSON.stringify({ name: report.report_name, dashboard_link: report.dashboard_link }, null, 2))}`;
|
|
503
|
+
if (report.scan_count === 0 && !stillProcessing) {
|
|
504
|
+
return [
|
|
505
|
+
...preface,
|
|
506
|
+
...processingNote(processing),
|
|
507
|
+
`No screens were scanned in the App Accessibility manual report below (status: ${report.status}), so there are no issues to list.`,
|
|
508
|
+
details,
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
const { records, page_length, total_issues, next_page } = report;
|
|
512
|
+
const where = `${report.scan_count} screen${report.scan_count === 1 ? "" : "s"} scanned${report.device ? ` on ${report.device}` : ""}`;
|
|
513
|
+
const dashboardNote = report.duplicate_occurrences > 0
|
|
514
|
+
? [
|
|
515
|
+
`The dashboard counts ${report.dashboard_count} issues for this report: ${report.duplicate_occurrences} of the occurrences are duplicates it folds, which the API does not mark.`,
|
|
516
|
+
]
|
|
517
|
+
: [];
|
|
518
|
+
return [
|
|
519
|
+
...preface,
|
|
520
|
+
...processingNote(processing),
|
|
521
|
+
`Retrieved ${page_length} accessibility issue occurrences (Total: ${total_issues}) from the App Accessibility manual report below (${where})`,
|
|
522
|
+
details,
|
|
523
|
+
...(cursor === 0 ? summaryNote(report.summary, total_issues) : []),
|
|
524
|
+
...dashboardNote,
|
|
525
|
+
...excludedIssuesNote(null, 0, report.hidden_issues),
|
|
526
|
+
`Issues: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
|
|
527
|
+
closingMessage(next_page, report.remaining, source, stillProcessing),
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
async function fetchAccessibilityIssues(requested, config, cursor = 0, context, { compareWith, pageUrl, exportTaskId, } = {}) {
|
|
213
531
|
const reportFetcher = await initializeReportFetcher(config);
|
|
214
|
-
const
|
|
532
|
+
const named = requested.kind === "link"
|
|
215
533
|
? await reportFetcher.sourceFromReportUrl(requested.reportUrl)
|
|
216
534
|
: requested;
|
|
535
|
+
// A build id names a web build or a mobile app build; the APIs tell which.
|
|
536
|
+
const source = await reportFetcher.resolveBuild(named);
|
|
537
|
+
// Website Scanner runs and App Accessibility builds can be compared, each in its own way.
|
|
538
|
+
if (compareWith !== undefined && source.kind === "scan") {
|
|
539
|
+
return compareScanRuns(requested.kind === "link" ? requested : source, compareWith, config, context);
|
|
540
|
+
}
|
|
541
|
+
if (compareWith !== undefined && source.kind !== "appBuild") {
|
|
542
|
+
return createErrorResponse(`compareWith compares Website Scanner runs and App Accessibility builds; ${describeSource(source)} is neither.`);
|
|
543
|
+
}
|
|
217
544
|
const processing = await waitForProcessing(reportFetcher, source, cursor === 0, context);
|
|
218
545
|
const stillProcessing = processing.status !== undefined &&
|
|
219
546
|
PROCESSING_STATUSES.has(processing.status);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
547
|
+
const preface = requested.kind === "link"
|
|
548
|
+
? [`Resolved the report link to ${describeSource(source)}.`]
|
|
549
|
+
: [];
|
|
550
|
+
if (source.kind === "appBuild" && compareWith !== undefined) {
|
|
551
|
+
return compareAppBuilds(reportFetcher, source, compareWith, cursor, preface, processing);
|
|
552
|
+
}
|
|
553
|
+
if (source.kind === "appBuild") {
|
|
554
|
+
return createSuccessResponse(await appBuildMessages(reportFetcher, source, cursor, preface, processing, stillProcessing));
|
|
555
|
+
}
|
|
556
|
+
if (source.kind === "appReport") {
|
|
557
|
+
return createSuccessResponse(await appReportMessages(reportFetcher, source, cursor, preface, processing, stillProcessing));
|
|
558
|
+
}
|
|
559
|
+
const pageArg = pageUrl ? `, pageUrl: "${pageUrl}"` : "";
|
|
560
|
+
let reportLink;
|
|
561
|
+
try {
|
|
562
|
+
reportLink = await reportFetcher.getReportLink(source, {
|
|
563
|
+
exportTaskId,
|
|
564
|
+
onWait: exportProgress(context),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
if (error instanceof ExportPendingError) {
|
|
569
|
+
return createSuccessResponse(exportPendingMessages(source, error, pageArg));
|
|
570
|
+
}
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
const { records, page_length, total_issues, next_page, remaining, duplicate_issues, unlinked_duplicates, hidden_issues, page, } = await parseAccessibilityReportFromCSV(reportLink, {
|
|
574
|
+
nextPage: cursor,
|
|
575
|
+
pageUrl,
|
|
576
|
+
});
|
|
577
|
+
return createSuccessResponse([
|
|
578
|
+
...preface,
|
|
231
579
|
...processingNote(processing),
|
|
580
|
+
...(page ? pageNote(page) : []),
|
|
232
581
|
`Retrieved ${page_length} accessibility issues (Total: ${total_issues})`,
|
|
233
582
|
...excludedIssuesNote(duplicate_issues, unlinked_duplicates, hidden_issues),
|
|
234
583
|
`You can also download the full report (CSV) from the following link, valid for about 12 hours: ${reportLink}`,
|
|
235
584
|
`Issues: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
|
|
585
|
+
closingMessage(next_page, remaining, source, stillProcessing, pageArg),
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
/** What one page of a report holds, in the dashboard's page-view terms. */
|
|
589
|
+
function pageNote(page) {
|
|
590
|
+
if (page.occurrences === 0) {
|
|
591
|
+
const similar = page.similar_pages?.length
|
|
592
|
+
? ` Pages in this report with a similar address: ${wrapUntrusted("report page addresses", JSON.stringify(page.similar_pages))}`
|
|
593
|
+
: "";
|
|
594
|
+
return [`No issue in this report is on ${page.url}.${similar}`];
|
|
595
|
+
}
|
|
596
|
+
const severities = Object.entries(page.by_severity)
|
|
597
|
+
.sort(([, a], [, b]) => b - a)
|
|
598
|
+
.map(([severity, n]) => `${n} ${severity}`)
|
|
599
|
+
.join(", ");
|
|
600
|
+
return [
|
|
601
|
+
`${page.occurrences} issue occurrences on ${page.url} (${severities}), as the dashboard's page view counts them: ${page.distinct_issues} distinct issues, ${page.first_found_here} of them first found on this page. Each issue below is listed once, with occurrences_on_page; first_found_on names the page an issue was first found on when it is another one.`,
|
|
236
602
|
];
|
|
237
|
-
|
|
238
|
-
|
|
603
|
+
}
|
|
604
|
+
/** At most this many new and resolved issues are listed in a comparison. */
|
|
605
|
+
const COMPARE_LIST_LIMIT = 50;
|
|
606
|
+
/**
|
|
607
|
+
* A Website Scanner run against an earlier run of the same scan: the run before it
|
|
608
|
+
* ("previous") or a given scanRunId. Issues are matched by rule, element and page; against
|
|
609
|
+
* the previous run, the dashboard's own counts (the run's changesSinceLastRun) are given too.
|
|
610
|
+
*/
|
|
611
|
+
async function compareScanRuns(requested, compareWith, config, context) {
|
|
612
|
+
const reportFetcher = await initializeReportFetcher(config);
|
|
613
|
+
const source = requested.kind === "link"
|
|
614
|
+
? await reportFetcher.sourceFromReportUrl(requested.reportUrl)
|
|
615
|
+
: requested;
|
|
616
|
+
if (source.kind !== "scan") {
|
|
617
|
+
return createErrorResponse('compareWith compares Website Scanner runs: pass the newer run (scanId and scanRunId, or its report link) and compareWith: "previous" or the older run\'s scanRunId.');
|
|
239
618
|
}
|
|
240
|
-
|
|
241
|
-
|
|
619
|
+
const previous = await reportFetcher.previousScanRun(source.scanId, source.scanRunId);
|
|
620
|
+
const baseRunId = compareWith === "previous" ? previous && String(previous.id) : compareWith;
|
|
621
|
+
if (!baseRunId) {
|
|
622
|
+
return createErrorResponse(`Run ${source.scanRunId} is the first run of scan ${source.scanId}; there is no earlier run to compare it with.`);
|
|
242
623
|
}
|
|
243
|
-
|
|
244
|
-
|
|
624
|
+
const base = { ...source, scanRunId: baseRunId };
|
|
625
|
+
const againstPrevious = previous !== undefined && String(previous.id) === baseRunId;
|
|
626
|
+
let links;
|
|
627
|
+
try {
|
|
628
|
+
links = await Promise.all([
|
|
629
|
+
reportFetcher.getReportLink(source, { onWait: exportProgress(context) }),
|
|
630
|
+
reportFetcher.getReportLink(base, {}),
|
|
631
|
+
]);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
if (error instanceof ExportPendingError) {
|
|
635
|
+
return createSuccessResponse([
|
|
636
|
+
`The CSV exports of these runs are still being generated (waited ${formatDuration(error.waitedMs)}). Call fetchAccessibilityIssues again with the same arguments in about a minute; the exports continue where they are.`,
|
|
637
|
+
]);
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
const [after, before] = await Promise.all(links.map(loadMainIssues));
|
|
642
|
+
const diff = compareIssues(before, after);
|
|
643
|
+
const changes = againstPrevious
|
|
644
|
+
? await reportFetcher.changesSinceLastRun(source.scanId, source.scanRunId)
|
|
645
|
+
: undefined;
|
|
646
|
+
const baseLabel = againstPrevious
|
|
647
|
+
? `the run before it, run ${baseRunId}${previous?.scanDate ? ` (${previous.scanDate})` : ""}`
|
|
648
|
+
: `run ${baseRunId}`;
|
|
649
|
+
const matched = `${diff.added.length} new, ${diff.resolved.length} resolved, ${diff.retained} still open`;
|
|
650
|
+
const listed = (issues) => wrapUntrusted("accessibility comparison", JSON.stringify(issues.slice(0, COMPARE_LIST_LIMIT), null, 2)) +
|
|
651
|
+
(issues.length > COMPARE_LIST_LIMIT
|
|
652
|
+
? `\n(${issues.length - COMPARE_LIST_LIMIT} more not listed; see the CSV links.)`
|
|
653
|
+
: "");
|
|
654
|
+
return createSuccessResponse([
|
|
655
|
+
...(requested.kind === "link"
|
|
656
|
+
? [`Resolved the report link to ${describeSource(source)}.`]
|
|
657
|
+
: []),
|
|
658
|
+
`Compared Website Scanner scan ${source.scanId}: run ${source.scanRunId} with ${baseLabel}.`,
|
|
659
|
+
changes
|
|
660
|
+
? `The dashboard's comparison: ${changes.newIssues} new, ${changes.resolvedIssues} resolved, ${changes.retainedIssues} still open. Matching the issues by rule, element and page gives ${matched}.`
|
|
661
|
+
: `Matching the issues by rule, element and page: ${matched}.`,
|
|
662
|
+
`New issues: ${listed(diff.added)}`,
|
|
663
|
+
`Resolved issues: ${listed(diff.resolved)}`,
|
|
664
|
+
`CSV of each run, valid for about 12 hours: newer ${links[0]} , older ${links[1]}`,
|
|
665
|
+
]);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* What App Accessibility has on record, newest first, with the ids the issues are read by.
|
|
669
|
+
* Without a project name, the automated listing shows the projects: a build list needs one.
|
|
670
|
+
*/
|
|
671
|
+
async function listAppAccessibilityReports(args, config) {
|
|
672
|
+
const reportFetcher = await initializeReportFetcher(config);
|
|
673
|
+
const limit = args.limit ?? 10;
|
|
674
|
+
const filtered = (what) => args.name ? ` whose ${what} contains "${args.name}"` : "";
|
|
675
|
+
if (args.kind === "manual") {
|
|
676
|
+
const { total, reports } = await reportFetcher.listAppManualReports(args.name, limit);
|
|
677
|
+
if (reports.length === 0) {
|
|
678
|
+
return createSuccessResponse([
|
|
679
|
+
`No App Accessibility manual reports${filtered("name")}. Drop the name filter to list the newest reports.`,
|
|
680
|
+
]);
|
|
681
|
+
}
|
|
682
|
+
return createSuccessResponse([
|
|
683
|
+
`${total} App Accessibility manual reports (newest first)${filtered("name")}${total > reports.length ? `, showing ${reports.length}` : ""}:`,
|
|
684
|
+
wrapUntrusted("app accessibility reports", JSON.stringify(reports, null, 2)),
|
|
685
|
+
`Read a report's issues with fetchAccessibilityIssues with appReportId: <reportId>; its dashboard_link opens it on the dashboard.`,
|
|
686
|
+
]);
|
|
687
|
+
}
|
|
688
|
+
if (!args.projectName) {
|
|
689
|
+
const { total, projects } = await reportFetcher.listAppProjects(limit);
|
|
690
|
+
return createSuccessResponse([
|
|
691
|
+
`${total} App Accessibility projects (newest first)${total > projects.length ? `, showing ${projects.length}` : ""}:`,
|
|
692
|
+
wrapUntrusted("app accessibility projects", JSON.stringify(projects, null, 2)),
|
|
693
|
+
`A build list needs a project: call listAppAccessibilityReports again with projectName: <name> (exact) to list its builds.`,
|
|
694
|
+
]);
|
|
695
|
+
}
|
|
696
|
+
const { project, total, builds } = await reportFetcher.listAppBuilds(args.projectName, args.name, limit);
|
|
697
|
+
if (builds.length === 0) {
|
|
698
|
+
return createSuccessResponse([
|
|
699
|
+
`No builds of App Accessibility project "${project.name}"${filtered("name")}${args.name
|
|
700
|
+
? ". Drop the name filter to list the project's newest builds"
|
|
701
|
+
: ": nothing has run with accessibility on in it yet"}.`,
|
|
702
|
+
]);
|
|
703
|
+
}
|
|
704
|
+
return createSuccessResponse([
|
|
705
|
+
`${total} builds of App Accessibility project "${project.name}" (newest first)${filtered("name")}${total > builds.length ? `, showing ${builds.length}` : ""}:`,
|
|
706
|
+
wrapUntrusted("app accessibility builds", JSON.stringify(builds, null, 2)),
|
|
707
|
+
`Read a build's issues with fetchAccessibilityIssues with buildId: <buildId>; it waits while a build is still processing. report_status "failed" with scan_count 0 means the run recorded no scans.`,
|
|
708
|
+
]);
|
|
709
|
+
}
|
|
710
|
+
async function executeListAppAccessibilityReports(args, server, config) {
|
|
711
|
+
try {
|
|
712
|
+
trackMCP("listAppAccessibilityReports", server.server.getClientVersion(), undefined, config);
|
|
713
|
+
return await listAppAccessibilityReports(args, config);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
return handleMCPError("listAppAccessibilityReports", server, config, error);
|
|
245
717
|
}
|
|
246
|
-
return createSuccessResponse(messages);
|
|
247
718
|
}
|
|
248
719
|
async function executeAccessibilityScan(args, context, server, config) {
|
|
249
720
|
try {
|
|
@@ -350,32 +821,33 @@ async function runAccessibilityScan(name, pageURL, context, config, authConfigId
|
|
|
350
821
|
return createScanFailureResponse(name, status);
|
|
351
822
|
}
|
|
352
823
|
const reportFetcher = await initializeReportFetcher(config);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
scanId,
|
|
356
|
-
|
|
357
|
-
|
|
824
|
+
let reportLink;
|
|
825
|
+
try {
|
|
826
|
+
reportLink = await reportFetcher.getReportLink({ kind: "scan", scanId, scanRunId }, { onWait: exportProgress(context) });
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
if (error instanceof ExportPendingError) {
|
|
830
|
+
return createSuccessResponse([
|
|
831
|
+
`Accessibility scan "${name}" completed. Scan ID: ${scanId} and Scan Run ID: ${scanRunId}`,
|
|
832
|
+
...exportPendingMessages({
|
|
833
|
+
kind: "scan",
|
|
834
|
+
scanId: String(scanId),
|
|
835
|
+
scanRunId: String(scanRunId),
|
|
836
|
+
}, error, ""),
|
|
837
|
+
]);
|
|
838
|
+
}
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
358
841
|
const { records, page_length, total_issues, next_page, duplicate_issues, unlinked_duplicates, hidden_issues, } = await parseAccessibilityReportFromCSV(reportLink);
|
|
359
842
|
return createScanSuccessResponse(name, total_issues, page_length, records, scanId, scanRunId, reportLink, next_page, excludedIssuesNote(duplicate_issues, unlinked_duplicates, hidden_issues));
|
|
360
843
|
}
|
|
361
844
|
export default function addAccessibilityTools(server, config) {
|
|
362
845
|
const tools = {};
|
|
363
|
-
tools.
|
|
364
|
-
query: z
|
|
365
|
-
.string()
|
|
366
|
-
.describe("Any accessibility, a11y, WCAG, or web accessibility question"),
|
|
367
|
-
}, {
|
|
368
|
-
title: "Accessibility Expert",
|
|
369
|
-
readOnlyHint: true,
|
|
370
|
-
openWorldHint: false,
|
|
371
|
-
destructiveHint: false,
|
|
372
|
-
idempotentHint: true,
|
|
373
|
-
}, async (args) => {
|
|
374
|
-
return await executeAccessibilityRAG(args, server, config);
|
|
375
|
-
});
|
|
376
|
-
tools.startAccessibilityScan = server.tool("startAccessibilityScan", "Start an accessibility scan via BrowserStack and retrieve a local CSV report path.", {
|
|
846
|
+
tools.startAccessibilityScan = server.tool("startAccessibilityScan", "Scan a web page URL for accessibility issues and get a CSV report. For a mobile app, run it on App Automate with accessibility on (setupBrowserStackAppAutomateTests), then fetchAccessibilityIssues.", {
|
|
377
847
|
name: z.string().describe("Name of the accessibility scan"),
|
|
378
|
-
pageURL: z
|
|
848
|
+
pageURL: z
|
|
849
|
+
.string()
|
|
850
|
+
.describe("The web page URL to scan for accessibility issues"),
|
|
379
851
|
authConfigId: z
|
|
380
852
|
.number()
|
|
381
853
|
.optional()
|
|
@@ -466,32 +938,50 @@ export default function addAccessibilityTools(server, config) {
|
|
|
466
938
|
}, async (args) => {
|
|
467
939
|
return await executeGetAuthConfig(args, server, config);
|
|
468
940
|
});
|
|
469
|
-
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues
|
|
941
|
+
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues from one source: scanId+scanRunId, buildId, workflowReportId, appReportId or reportUrl." +
|
|
470
942
|
NEEDS_A11Y_SCAN_ID, {
|
|
471
943
|
scanId: z
|
|
472
944
|
.string()
|
|
473
945
|
.optional()
|
|
474
|
-
.describe("Website Scanner: the scan
|
|
946
|
+
.describe("Website Scanner: the scan id from startAccessibilityScan."),
|
|
475
947
|
scanRunId: z
|
|
476
948
|
.string()
|
|
477
949
|
.optional()
|
|
478
|
-
.describe("Website Scanner: the scan run
|
|
950
|
+
.describe("Website Scanner: the scan run id from startAccessibilityScan."),
|
|
479
951
|
buildId: z
|
|
480
952
|
.string()
|
|
953
|
+
.regex(/^[a-z0-9]{40}$/, "40 lowercase letters and digits")
|
|
481
954
|
.optional()
|
|
482
|
-
.describe("Automated Tests
|
|
955
|
+
.describe("Automated Tests or App Accessibility build id: the SDK's testhub id, or the thBuildId in a web report link."),
|
|
483
956
|
workflowReportId: z
|
|
484
957
|
.string()
|
|
485
958
|
.optional()
|
|
486
|
-
.describe("Workflow Analyzer: the report's numeric id
|
|
959
|
+
.describe("Workflow Analyzer: the report's numeric id."),
|
|
960
|
+
appReportId: z
|
|
961
|
+
.string()
|
|
962
|
+
.regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "A manual report id is a UUID")
|
|
963
|
+
.optional()
|
|
964
|
+
.describe("Manual App Accessibility report id (a UUID), from listAppAccessibilityReports or its /report/ link."),
|
|
487
965
|
reportUrl: z
|
|
488
966
|
.string()
|
|
489
967
|
.optional()
|
|
490
|
-
.describe("A
|
|
968
|
+
.describe("A web or app report link (dashboard, scanner share link, manual report), or an Automate or App Automate link."),
|
|
491
969
|
cursor: z
|
|
492
970
|
.number()
|
|
493
971
|
.optional()
|
|
494
|
-
.describe("Character offset for pagination (default: 0)"),
|
|
972
|
+
.describe("Character offset for pagination (default: 0); with compareWith, how many issues are already listed."),
|
|
973
|
+
pageUrl: z
|
|
974
|
+
.string()
|
|
975
|
+
.optional()
|
|
976
|
+
.describe("Only the issues on this page URL, counted as the dashboard's page view counts them."),
|
|
977
|
+
compareWith: z
|
|
978
|
+
.string()
|
|
979
|
+
.optional()
|
|
980
|
+
.describe(`"previous", an older scanRunId (Website Scanner), or an earlier app build's id or link; lists new and resolved issues.`),
|
|
981
|
+
exportTaskId: z
|
|
982
|
+
.string()
|
|
983
|
+
.optional()
|
|
984
|
+
.describe("Continue a CSV export that an earlier call said is still being generated."),
|
|
495
985
|
}, {
|
|
496
986
|
title: "Fetch Accessibility Issues",
|
|
497
987
|
readOnlyHint: true,
|
|
@@ -501,5 +991,36 @@ export default function addAccessibilityTools(server, config) {
|
|
|
501
991
|
}, async (args, context) => {
|
|
502
992
|
return await executeFetchAccessibilityIssues(args, context, server, config);
|
|
503
993
|
});
|
|
994
|
+
tools.listAppAccessibilityReports = server.tool("listAppAccessibilityReports", "List App Accessibility work newest first, with the ids fetchAccessibilityIssues reads: a project's builds (automated; omit projectName to list projects) or manual reports.", {
|
|
995
|
+
kind: z
|
|
996
|
+
.enum(["automated", "manual"])
|
|
997
|
+
.optional()
|
|
998
|
+
.default("automated")
|
|
999
|
+
.describe("automated (App Automate builds, default) or manual (Workflow Analyzer reports)."),
|
|
1000
|
+
projectName: z
|
|
1001
|
+
.string()
|
|
1002
|
+
.optional()
|
|
1003
|
+
.describe("Automated only: the exact App Accessibility project name; omit to list the projects."),
|
|
1004
|
+
name: z
|
|
1005
|
+
.string()
|
|
1006
|
+
.optional()
|
|
1007
|
+
.describe("Partial build or report name to filter by."),
|
|
1008
|
+
limit: z
|
|
1009
|
+
.number()
|
|
1010
|
+
.int()
|
|
1011
|
+
.min(1)
|
|
1012
|
+
.max(30)
|
|
1013
|
+
.optional()
|
|
1014
|
+
.default(10)
|
|
1015
|
+
.describe("Rows to return, newest first (1 to 30, default 10)."),
|
|
1016
|
+
}, {
|
|
1017
|
+
title: "List App Accessibility Reports",
|
|
1018
|
+
readOnlyHint: true,
|
|
1019
|
+
openWorldHint: false,
|
|
1020
|
+
destructiveHint: false,
|
|
1021
|
+
idempotentHint: true,
|
|
1022
|
+
}, async (args) => {
|
|
1023
|
+
return await executeListAppAccessibilityReports(args, server, config);
|
|
1024
|
+
});
|
|
504
1025
|
return tools;
|
|
505
1026
|
}
|