@browserstack/mcp-server 2.2.4-beta.1 → 2.2.5-beta.1
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/tools/accessibility.d.ts +3 -0
- package/dist/tools/accessibility.js +180 -17
- package/dist/tools/accessiblity-utils/report-fetcher.d.ts +40 -1
- package/dist/tools/accessiblity-utils/report-fetcher.js +187 -16
- package/dist/tools/accessiblity-utils/report-parser.d.ts +13 -0
- package/dist/tools/accessiblity-utils/report-parser.js +81 -12
- package/dist/tools/ask-browserstack/register.js +1 -1
- package/dist/tools/ask-browserstack/types.d.ts +1 -1
- package/dist/tools/ask-browserstack/types.js +5 -4
- package/dist/tools/tool-handoff.d.ts +4 -1
- package/dist/tools/tool-handoff.js +11 -6
- package/package.json +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { BrowserStackConfig } from "../lib/types.js";
|
|
3
|
+
/** How often, and for how long, a first read waits on a report that is still processing. */
|
|
4
|
+
export declare const REPORT_STATUS_POLL_MS = 10000;
|
|
5
|
+
export declare const REPORT_STATUS_WAIT_MS: number;
|
|
3
6
|
export default function addAccessibilityTools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
|
|
@@ -1,6 +1,6 @@
|
|
|
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, } from "./accessiblity-utils/report-fetcher.js";
|
|
4
4
|
import { AccessibilityAuthConfig, safeAuthConfigData, } from "./accessiblity-utils/auth-config.js";
|
|
5
5
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
6
|
import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js";
|
|
@@ -26,11 +26,15 @@ function createErrorResponse(message) {
|
|
|
26
26
|
isError: true,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Each message ends with a newline: clients that join text blocks with no separator
|
|
31
|
+
* would otherwise run a link at the end of one block into the next block's first word.
|
|
32
|
+
*/
|
|
29
33
|
function createSuccessResponse(messages) {
|
|
30
34
|
return {
|
|
31
35
|
content: messages.map((text) => ({
|
|
32
36
|
type: "text",
|
|
33
|
-
text
|
|
37
|
+
text: `${text}\n`,
|
|
34
38
|
})),
|
|
35
39
|
};
|
|
36
40
|
}
|
|
@@ -71,30 +75,170 @@ async function executeAccessibilityRAG(args, server, config) {
|
|
|
71
75
|
return handleMCPError("accessibilityExpert", server, config, error);
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
|
-
|
|
78
|
+
/** The one source the arguments name, or why they do not name exactly one. */
|
|
79
|
+
function issueSourceFrom(args) {
|
|
80
|
+
const named = [
|
|
81
|
+
Boolean(args.scanId || args.scanRunId),
|
|
82
|
+
Boolean(args.buildId),
|
|
83
|
+
Boolean(args.workflowReportId),
|
|
84
|
+
Boolean(args.reportUrl),
|
|
85
|
+
].filter(Boolean).length;
|
|
86
|
+
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).";
|
|
88
|
+
}
|
|
89
|
+
if (args.reportUrl)
|
|
90
|
+
return { kind: "link", reportUrl: args.reportUrl };
|
|
91
|
+
if (args.buildId)
|
|
92
|
+
return { kind: "build", buildId: args.buildId };
|
|
93
|
+
if (args.workflowReportId) {
|
|
94
|
+
return { kind: "workflow", reportId: args.workflowReportId };
|
|
95
|
+
}
|
|
96
|
+
if (!args.scanId || !args.scanRunId) {
|
|
97
|
+
return "A Website Scanner run needs both scanId and scanRunId.";
|
|
98
|
+
}
|
|
99
|
+
return { kind: "scan", scanId: args.scanId, scanRunId: args.scanRunId };
|
|
100
|
+
}
|
|
101
|
+
/** The arguments that name the same source again, for the next-page hint. */
|
|
102
|
+
function sourceArgs(source) {
|
|
103
|
+
switch (source.kind) {
|
|
104
|
+
case "scan":
|
|
105
|
+
return `scanId: "${source.scanId}", scanRunId: "${source.scanRunId}"`;
|
|
106
|
+
case "build":
|
|
107
|
+
return `buildId: "${source.buildId}"`;
|
|
108
|
+
case "workflow":
|
|
109
|
+
return `workflowReportId: "${source.reportId}"`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** What a dashboard link resolved to, in words. */
|
|
113
|
+
function describeSource(source) {
|
|
114
|
+
switch (source.kind) {
|
|
115
|
+
case "scan":
|
|
116
|
+
return `Website Scanner scan ${source.scanId}, run ${source.scanRunId}`;
|
|
117
|
+
case "build":
|
|
118
|
+
return `Automated Tests build ${source.buildId}`;
|
|
119
|
+
case "workflow":
|
|
120
|
+
return `Workflow Analyzer report ${source.reportId}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function executeFetchAccessibilityIssues(args, context, server, config) {
|
|
75
124
|
try {
|
|
76
125
|
trackMCP("fetchAccessibilityIssues", server.server.getClientVersion(), undefined, config);
|
|
77
|
-
|
|
126
|
+
const source = issueSourceFrom(args);
|
|
127
|
+
if (typeof source === "string")
|
|
128
|
+
return createErrorResponse(source);
|
|
129
|
+
return await fetchAccessibilityIssues(source, config, args.cursor, context);
|
|
78
130
|
}
|
|
79
131
|
catch (error) {
|
|
80
132
|
return handleMCPError("fetchAccessibilityIssues", server, config, error);
|
|
81
133
|
}
|
|
82
134
|
}
|
|
83
|
-
|
|
135
|
+
/**
|
|
136
|
+
* What the total leaves out, as the dashboard's count does: duplicate occurrences (folded
|
|
137
|
+
* into their main issue only where one could be matched) and issues hidden in the dashboard.
|
|
138
|
+
*/
|
|
139
|
+
function excludedIssuesNote(duplicates, unlinked, hidden) {
|
|
140
|
+
const notes = [];
|
|
141
|
+
if (duplicates) {
|
|
142
|
+
const folded = duplicates - unlinked;
|
|
143
|
+
const how = unlinked === 0
|
|
144
|
+
? " are folded into their main issue (see duplicate_count) and are not included in the total."
|
|
145
|
+
: folded > 0
|
|
146
|
+
? ` are not included in the total. ${folded} are folded into their main issue (see duplicate_count); ${unlinked} could not be matched to one.`
|
|
147
|
+
: " are not included in the total.";
|
|
148
|
+
notes.push(`This report groups repeated occurrences: ${duplicates} duplicate occurrences${how}`);
|
|
149
|
+
}
|
|
150
|
+
if (hidden) {
|
|
151
|
+
notes.push(`${hidden} issues hidden in the dashboard are not included, as the dashboard leaves them out of its count too.`);
|
|
152
|
+
}
|
|
153
|
+
return notes;
|
|
154
|
+
}
|
|
155
|
+
/** How often, and for how long, a first read waits on a report that is still processing. */
|
|
156
|
+
export const REPORT_STATUS_POLL_MS = 10_000;
|
|
157
|
+
export const REPORT_STATUS_WAIT_MS = 5 * 60 * 1000;
|
|
158
|
+
/** Statuses during which a report's issues are still being added. */
|
|
159
|
+
const PROCESSING_STATUSES = new Set([
|
|
160
|
+
"queued",
|
|
161
|
+
"running",
|
|
162
|
+
"processing",
|
|
163
|
+
"finalizing",
|
|
164
|
+
]);
|
|
165
|
+
/**
|
|
166
|
+
* Where a report is in its processing before its issues are read. Issues keep being added
|
|
167
|
+
* until it completes (an advanced scan adds its advanced-rule issues while "finalizing", for
|
|
168
|
+
* minutes after the tests end), so a first read waits for it, reporting progress, for up to
|
|
169
|
+
* REPORT_STATUS_WAIT_MS. A later page does not wait again, and a status that cannot be read
|
|
170
|
+
* is not waited on.
|
|
171
|
+
*/
|
|
172
|
+
async function waitForProcessing(reportFetcher, source, wait, context) {
|
|
173
|
+
const started = Date.now();
|
|
174
|
+
const firstStatus = await reportFetcher.reportStatus(source);
|
|
175
|
+
let status = firstStatus;
|
|
176
|
+
while (wait &&
|
|
177
|
+
status &&
|
|
178
|
+
PROCESSING_STATUSES.has(status) &&
|
|
179
|
+
Date.now() - started < REPORT_STATUS_WAIT_MS) {
|
|
180
|
+
const waited = Date.now() - started;
|
|
181
|
+
if (context) {
|
|
182
|
+
await notifyScanProgress(context, `The report is still ${status}; waiting for it to complete (${formatDuration(waited)} of up to ${formatDuration(REPORT_STATUS_WAIT_MS)}).`, Math.min(99, Math.round((waited / REPORT_STATUS_WAIT_MS) * 100)));
|
|
183
|
+
}
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, REPORT_STATUS_POLL_MS));
|
|
185
|
+
status = await reportFetcher.reportStatus(source);
|
|
186
|
+
}
|
|
187
|
+
return { status, firstStatus, waitedMs: Date.now() - started };
|
|
188
|
+
}
|
|
189
|
+
function formatDuration(ms) {
|
|
190
|
+
const seconds = Math.round(ms / 1000);
|
|
191
|
+
return seconds < 60
|
|
192
|
+
? `${seconds}s`
|
|
193
|
+
: `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
194
|
+
}
|
|
195
|
+
/** What the answer says about the report's processing, if anything. */
|
|
196
|
+
function processingNote({ status, firstStatus, waitedMs, }) {
|
|
197
|
+
if (status && PROCESSING_STATUSES.has(status)) {
|
|
198
|
+
const meaning = status === "finalizing"
|
|
199
|
+
? ", which on an advanced scan means advanced issue processing"
|
|
200
|
+
: "";
|
|
201
|
+
return [
|
|
202
|
+
`⚠️ This report is still processing (status: ${status}${meaning}). The issues below are only those found so far: more will be added and the total will grow. Call fetchAccessibilityIssues again in a few minutes for the complete list.`,
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
if (firstStatus && PROCESSING_STATUSES.has(firstStatus)) {
|
|
206
|
+
return [
|
|
207
|
+
`The report was still processing (status: ${firstStatus}); waited ${formatDuration(waitedMs)} for it to complete before reading its issues.`,
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
async function fetchAccessibilityIssues(requested, config, cursor = 0, context) {
|
|
84
213
|
const reportFetcher = await initializeReportFetcher(config);
|
|
85
|
-
const
|
|
86
|
-
|
|
214
|
+
const source = requested.kind === "link"
|
|
215
|
+
? await reportFetcher.sourceFromReportUrl(requested.reportUrl)
|
|
216
|
+
: requested;
|
|
217
|
+
const processing = await waitForProcessing(reportFetcher, source, cursor === 0, context);
|
|
218
|
+
const stillProcessing = processing.status !== undefined &&
|
|
219
|
+
PROCESSING_STATUSES.has(processing.status);
|
|
220
|
+
const reportLink = await reportFetcher.getReportLink(source);
|
|
221
|
+
const { records, page_length, total_issues, next_page, duplicate_issues, unlinked_duplicates, hidden_issues, } = await parseAccessibilityReportFromCSV(reportLink, { nextPage: cursor });
|
|
87
222
|
const currentlyShown = cursor === 0
|
|
88
223
|
? page_length
|
|
89
224
|
: Math.floor(cursor / JSON.stringify(records[0] || {}).length) +
|
|
90
225
|
page_length;
|
|
91
226
|
const remainingIssues = total_issues - currentlyShown;
|
|
92
227
|
const messages = [
|
|
228
|
+
...(requested.kind === "link"
|
|
229
|
+
? [`Resolved the report link to ${describeSource(source)}.`]
|
|
230
|
+
: []),
|
|
231
|
+
...processingNote(processing),
|
|
93
232
|
`Retrieved ${page_length} accessibility issues (Total: ${total_issues})`,
|
|
233
|
+
...excludedIssuesNote(duplicate_issues, unlinked_duplicates, hidden_issues),
|
|
234
|
+
`You can also download the full report (CSV) from the following link, valid for about 12 hours: ${reportLink}`,
|
|
94
235
|
`Issues: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
|
|
95
236
|
];
|
|
96
237
|
if (next_page !== null) {
|
|
97
|
-
messages.push(`${remainingIssues} more issues available. Use fetchAccessibilityIssues with cursor: ${next_page} to get the next batch.`);
|
|
238
|
+
messages.push(`${remainingIssues} more issues available. Use fetchAccessibilityIssues with ${sourceArgs(source)} and cursor: ${next_page} to get the next batch.`);
|
|
239
|
+
}
|
|
240
|
+
else if (stillProcessing) {
|
|
241
|
+
messages.push(`All issues found so far retrieved; the report is still processing.`);
|
|
98
242
|
}
|
|
99
243
|
else {
|
|
100
244
|
messages.push(`✅ All issues retrieved.`);
|
|
@@ -181,12 +325,13 @@ async function executeGetAuthConfig(args, server, config) {
|
|
|
181
325
|
function createScanFailureResponse(name, status) {
|
|
182
326
|
return createErrorResponse(`❌ Accessibility scan "${name}" failed with status: ${status} , check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`);
|
|
183
327
|
}
|
|
184
|
-
function createScanSuccessResponse(name, totalIssues, pageLength, records, scanId, scanRunId, reportUrl, cursor) {
|
|
328
|
+
function createScanSuccessResponse(name, totalIssues, pageLength, records, scanId, scanRunId, reportUrl, cursor, duplicates = []) {
|
|
185
329
|
const messages = [
|
|
186
330
|
`Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
|
|
187
331
|
`Scan ID: ${scanId} and Scan Run ID: ${scanRunId}`,
|
|
188
332
|
`You can also download the full report from the following link: ${reportUrl}`,
|
|
189
333
|
`We found ${totalIssues} issues. Below are the details of the ${pageLength} most critical issues.`,
|
|
334
|
+
...duplicates,
|
|
190
335
|
`Scan results: ${wrapUntrusted("accessibility scan results", JSON.stringify(records, null, 2))}`,
|
|
191
336
|
];
|
|
192
337
|
if (cursor !== null) {
|
|
@@ -205,9 +350,13 @@ async function runAccessibilityScan(name, pageURL, context, config, authConfigId
|
|
|
205
350
|
return createScanFailureResponse(name, status);
|
|
206
351
|
}
|
|
207
352
|
const reportFetcher = await initializeReportFetcher(config);
|
|
208
|
-
const reportLink = await reportFetcher.getReportLink(
|
|
209
|
-
|
|
210
|
-
|
|
353
|
+
const reportLink = await reportFetcher.getReportLink({
|
|
354
|
+
kind: "scan",
|
|
355
|
+
scanId,
|
|
356
|
+
scanRunId,
|
|
357
|
+
});
|
|
358
|
+
const { records, page_length, total_issues, next_page, duplicate_issues, unlinked_duplicates, hidden_issues, } = await parseAccessibilityReportFromCSV(reportLink);
|
|
359
|
+
return createScanSuccessResponse(name, total_issues, page_length, records, scanId, scanRunId, reportLink, next_page, excludedIssuesNote(duplicate_issues, unlinked_duplicates, hidden_issues));
|
|
211
360
|
}
|
|
212
361
|
export default function addAccessibilityTools(server, config) {
|
|
213
362
|
const tools = {};
|
|
@@ -317,14 +466,28 @@ export default function addAccessibilityTools(server, config) {
|
|
|
317
466
|
}, async (args) => {
|
|
318
467
|
return await executeGetAuthConfig(args, server, config);
|
|
319
468
|
});
|
|
320
|
-
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues from a completed
|
|
469
|
+
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues with pagination support, from exactly one source: a completed Website Scanner run (scanId with scanRunId), an Automated Tests build from a BrowserStack SDK test run (buildId), a Workflow Analyzer report (workflowReportId), or a Website Scanner or Automated Tests report's dashboard link (reportUrl). Also returns a link to download the full report as a CSV. Use cursor parameter to get subsequent pages of results." +
|
|
321
470
|
NEEDS_A11Y_SCAN_ID, {
|
|
322
471
|
scanId: z
|
|
323
472
|
.string()
|
|
324
|
-
.
|
|
473
|
+
.optional()
|
|
474
|
+
.describe("Website Scanner: the scan ID from a completed accessibility scan"),
|
|
325
475
|
scanRunId: z
|
|
326
476
|
.string()
|
|
327
|
-
.
|
|
477
|
+
.optional()
|
|
478
|
+
.describe("Website Scanner: the scan run ID from a completed accessibility scan"),
|
|
479
|
+
buildId: z
|
|
480
|
+
.string()
|
|
481
|
+
.optional()
|
|
482
|
+
.describe("Automated Tests: the Accessibility build id, shown as thBuildId in the build's accessibility report link (40 lowercase letters and digits). Not the Automate build id or the build name."),
|
|
483
|
+
workflowReportId: z
|
|
484
|
+
.string()
|
|
485
|
+
.optional()
|
|
486
|
+
.describe("Workflow Analyzer: the report's numeric id, e.g. 123456"),
|
|
487
|
+
reportUrl: z
|
|
488
|
+
.string()
|
|
489
|
+
.optional()
|
|
490
|
+
.describe("A report's Accessibility dashboard link, as the user pasted it: a Website Scanner or Automated Tests report link carrying thBuildId. A Website Scanner link resolves only to its scan's latest run. Workflow Analyzer links are not supported; use workflowReportId."),
|
|
328
491
|
cursor: z
|
|
329
492
|
.number()
|
|
330
493
|
.optional()
|
|
@@ -335,8 +498,8 @@ export default function addAccessibilityTools(server, config) {
|
|
|
335
498
|
openWorldHint: false,
|
|
336
499
|
destructiveHint: false,
|
|
337
500
|
idempotentHint: true,
|
|
338
|
-
}, async (args) => {
|
|
339
|
-
return await executeFetchAccessibilityIssues(args, server, config);
|
|
501
|
+
}, async (args, context) => {
|
|
502
|
+
return await executeFetchAccessibilityIssues(args, context, server, config);
|
|
340
503
|
});
|
|
341
504
|
return tools;
|
|
342
505
|
}
|
|
@@ -1,8 +1,47 @@
|
|
|
1
|
+
/** What to read issues from: a Website Scanner run, an Automated Tests build or a Workflow Analyzer report. */
|
|
2
|
+
export type IssueSource = {
|
|
3
|
+
kind: "scan";
|
|
4
|
+
scanId: string;
|
|
5
|
+
scanRunId: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "build";
|
|
8
|
+
buildId: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "workflow";
|
|
11
|
+
reportId: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* How far a report link's scan is searched for. The scan list has no name or link filter and
|
|
15
|
+
* spans the whole organisation, so the walk covers only the newest scans, which is where a
|
|
16
|
+
* link someone just opened will be. It runs at the list's default page size (~1.2 s a page):
|
|
17
|
+
* at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_SCAN_LIST_PAGES = 15;
|
|
1
20
|
export declare class AccessibilityReportFetcher {
|
|
2
21
|
private auth;
|
|
3
22
|
setAuth(auth: {
|
|
4
23
|
username: string;
|
|
5
24
|
password: string;
|
|
6
25
|
}): void;
|
|
7
|
-
|
|
26
|
+
private authHeaders;
|
|
27
|
+
/**
|
|
28
|
+
* The source a dashboard report link points at. An Automated Tests link carries the build
|
|
29
|
+
* id itself. A Website Scanner link carries the thBuildId of one run, which only the scan
|
|
30
|
+
* list can map to ids: each scan there carries its latest run's link. A link to an older
|
|
31
|
+
* run therefore matches no scan, and is reported as such rather than guessed at.
|
|
32
|
+
*/
|
|
33
|
+
sourceFromReportUrl(reportUrl: string): Promise<IssueSource>;
|
|
34
|
+
/** Walk the newest scans for the one whose own link carries this thBuildId. */
|
|
35
|
+
private findScanRun;
|
|
36
|
+
getReportLink(source: IssueSource): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Where the report is in its processing, as the report's own status reads it: `completed`,
|
|
39
|
+
* or `queued` / `running` / `processing` / `finalizing` while issues are still being added.
|
|
40
|
+
* Undefined when it cannot be read. A build's overview carries no status, so a build's comes
|
|
41
|
+
* from its project's build list (newest first), found by id.
|
|
42
|
+
*/
|
|
43
|
+
reportStatus(source: IssueSource): Promise<string | undefined>;
|
|
44
|
+
private buildStatus;
|
|
45
|
+
/** The `data` of a successful API response, or undefined. */
|
|
46
|
+
private getData;
|
|
8
47
|
}
|
|
@@ -1,37 +1,160 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
|
+
const API_BASE = "https://api-accessibility.browserstack.com/api";
|
|
3
|
+
/**
|
|
4
|
+
* How far a report link's scan is searched for. The scan list has no name or link filter and
|
|
5
|
+
* spans the whole organisation, so the walk covers only the newest scans, which is where a
|
|
6
|
+
* link someone just opened will be. It runs at the list's default page size (~1.2 s a page):
|
|
7
|
+
* at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_SCAN_LIST_PAGES = 15;
|
|
10
|
+
/** A build whose status is checked is one just run, so it sits at the top of its project's list. */
|
|
11
|
+
const MAX_BUILD_LIST_PAGES = 3;
|
|
12
|
+
/** The thBuildId a dashboard report link carries, if any. */
|
|
13
|
+
function thBuildIdOf(link) {
|
|
14
|
+
try {
|
|
15
|
+
return link ? new URL(link).searchParams.get("thBuildId") : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Each source exports its issues the same way: one call starts a CSV export and returns a
|
|
23
|
+
* task id, and the same route polled with that task id returns the link. Only the route and
|
|
24
|
+
* its parameter names differ (the scanner's are snake_case, the other two camelCase).
|
|
25
|
+
*/
|
|
26
|
+
function exportUrls(source) {
|
|
27
|
+
const q = encodeURIComponent;
|
|
28
|
+
switch (source.kind) {
|
|
29
|
+
case "scan": {
|
|
30
|
+
const route = `${API_BASE}/website-scanner/v1/scans/${q(source.scanId)}/scan_runs/issues`;
|
|
31
|
+
return {
|
|
32
|
+
start: `${route}?scan_run_id=${q(source.scanRunId)}`,
|
|
33
|
+
poll: (taskId) => `${route}?task_id=${q(taskId)}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
case "build": {
|
|
37
|
+
const route = `${API_BASE}/automated-tests/v1/builds/issues`;
|
|
38
|
+
return {
|
|
39
|
+
start: `${route}?buildUuid=${q(source.buildId)}`,
|
|
40
|
+
poll: (taskId) => `${route}?taskId=${q(taskId)}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case "workflow": {
|
|
44
|
+
const route = `${API_BASE}/workflow-analyzer/v1/reports/issues`;
|
|
45
|
+
return {
|
|
46
|
+
start: `${route}?reportId=${q(source.reportId)}`,
|
|
47
|
+
poll: (taskId) => `${route}?taskId=${q(taskId)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Why an export did not start, in the API's words, and where a build's id comes from when it is not found. */
|
|
53
|
+
function startFailure(source, status, body) {
|
|
54
|
+
const reason = [body?.error].flat().filter(Boolean).join("; ") ||
|
|
55
|
+
body?.data?.message ||
|
|
56
|
+
`HTTP ${status}`;
|
|
57
|
+
// The API takes only its own build id; the Automate build id is a 404 "Build not found".
|
|
58
|
+
const hint = status === 404 && source.kind === "build"
|
|
59
|
+
? ". Pass the Accessibility build id: the thBuildId in the build's accessibility report link (40 lowercase letters and digits), not the Automate build id or the build name"
|
|
60
|
+
: "";
|
|
61
|
+
return `Failed to initiate report: ${reason}${hint}`;
|
|
62
|
+
}
|
|
2
63
|
export class AccessibilityReportFetcher {
|
|
3
64
|
auth;
|
|
4
65
|
setAuth(auth) {
|
|
5
66
|
this.auth = auth;
|
|
6
67
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
68
|
+
authHeaders() {
|
|
69
|
+
if (!this.auth)
|
|
70
|
+
return undefined;
|
|
71
|
+
const { username, password } = this.auth;
|
|
72
|
+
return {
|
|
73
|
+
Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The source a dashboard report link points at. An Automated Tests link carries the build
|
|
78
|
+
* id itself. A Website Scanner link carries the thBuildId of one run, which only the scan
|
|
79
|
+
* list can map to ids: each scan there carries its latest run's link. A link to an older
|
|
80
|
+
* run therefore matches no scan, and is reported as such rather than guessed at.
|
|
81
|
+
*/
|
|
82
|
+
async sourceFromReportUrl(reportUrl) {
|
|
83
|
+
let url;
|
|
84
|
+
try {
|
|
85
|
+
url = new URL(reportUrl);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
url = undefined;
|
|
89
|
+
}
|
|
90
|
+
const thBuildId = url?.searchParams.get("thBuildId");
|
|
91
|
+
if (url && thBuildId && url.pathname.includes("/automated-tests/")) {
|
|
92
|
+
return { kind: "build", buildId: thBuildId };
|
|
93
|
+
}
|
|
94
|
+
const scanName = url?.pathname.match(/\/site-scanner\/projects\/([^/]+)\/builds/)?.[1];
|
|
95
|
+
if (thBuildId && scanName) {
|
|
96
|
+
return this.findScanRun(thBuildId, decodeURIComponent(scanName));
|
|
97
|
+
}
|
|
98
|
+
throw new Error("This link does not name a report the API can look up. Pass an Automated Tests or Website Scanner report link from the Accessibility dashboard (both carry thBuildId), or the ids: scanId with scanRunId, buildId, or workflowReportId");
|
|
99
|
+
}
|
|
100
|
+
/** Walk the newest scans for the one whose own link carries this thBuildId. */
|
|
101
|
+
async findScanRun(thBuildId, scanName) {
|
|
102
|
+
let nextPage;
|
|
103
|
+
let sameName;
|
|
104
|
+
for (let page = 0; page < MAX_SCAN_LIST_PAGES; page++) {
|
|
105
|
+
const resp = await apiClient.get({
|
|
106
|
+
url: `${API_BASE}/website-scanner/v1/scans${nextPage ? `?nextPage=${encodeURIComponent(nextPage)}` : ""}`,
|
|
107
|
+
headers: this.authHeaders(),
|
|
108
|
+
});
|
|
109
|
+
const data = resp.data?.data;
|
|
110
|
+
for (const scan of data?.scans ?? []) {
|
|
111
|
+
if (thBuildIdOf(scan.scanUrl) === thBuildId) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "scan",
|
|
114
|
+
scanId: String(scan.id),
|
|
115
|
+
scanRunId: String(scan.lastScanDetails?.scanRunId),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (sameName === undefined && scan.name === scanName) {
|
|
119
|
+
sameName = scan.id;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!data?.pagination?.hasNext || !data.pagination.nextPage) {
|
|
123
|
+
nextPage = undefined;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
nextPage = data.pagination.nextPage;
|
|
15
127
|
}
|
|
128
|
+
if (sameName !== undefined) {
|
|
129
|
+
throw new Error(`This link is to an earlier run of scan "${scanName}" (scanId ${sameName}). A link resolves only to a scan's latest run; ask the user for that run's scanRunId and pass it with scanId ${sameName}`);
|
|
130
|
+
}
|
|
131
|
+
// nextPage is still set only when the walk stopped at the page cap, not at the end.
|
|
132
|
+
const searched = nextPage
|
|
133
|
+
? ` among the most recent scans (${MAX_SCAN_LIST_PAGES} pages)`
|
|
134
|
+
: "";
|
|
135
|
+
throw new Error(`No Website Scanner scan${searched} matches this link. Pass scanId and scanRunId instead`);
|
|
136
|
+
}
|
|
137
|
+
async getReportLink(source) {
|
|
138
|
+
const urls = exportUrls(source);
|
|
139
|
+
// Initiate CSV link generation
|
|
16
140
|
const initResp = await apiClient.get({
|
|
17
|
-
url:
|
|
18
|
-
headers:
|
|
141
|
+
url: urls.start,
|
|
142
|
+
headers: this.authHeaders(),
|
|
143
|
+
raise_error: false,
|
|
19
144
|
});
|
|
20
145
|
const initData = initResp.data;
|
|
21
|
-
if (!initData
|
|
22
|
-
throw new Error(
|
|
146
|
+
if (!initResp.ok || !initData?.success) {
|
|
147
|
+
throw new Error(startFailure(source, initResp.status, initData));
|
|
23
148
|
}
|
|
24
149
|
const taskId = initData.data.task_id;
|
|
25
150
|
// Poll for the generated CSV link (task is async, may take a few seconds)
|
|
26
|
-
const reportUrl =
|
|
151
|
+
const reportUrl = urls.poll(taskId);
|
|
27
152
|
const maxAttempts = 3;
|
|
28
153
|
const pollIntervalMs = 2000;
|
|
29
154
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
30
155
|
const reportResp = await apiClient.get({
|
|
31
156
|
url: reportUrl,
|
|
32
|
-
headers:
|
|
33
|
-
? { Authorization: basicAuthHeader }
|
|
34
|
-
: undefined,
|
|
157
|
+
headers: this.authHeaders(),
|
|
35
158
|
});
|
|
36
159
|
const reportData = reportResp.data;
|
|
37
160
|
if (!reportData.success) {
|
|
@@ -45,4 +168,52 @@ export class AccessibilityReportFetcher {
|
|
|
45
168
|
}
|
|
46
169
|
throw new Error(`Report link was not available after ${maxAttempts} attempts. The CSV generation may still be in progress.`);
|
|
47
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Where the report is in its processing, as the report's own status reads it: `completed`,
|
|
173
|
+
* or `queued` / `running` / `processing` / `finalizing` while issues are still being added.
|
|
174
|
+
* Undefined when it cannot be read. A build's overview carries no status, so a build's comes
|
|
175
|
+
* from its project's build list (newest first), found by id.
|
|
176
|
+
*/
|
|
177
|
+
async reportStatus(source) {
|
|
178
|
+
const q = encodeURIComponent;
|
|
179
|
+
try {
|
|
180
|
+
switch (source.kind) {
|
|
181
|
+
case "scan":
|
|
182
|
+
return (await this.getData(`${API_BASE}/website-scanner/v1/scans/${q(source.scanId)}/scan_runs/${q(source.scanRunId)}/status`))?.status;
|
|
183
|
+
case "workflow":
|
|
184
|
+
return (await this.getData(`${API_BASE}/workflow-analyzer/v1/reports/${q(source.reportId)}`))?.summary?.status;
|
|
185
|
+
case "build":
|
|
186
|
+
return await this.buildStatus(source.buildId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async buildStatus(buildId) {
|
|
194
|
+
const q = encodeURIComponent;
|
|
195
|
+
const project = (await this.getData(`${API_BASE}/automated-tests/v1/builds/${q(buildId)}`))?.projectName;
|
|
196
|
+
if (!project)
|
|
197
|
+
return undefined;
|
|
198
|
+
let nextPage;
|
|
199
|
+
for (let page = 0; page < MAX_BUILD_LIST_PAGES; page++) {
|
|
200
|
+
const data = await this.getData(`${API_BASE}/automated-tests/v1/builds?project_name=${q(project)}&limit=100${nextPage ? `&next_page=${q(nextPage)}` : ""}`);
|
|
201
|
+
const row = data?.builds?.find((build) => build.id === buildId);
|
|
202
|
+
if (row)
|
|
203
|
+
return row.status;
|
|
204
|
+
if (!data?.pagination?.has_next || !data.pagination.next_page)
|
|
205
|
+
break;
|
|
206
|
+
nextPage = data.pagination.next_page;
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
/** The `data` of a successful API response, or undefined. */
|
|
211
|
+
async getData(url) {
|
|
212
|
+
const resp = await apiClient.get({
|
|
213
|
+
url,
|
|
214
|
+
headers: this.authHeaders(),
|
|
215
|
+
raise_error: false,
|
|
216
|
+
});
|
|
217
|
+
return resp.ok ? resp.data?.data : undefined;
|
|
218
|
+
}
|
|
48
219
|
}
|
|
@@ -5,6 +5,12 @@ type SimplifiedAccessibilityIssue = {
|
|
|
5
5
|
HTML_snippet: string;
|
|
6
6
|
how_to_fix: string;
|
|
7
7
|
severity: string;
|
|
8
|
+
/** The page the issue is on; builds and Workflow Analyzer reports span several. */
|
|
9
|
+
affected_page: string;
|
|
10
|
+
/** Automated Tests builds only: the test that found the issue. */
|
|
11
|
+
source_test?: string;
|
|
12
|
+
/** Grouped reports only: how many duplicate rows point at this issue. */
|
|
13
|
+
duplicate_count?: number;
|
|
8
14
|
};
|
|
9
15
|
type PaginationOptions = {
|
|
10
16
|
/** How many JSON-chars max per “page” (default 10000) */
|
|
@@ -16,8 +22,15 @@ type PaginatedResult = {
|
|
|
16
22
|
records: SimplifiedAccessibilityIssue[];
|
|
17
23
|
/** Character offset for the next page, or null if done */
|
|
18
24
|
page_length: number;
|
|
25
|
+
/** Main issues only when the report is grouped, so it matches the run's issue count. */
|
|
19
26
|
total_issues: number;
|
|
20
27
|
next_page: number | null;
|
|
28
|
+
/** Duplicate rows left out of `records`; null when the report is not grouped. */
|
|
29
|
+
duplicate_issues: number | null;
|
|
30
|
+
/** Of those, the ones whose main issue could not be identified (counted only here). */
|
|
31
|
+
unlinked_duplicates: number;
|
|
32
|
+
/** Issues hidden in the dashboard and left out; null when the export has no Issue status. */
|
|
33
|
+
hidden_issues: number | null;
|
|
21
34
|
};
|
|
22
35
|
export declare function parseAccessibilityReportFromCSV(reportLink: string, { maxCharacterLength, nextPage }?: PaginationOptions): Promise<PaginatedResult>;
|
|
23
36
|
export {};
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
2
|
import { parse } from "csv-parse/sync";
|
|
3
3
|
import logger from "../../logger.js";
|
|
4
|
+
const SOURCE_TEST_COLUMN = "Source test";
|
|
5
|
+
const DUPLICATE_COLUMN = "Duplicate Issue of";
|
|
6
|
+
const ISSUE_LINK_COLUMN = "Issue link";
|
|
7
|
+
const STATUS_COLUMN = "Issue status";
|
|
8
|
+
/** An issue hidden in the dashboard, which leaves it out of its count. */
|
|
9
|
+
function isHidden(row) {
|
|
10
|
+
return (row[STATUS_COLUMN] ?? "").trim().toLowerCase() === "hidden";
|
|
11
|
+
}
|
|
12
|
+
/** A grouped report marks a main issue `NA`; a duplicate carries its main issue's link. */
|
|
13
|
+
function isMainIssue(row) {
|
|
14
|
+
const value = (row[DUPLICATE_COLUMN] ?? "").trim();
|
|
15
|
+
return value === "" || value.toUpperCase() === "NA";
|
|
16
|
+
}
|
|
4
17
|
export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 5000, nextPage = 0 } = {}) {
|
|
5
18
|
// 1) Download & parse
|
|
6
19
|
const res = await apiClient.get({ url: reportLink });
|
|
@@ -8,9 +21,13 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
8
21
|
throw new Error(`Failed to download report: ${res.statusText}`);
|
|
9
22
|
const text = typeof res.data === "string" ? res.data : JSON.stringify(res.data);
|
|
10
23
|
let parsed;
|
|
24
|
+
let columns = [];
|
|
11
25
|
try {
|
|
12
26
|
parsed = parse(text, {
|
|
13
|
-
columns:
|
|
27
|
+
columns: (header) => {
|
|
28
|
+
columns = header;
|
|
29
|
+
return header;
|
|
30
|
+
},
|
|
14
31
|
skip_empty_lines: true,
|
|
15
32
|
relax_column_count_less: true,
|
|
16
33
|
});
|
|
@@ -19,16 +36,63 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
19
36
|
logger.error(`[AccessibilityReport] CSV parse failed (${text.length} chars): ${err instanceof Error ? err.message : String(err)}`);
|
|
20
37
|
throw err;
|
|
21
38
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
// 2) Leave out hidden issues and fold duplicates into their main issue, as the
|
|
40
|
+
// dashboard's count does.
|
|
41
|
+
//
|
|
42
|
+
// A hidden issue (Issue status "hidden") is left out with every occurrence of it. In a
|
|
43
|
+
// grouped report, a duplicate's "Duplicate Issue of" is the exact "Issue link" of the
|
|
44
|
+
// main issue it repeats, so the two are matched verbatim. A value that matches no main
|
|
45
|
+
// issue, or matches more than one, is not guessed at: it is counted in
|
|
46
|
+
// `unlinked_duplicates` and the duplicate total, never against a specific issue.
|
|
47
|
+
const grouped = columns.includes(DUPLICATE_COLUMN);
|
|
48
|
+
const linkable = grouped && columns.includes(ISSUE_LINK_COLUMN);
|
|
49
|
+
const hasSourceTest = columns.includes(SOURCE_TEST_COLUMN);
|
|
50
|
+
const mainRows = grouped ? parsed.filter(isMainIssue) : parsed;
|
|
51
|
+
const visibleMainRows = mainRows.filter((row) => !isHidden(row));
|
|
52
|
+
const duplicateRows = grouped
|
|
53
|
+
? parsed.filter((row) => !isMainIssue(row))
|
|
54
|
+
: [];
|
|
55
|
+
const recordOf = new Map();
|
|
56
|
+
const all = visibleMainRows.map((row) => {
|
|
57
|
+
const record = {
|
|
58
|
+
issue_type: row["Issue type"],
|
|
59
|
+
component: row["Component"],
|
|
60
|
+
issue_description: row["Issue description"],
|
|
61
|
+
HTML_snippet: row["HTML snippet"],
|
|
62
|
+
how_to_fix: row["How to fix this issue"],
|
|
63
|
+
severity: (row["Severity"] || "unknown").trim(),
|
|
64
|
+
affected_page: row["Affected page"],
|
|
65
|
+
...(hasSourceTest ? { source_test: row[SOURCE_TEST_COLUMN] } : {}),
|
|
66
|
+
...(linkable ? { duplicate_count: 0 } : {}),
|
|
67
|
+
};
|
|
68
|
+
recordOf.set(row, record);
|
|
69
|
+
return record;
|
|
70
|
+
});
|
|
71
|
+
// Each main issue's link → its record, "hidden" when it is left out, or null when two
|
|
72
|
+
// main issues share the link and their duplicates cannot be told apart.
|
|
73
|
+
const byLink = new Map();
|
|
74
|
+
if (linkable) {
|
|
75
|
+
for (const row of mainRows) {
|
|
76
|
+
const link = (row[ISSUE_LINK_COLUMN] ?? "").trim();
|
|
77
|
+
if (!link)
|
|
78
|
+
continue;
|
|
79
|
+
byLink.set(link, byLink.has(link) ? null : (recordOf.get(row) ?? "hidden"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let duplicates = 0;
|
|
83
|
+
let unlinkedDuplicates = 0;
|
|
84
|
+
for (const row of duplicateRows) {
|
|
85
|
+
const main = byLink.get((row[DUPLICATE_COLUMN] ?? "").trim());
|
|
86
|
+
if (isHidden(row) || main === "hidden")
|
|
87
|
+
continue;
|
|
88
|
+
duplicates++;
|
|
89
|
+
if (main)
|
|
90
|
+
main.duplicate_count = (main.duplicate_count ?? 0) + 1;
|
|
91
|
+
else
|
|
92
|
+
unlinkedDuplicates++;
|
|
93
|
+
}
|
|
30
94
|
const totalIssues = all.length;
|
|
31
|
-
//
|
|
95
|
+
// 3) Sort by severity
|
|
32
96
|
const order = {
|
|
33
97
|
critical: 0,
|
|
34
98
|
serious: 1,
|
|
@@ -36,7 +100,7 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
36
100
|
minor: 3,
|
|
37
101
|
};
|
|
38
102
|
all.sort((a, b) => (order[a.severity] ?? 99) - (order[b.severity] ?? 99));
|
|
39
|
-
//
|
|
103
|
+
// 4) Walk to the starting offset
|
|
40
104
|
let charCursor = 0;
|
|
41
105
|
let idx = 0;
|
|
42
106
|
for (; idx < all.length; idx++) {
|
|
@@ -45,7 +109,7 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
45
109
|
break;
|
|
46
110
|
charCursor += len;
|
|
47
111
|
}
|
|
48
|
-
//
|
|
112
|
+
// 5) Collect up to maxCharacterLength
|
|
49
113
|
const page = [];
|
|
50
114
|
for (let i = idx; i < all.length; i++) {
|
|
51
115
|
const recStr = JSON.stringify(all[i]);
|
|
@@ -61,5 +125,10 @@ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacter
|
|
|
61
125
|
next_page: hasMore ? charCursor : null,
|
|
62
126
|
page_length: pageLength,
|
|
63
127
|
total_issues: totalIssues,
|
|
128
|
+
duplicate_issues: grouped ? duplicates : null,
|
|
129
|
+
unlinked_duplicates: unlinkedDuplicates,
|
|
130
|
+
hidden_issues: columns.includes(STATUS_COLUMN)
|
|
131
|
+
? mainRows.length - visibleMainRows.length
|
|
132
|
+
: null,
|
|
64
133
|
};
|
|
65
134
|
}
|
|
@@ -406,7 +406,7 @@ export function addAskBrowserStackAITool(server, deps, config) {
|
|
|
406
406
|
tools.askBrowserStackAI = server.tool("askBrowserStackAI", DESCRIPTION, {
|
|
407
407
|
product: z
|
|
408
408
|
.enum(PRODUCTS)
|
|
409
|
-
.describe("Which product to work in: tm (Test Management),
|
|
409
|
+
.describe("Which product to work in: tm (Test Management), " +
|
|
410
410
|
"tra (Test Reporting & Analytics)."),
|
|
411
411
|
// OPTIONAL, because a resume does not carry one. `next_step` tells the model to
|
|
412
412
|
// call again "with exactly these params" — `product`, `run_id`, `perm_id`,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* different repo, at the same time. A field renamed here to read better is a field the
|
|
7
7
|
* other half will never send. Nothing here changes without changing that document first.
|
|
8
8
|
*/
|
|
9
|
-
export declare const PRODUCTS: readonly ["tm", "
|
|
9
|
+
export declare const PRODUCTS: readonly ["tm", "tra"];
|
|
10
10
|
export type Product = (typeof PRODUCTS)[number];
|
|
11
11
|
/** CONTRACT §2 — what Atlas emits on the run's stream when its gate needs a human. */
|
|
12
12
|
export interface PermissionAsk {
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* different repo, at the same time. A field renamed here to read better is a field the
|
|
7
7
|
* other half will never send. Nothing here changes without changing that document first.
|
|
8
8
|
*/
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
9
|
+
// a11y is deliberately ABSENT while Ask AI is in limited alpha. Atlas itself serves the
|
|
10
|
+
// product; this tool just does not offer it yet. Re-adding it means this list, the `product`
|
|
11
|
+
// describe text in register.ts, and the two a11y handoffs in tool-handoff.ts — which stop
|
|
12
|
+
// pointing here precisely because the call would now be rejected.
|
|
13
|
+
export const PRODUCTS = ["tm", "tra"];
|
|
13
14
|
export const ASK_STATUSES = [
|
|
14
15
|
"ok",
|
|
15
16
|
"blocked",
|
|
@@ -61,7 +61,10 @@ export declare const NEEDS_BUILD_ID: string;
|
|
|
61
61
|
* pointing at the agent when a real tool exists is exactly what the header above forbids.
|
|
62
62
|
*/
|
|
63
63
|
export declare const NEEDS_SESSION_ID: string;
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* A scan run's ids come from startAccessibilityScan. Nothing here lists builds or reports,
|
|
66
|
+
* but the tool takes a report's dashboard link and looks the ids up itself.
|
|
67
|
+
*/
|
|
65
68
|
export declare const NEEDS_A11Y_SCAN_ID: string;
|
|
66
69
|
/** Auth-config ids are not listable by any tool here. */
|
|
67
70
|
export declare const NEEDS_A11Y_CONFIG_ID: string;
|
|
@@ -70,13 +70,18 @@ export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId
|
|
|
70
70
|
*/
|
|
71
71
|
export const NEEDS_SESSION_ID = " Requires a session id. listSessions lists them for a build; if you do not have the " +
|
|
72
72
|
"build either, getBuildId or listBuildId resolves one from a project and build name.";
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
/**
|
|
74
|
+
* A scan run's ids come from startAccessibilityScan. Nothing here lists builds or reports,
|
|
75
|
+
* but the tool takes a report's dashboard link and looks the ids up itself.
|
|
76
|
+
*/
|
|
77
|
+
export const NEEDS_A11Y_SCAN_ID = " Requires a completed source: a Website Scanner run's ids, returned by " +
|
|
78
|
+
"startAccessibilityScan; an Automated Tests build's id, the thBuildId in its accessibility " +
|
|
79
|
+
"report link; a Workflow Analyzer report's numeric id; or a Website Scanner or Automated " +
|
|
80
|
+
"Tests report's dashboard link, passed as reportUrl. No tool here lists past scans, builds " +
|
|
81
|
+
"or reports, so if you have none of these, ask the user for the report link, or start a " +
|
|
82
|
+
"new scan, rather than guessing.";
|
|
77
83
|
/** Auth-config ids are not listable by any tool here. */
|
|
78
84
|
export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
|
|
79
|
-
"existing configurations, so if you do not have the id,
|
|
80
|
-
'product "a11y" to find it, or create one rather than guessing.';
|
|
85
|
+
"existing configurations, so if you do not have the id, create one rather than guessing.";
|
|
81
86
|
/** Test ids come from listTestIds, which itself needs a build id. */
|
|
82
87
|
export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");
|