@browserstack/mcp-server 2.2.6-beta.2 → 2.2.6
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/server-factory.js +1 -6
- package/dist/tools/accessibility.js +69 -680
- package/dist/tools/accessiblity-utils/accessibility-rag.d.ts +12 -0
- package/dist/tools/accessiblity-utils/accessibility-rag.js +59 -0
- package/dist/tools/accessiblity-utils/report-fetcher.d.ts +7 -116
- package/dist/tools/accessiblity-utils/report-fetcher.js +29 -268
- package/dist/tools/accessiblity-utils/report-parser.d.ts +1 -41
- package/dist/tools/accessiblity-utils/report-parser.js +43 -142
- package/dist/tools/accessiblity-utils/scanner.d.ts +1 -15
- package/dist/tools/accessiblity-utils/scanner.js +4 -3
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +0 -2
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +5 -15
- package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -2
- package/dist/tools/appautomate-utils/appium-sdk/constants.js +1 -6
- package/dist/tools/appautomate-utils/appium-sdk/handler.js +1 -12
- package/dist/tools/appautomate-utils/appium-sdk/instructions.d.ts +1 -3
- package/dist/tools/appautomate-utils/appium-sdk/instructions.js +2 -2
- package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.d.ts +1 -3
- package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.js +6 -19
- package/dist/tools/appautomate-utils/native-execution/appautomate.d.ts +2 -7
- package/dist/tools/appautomate-utils/native-execution/appautomate.js +12 -15
- package/dist/tools/appautomate-utils/native-execution/constants.d.ts +1 -2
- package/dist/tools/appautomate-utils/native-execution/constants.js +1 -6
- package/dist/tools/appautomate.js +16 -29
- 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 +2 -3
- package/dist/tools/tool-handoff.js +2 -3
- package/package.json +1 -1
- package/dist/lib/accessibility-routing.d.ts +0 -6
- package/dist/lib/accessibility-routing.js +0 -6
- package/dist/tools/accessiblity-utils/app-report-fetcher.d.ts +0 -412
- package/dist/tools/accessiblity-utils/app-report-fetcher.js +0 -926
- package/dist/tools/accessiblity-utils/paginate.d.ts +0 -27
- package/dist/tools/accessiblity-utils/paginate.js +0 -50
- package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.d.ts +0 -4
- package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.js +0 -47
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RAGChunk {
|
|
2
|
+
url: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
6
|
+
export interface AccessibilityRAGResponse {
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function queryAccessibilityRAG(userQuery: string, config: BrowserStackConfig): Promise<any>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
2
|
+
import { wrapUntrusted } from "../../lib/untrusted-content.js";
|
|
3
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
4
|
+
export async function queryAccessibilityRAG(userQuery, config) {
|
|
5
|
+
const url = "https://accessibility.browserstack.com/api/tcg-proxy/search";
|
|
6
|
+
const authString = getBrowserStackAuth(config);
|
|
7
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
8
|
+
const response = await apiClient.post({
|
|
9
|
+
url,
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
Authorization: `Basic ${auth}`,
|
|
13
|
+
},
|
|
14
|
+
body: {
|
|
15
|
+
query: userQuery,
|
|
16
|
+
},
|
|
17
|
+
raise_error: false,
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const errorText = typeof response.data === "string"
|
|
21
|
+
? response.data
|
|
22
|
+
: JSON.stringify(response.data);
|
|
23
|
+
throw new Error(`RAG endpoint error: ${response.status} ${errorText}`);
|
|
24
|
+
}
|
|
25
|
+
const responseData = response.data;
|
|
26
|
+
if (!responseData.success) {
|
|
27
|
+
throw new Error("Something went wrong: " + responseData.message);
|
|
28
|
+
}
|
|
29
|
+
// Parse the stringified JSON data
|
|
30
|
+
let parsedData;
|
|
31
|
+
try {
|
|
32
|
+
parsedData = JSON.parse(responseData.data);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error("Failed to parse RAG response data as JSON: " +
|
|
36
|
+
(err instanceof Error ? err.message : String(err)));
|
|
37
|
+
}
|
|
38
|
+
if (!parsedData ||
|
|
39
|
+
!parsedData.data ||
|
|
40
|
+
!Array.isArray(parsedData.data.chunks)) {
|
|
41
|
+
throw new Error("RAG response data is missing expected 'data.chunks' array");
|
|
42
|
+
}
|
|
43
|
+
const chunks = parsedData.data.chunks;
|
|
44
|
+
// Format the response properly
|
|
45
|
+
const instruction = "IMPORTANT: Use ONLY the data provided below to answer the user's accessibility question. Do not use any external knowledge. When answering, you MUST include the relevant BrowserStack documentation links provided in the sources for personalization and further reference.\n\n";
|
|
46
|
+
const formattedChunks = chunks
|
|
47
|
+
.map((chunk, index) => `${index + 1}: Source: ${chunk.url}\n\n${chunk.content}`)
|
|
48
|
+
.join("\n\n---\n\n");
|
|
49
|
+
const formattedResponse = instruction +
|
|
50
|
+
wrapUntrusted("BrowserStack accessibility documentation", formattedChunks);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: formattedResponse,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export { MAX_APP_BUILD_LIST_PAGES } from "./app-report-fetcher.js";
|
|
3
|
-
/**
|
|
4
|
-
* What to read issues from: a Website Scanner run, an Automated Tests build, a Workflow
|
|
5
|
-
* Analyzer report, or an App Accessibility build (a mobile app run through App Automate).
|
|
6
|
-
*/
|
|
1
|
+
/** What to read issues from: a Website Scanner run, an Automated Tests build or a Workflow Analyzer report. */
|
|
7
2
|
export type IssueSource = {
|
|
8
3
|
kind: "scan";
|
|
9
4
|
scanId: string;
|
|
@@ -14,12 +9,6 @@ export type IssueSource = {
|
|
|
14
9
|
} | {
|
|
15
10
|
kind: "workflow";
|
|
16
11
|
reportId: string;
|
|
17
|
-
} | {
|
|
18
|
-
kind: "appBuild";
|
|
19
|
-
buildId: string;
|
|
20
|
-
} | {
|
|
21
|
-
kind: "appReport";
|
|
22
|
-
reportId: string;
|
|
23
12
|
};
|
|
24
13
|
/**
|
|
25
14
|
* How far a report link's scan is searched for. The scan list has no name or link filter and
|
|
@@ -28,46 +17,8 @@ export type IssueSource = {
|
|
|
28
17
|
* at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster.
|
|
29
18
|
*/
|
|
30
19
|
export declare const MAX_SCAN_LIST_PAGES = 15;
|
|
31
|
-
/**
|
|
32
|
-
* How long one call waits on a CSV export. Export time grows with the report's rows: a few
|
|
33
|
-
* seconds for hundreds, ~17 s for 18k rows, over 2 minutes for 30k issues. A call stays well
|
|
34
|
-
* under the 60 s request timeout MCP clients default to; an export still running after this
|
|
35
|
-
* is handed back to be continued by the next call rather than started again.
|
|
36
|
-
*/
|
|
37
|
-
export declare const EXPORT_WAIT_MS = 45000;
|
|
38
|
-
/**
|
|
39
|
-
* Exports per report, kept for the life of the server: the finished link to reuse for later
|
|
40
|
-
* pages, or the task of an export still running, to continue instead of starting another.
|
|
41
|
-
* Keyed by the account and the report, so one account never sees another's.
|
|
42
|
-
*/
|
|
43
|
-
export declare const reportExports: Map<string, {
|
|
44
|
-
link?: string;
|
|
45
|
-
expiresAt?: number;
|
|
46
|
-
taskId?: string;
|
|
47
|
-
}>;
|
|
48
|
-
/** An export still being generated when a call stopped waiting on it. */
|
|
49
|
-
export declare class ExportPendingError extends Error {
|
|
50
|
-
readonly taskId: string;
|
|
51
|
-
readonly waitedMs: number;
|
|
52
|
-
constructor(taskId: string, waitedMs: number);
|
|
53
|
-
}
|
|
54
|
-
/** A run in a scan's history, newest first in the API's list. */
|
|
55
|
-
export interface ScanRun {
|
|
56
|
-
id: number;
|
|
57
|
-
scanDate: string;
|
|
58
|
-
issues?: number;
|
|
59
|
-
}
|
|
60
|
-
/** What the dashboard counts between a run and the run before it. */
|
|
61
|
-
export interface RunChanges {
|
|
62
|
-
newIssues: number;
|
|
63
|
-
retainedIssues: number;
|
|
64
|
-
resolvedIssues: number;
|
|
65
|
-
}
|
|
66
20
|
export declare class AccessibilityReportFetcher {
|
|
67
21
|
private auth;
|
|
68
|
-
private readonly app;
|
|
69
|
-
/** Web builds whose overview was read, by id, so a status check need not read it again. */
|
|
70
|
-
private readonly webBuildProjects;
|
|
71
22
|
setAuth(auth: {
|
|
72
23
|
username: string;
|
|
73
24
|
password: string;
|
|
@@ -75,78 +26,18 @@ export declare class AccessibilityReportFetcher {
|
|
|
75
26
|
private authHeaders;
|
|
76
27
|
/**
|
|
77
28
|
* The source a dashboard report link points at. An Automated Tests link carries the build
|
|
78
|
-
* id itself.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* A link to an older run therefore matches no scan, and is reported as such rather than
|
|
82
|
-
* guessed at. A scanner share link carries the dashboard link it shows. An App
|
|
83
|
-
* Accessibility link names its build by project, name and number, which the project's
|
|
84
|
-
* build list maps to the id. An App Automate build or session link maps to the
|
|
85
|
-
* accessibility build through a session's test code.
|
|
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.
|
|
86
32
|
*/
|
|
87
33
|
sourceFromReportUrl(reportUrl: string): Promise<IssueSource>;
|
|
88
|
-
/**
|
|
89
|
-
* A build id names a web build or a mobile app build: both are TestHub ids, and only the
|
|
90
|
-
* API that ran the build knows it. The web API is asked first; a build it does not know
|
|
91
|
-
* that the App Accessibility API does is an app build. One neither knows is refused
|
|
92
|
-
* here, naming both.
|
|
93
|
-
*/
|
|
94
|
-
resolveBuild(source: IssueSource): Promise<IssueSource>;
|
|
95
|
-
/** An app build's issues, read inline from its scanned screens. */
|
|
96
|
-
appBuildReport(buildId: string, cursor?: number, maxCharacterLength?: number): Promise<AppBuildReport>;
|
|
97
|
-
/** Every issue of an app build at once, to compare it with another. */
|
|
98
|
-
appBuildIssues(buildId: string): Promise<AppBuildIssues>;
|
|
99
|
-
/** The build before an app build in its project, if it has one. */
|
|
100
|
-
previousAppBuild(buildId: string): Promise<AppBuildRef | undefined>;
|
|
101
|
-
/** A manual (Workflow Analyzer) app report's issues, read inline from its screens. */
|
|
102
|
-
appReportIssues(reportId: string, cursor?: number, maxCharacterLength?: number): Promise<AppManualReport>;
|
|
103
|
-
listAppProjects(limit: number): Promise<{
|
|
104
|
-
projects: AppProjectRow[];
|
|
105
|
-
total: number;
|
|
106
|
-
}>;
|
|
107
|
-
listAppBuilds(projectName: string, buildName: string | undefined, limit: number): Promise<{
|
|
108
|
-
project: {
|
|
109
|
-
id: number;
|
|
110
|
-
name: string;
|
|
111
|
-
};
|
|
112
|
-
total: number;
|
|
113
|
-
builds: AppBuildRow[];
|
|
114
|
-
}>;
|
|
115
|
-
listAppManualReports(name: string | undefined, limit: number): Promise<{
|
|
116
|
-
total: number;
|
|
117
|
-
reports: AppManualReportRow[];
|
|
118
|
-
}>;
|
|
119
|
-
/** Any one session of an Automate build; every session of an SDK run names the same build. */
|
|
120
|
-
private anyAutomateSession;
|
|
121
|
-
/**
|
|
122
|
-
* The Accessibility build an Automate session ran in. A session run through the BrowserStack
|
|
123
|
-
* SDK belongs to the SDK's build, whose id is also the Accessibility build id, and the
|
|
124
|
-
* session's test-code record names it (build_uuid). A session run without the SDK has no
|
|
125
|
-
* such record, and an SDK run without accessibility testing has a build the Accessibility
|
|
126
|
-
* API does not know; both are said plainly instead of surfacing later as a bare 404.
|
|
127
|
-
*/
|
|
128
|
-
private buildOfAutomateSession;
|
|
129
34
|
/** Walk the newest scans for the one whose own link carries this thBuildId. */
|
|
130
35
|
private findScanRun;
|
|
131
|
-
|
|
132
|
-
* The CSV link of a report's issues. A finished export is reused until its link expires.
|
|
133
|
-
* Otherwise an export is started, or continued (`exportTaskId`, or the one an earlier call
|
|
134
|
-
* left running), and waited on for up to EXPORT_WAIT_MS; one still running then is handed
|
|
135
|
-
* back as an ExportPendingError carrying its task id.
|
|
136
|
-
*/
|
|
137
|
-
getReportLink(source: IssueSource, options?: {
|
|
138
|
-
exportTaskId?: string;
|
|
139
|
-
onWait?: (waitedMs: number) => Promise<void>;
|
|
140
|
-
}): Promise<string>;
|
|
141
|
-
/** The run before `scanRunId` in its scan's history, or undefined for the first run. */
|
|
142
|
-
previousScanRun(scanId: string, scanRunId: string): Promise<ScanRun | undefined>;
|
|
143
|
-
/** The dashboard's new / retained / resolved counts against the run before this one. */
|
|
144
|
-
changesSinceLastRun(scanId: string, scanRunId: string): Promise<RunChanges | undefined>;
|
|
36
|
+
getReportLink(source: IssueSource): Promise<string>;
|
|
145
37
|
/**
|
|
146
38
|
* Where the report is in its processing, as the report's own status reads it: `completed`,
|
|
147
|
-
* or `queued` / `running` / `processing` / `finalizing` while issues are still being added
|
|
148
|
-
*
|
|
149
|
-
* when it cannot be read. A web build's overview carries no status, so a build's comes
|
|
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
|
|
150
41
|
* from its project's build list (newest first), found by id.
|
|
151
42
|
*/
|
|
152
43
|
reportStatus(source: IssueSource): Promise<string | undefined>;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
|
-
import { AppAccessibilityApi, manualReportIdFromDashboardLink, manualReportStatus, } from "./app-report-fetcher.js";
|
|
3
|
-
export { MAX_APP_BUILD_LIST_PAGES } from "./app-report-fetcher.js";
|
|
4
|
-
import { SessionType } from "../../lib/constants.js";
|
|
5
|
-
import { sessionsListUrl } from "../automate-utils/list-session-ids.js";
|
|
6
|
-
import { getAutomationBaseUrl } from "../rca-agent-utils/constants.js";
|
|
7
2
|
const API_BASE = "https://api-accessibility.browserstack.com/api";
|
|
8
3
|
/**
|
|
9
4
|
* How far a report link's scan is searched for. The scan list has no name or link filter and
|
|
@@ -12,50 +7,8 @@ const API_BASE = "https://api-accessibility.browserstack.com/api";
|
|
|
12
7
|
* at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster.
|
|
13
8
|
*/
|
|
14
9
|
export const MAX_SCAN_LIST_PAGES = 15;
|
|
15
|
-
/** How many pages of a scan's runs are read to find the run before a given one. */
|
|
16
|
-
const MAX_SCAN_RUN_PAGES = 10;
|
|
17
10
|
/** A build whose status is checked is one just run, so it sits at the top of its project's list. */
|
|
18
11
|
const MAX_BUILD_LIST_PAGES = 3;
|
|
19
|
-
/**
|
|
20
|
-
* How long one call waits on a CSV export. Export time grows with the report's rows: a few
|
|
21
|
-
* seconds for hundreds, ~17 s for 18k rows, over 2 minutes for 30k issues. A call stays well
|
|
22
|
-
* under the 60 s request timeout MCP clients default to; an export still running after this
|
|
23
|
-
* is handed back to be continued by the next call rather than started again.
|
|
24
|
-
*/
|
|
25
|
-
export const EXPORT_WAIT_MS = 45_000;
|
|
26
|
-
const EXPORT_POLL_MS = [1000, 2000, 3000, 5000];
|
|
27
|
-
/** A finished export's link is reused until shortly before it expires (about 12 hours). */
|
|
28
|
-
const LINK_EXPIRY_MARGIN_MS = 10 * 60 * 1000;
|
|
29
|
-
const LINK_FALLBACK_TTL_MS = 60 * 60 * 1000;
|
|
30
|
-
/**
|
|
31
|
-
* Exports per report, kept for the life of the server: the finished link to reuse for later
|
|
32
|
-
* pages, or the task of an export still running, to continue instead of starting another.
|
|
33
|
-
* Keyed by the account and the report, so one account never sees another's.
|
|
34
|
-
*/
|
|
35
|
-
export const reportExports = new Map();
|
|
36
|
-
/** An export still being generated when a call stopped waiting on it. */
|
|
37
|
-
export class ExportPendingError extends Error {
|
|
38
|
-
taskId;
|
|
39
|
-
waitedMs;
|
|
40
|
-
constructor(taskId, waitedMs) {
|
|
41
|
-
super(`The report's CSV export is still being generated after ${Math.round(waitedMs / 1000)}s (export task ${taskId}).`);
|
|
42
|
-
this.taskId = taskId;
|
|
43
|
-
this.waitedMs = waitedMs;
|
|
44
|
-
this.name = "ExportPendingError";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** When a presigned export link stops working, from its Expires parameter. */
|
|
48
|
-
function linkExpiry(link) {
|
|
49
|
-
try {
|
|
50
|
-
const expires = Number(new URL(link).searchParams.get("Expires"));
|
|
51
|
-
if (expires > 0)
|
|
52
|
-
return expires * 1000 - LINK_EXPIRY_MARGIN_MS;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Not a URL we can read; fall back below.
|
|
56
|
-
}
|
|
57
|
-
return Date.now() + LINK_FALLBACK_TTL_MS;
|
|
58
|
-
}
|
|
59
12
|
/** The thBuildId a dashboard report link carries, if any. */
|
|
60
13
|
function thBuildIdOf(link) {
|
|
61
14
|
try {
|
|
@@ -94,9 +47,6 @@ function exportUrls(source) {
|
|
|
94
47
|
poll: (taskId) => `${route}?taskId=${q(taskId)}`,
|
|
95
48
|
};
|
|
96
49
|
}
|
|
97
|
-
case "appBuild":
|
|
98
|
-
case "appReport":
|
|
99
|
-
throw new Error("An App Accessibility build or report has no CSV export; its issues are read inline");
|
|
100
50
|
}
|
|
101
51
|
}
|
|
102
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. */
|
|
@@ -106,24 +56,12 @@ function startFailure(source, status, body) {
|
|
|
106
56
|
`HTTP ${status}`;
|
|
107
57
|
// The API takes only its own build id; the Automate build id is a 404 "Build not found".
|
|
108
58
|
const hint = status === 404 && source.kind === "build"
|
|
109
|
-
? ". 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
|
|
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"
|
|
110
60
|
: "";
|
|
111
61
|
return `Failed to initiate report: ${reason}${hint}`;
|
|
112
62
|
}
|
|
113
|
-
/** The build and session ids an Automate dashboard link carries, if it is one. */
|
|
114
|
-
function automateIds(url) {
|
|
115
|
-
if (url.hostname !== "automate.browserstack.com")
|
|
116
|
-
return undefined;
|
|
117
|
-
return {
|
|
118
|
-
buildId: url.pathname.match(/\/builds\/([0-9a-f]{40})(?:\/|$)/i)?.[1],
|
|
119
|
-
sessionId: url.pathname.match(/\/sessions\/([0-9a-f]{40})(?:\/|$)/i)?.[1],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
63
|
export class AccessibilityReportFetcher {
|
|
123
64
|
auth;
|
|
124
|
-
app = new AppAccessibilityApi(() => this.authHeaders());
|
|
125
|
-
/** Web builds whose overview was read, by id, so a status check need not read it again. */
|
|
126
|
-
webBuildProjects = new Map();
|
|
127
65
|
setAuth(auth) {
|
|
128
66
|
this.auth = auth;
|
|
129
67
|
}
|
|
@@ -137,14 +75,9 @@ export class AccessibilityReportFetcher {
|
|
|
137
75
|
}
|
|
138
76
|
/**
|
|
139
77
|
* The source a dashboard report link points at. An Automated Tests link carries the build
|
|
140
|
-
* id itself.
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* A link to an older run therefore matches no scan, and is reported as such rather than
|
|
144
|
-
* guessed at. A scanner share link carries the dashboard link it shows. An App
|
|
145
|
-
* Accessibility link names its build by project, name and number, which the project's
|
|
146
|
-
* build list maps to the id. An App Automate build or session link maps to the
|
|
147
|
-
* accessibility build through a session's test code.
|
|
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.
|
|
148
81
|
*/
|
|
149
82
|
async sourceFromReportUrl(reportUrl) {
|
|
150
83
|
let url;
|
|
@@ -154,26 +87,6 @@ export class AccessibilityReportFetcher {
|
|
|
154
87
|
catch {
|
|
155
88
|
url = undefined;
|
|
156
89
|
}
|
|
157
|
-
// A scanner share link (scanner.browserstack.com/.../a11y-report) carries the dashboard
|
|
158
|
-
// link it shows as its reportUrl parameter.
|
|
159
|
-
const shared = url?.searchParams.get("reportUrl");
|
|
160
|
-
if (shared && shared !== reportUrl)
|
|
161
|
-
return this.sourceFromReportUrl(shared);
|
|
162
|
-
const manualReportId = url && manualReportIdFromDashboardLink(url);
|
|
163
|
-
if (manualReportId)
|
|
164
|
-
return { kind: "appReport", reportId: manualReportId };
|
|
165
|
-
const appBuildId = url &&
|
|
166
|
-
((await this.app.buildIdFromDashboardLink(url)) ??
|
|
167
|
-
(await this.app.buildIdFromAppAutomateLink(url)));
|
|
168
|
-
if (appBuildId)
|
|
169
|
-
return { kind: "appBuild", buildId: appBuildId };
|
|
170
|
-
const automate = url && automateIds(url);
|
|
171
|
-
if (automate?.sessionId) {
|
|
172
|
-
return this.buildOfAutomateSession(automate.sessionId);
|
|
173
|
-
}
|
|
174
|
-
if (automate?.buildId) {
|
|
175
|
-
return this.buildOfAutomateSession(await this.anyAutomateSession(automate.buildId));
|
|
176
|
-
}
|
|
177
90
|
const thBuildId = url?.searchParams.get("thBuildId");
|
|
178
91
|
if (url && thBuildId && url.pathname.includes("/automated-tests/")) {
|
|
179
92
|
return { kind: "build", buildId: thBuildId };
|
|
@@ -182,107 +95,7 @@ export class AccessibilityReportFetcher {
|
|
|
182
95
|
if (thBuildId && scanName) {
|
|
183
96
|
return this.findScanRun(thBuildId, decodeURIComponent(scanName));
|
|
184
97
|
}
|
|
185
|
-
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)
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* A build id names a web build or a mobile app build: both are TestHub ids, and only the
|
|
189
|
-
* API that ran the build knows it. The web API is asked first; a build it does not know
|
|
190
|
-
* that the App Accessibility API does is an app build. One neither knows is refused
|
|
191
|
-
* here, naming both.
|
|
192
|
-
*/
|
|
193
|
-
async resolveBuild(source) {
|
|
194
|
-
if (source.kind !== "build")
|
|
195
|
-
return source;
|
|
196
|
-
const web = await this.getData(`${API_BASE}/automated-tests/v1/builds/${encodeURIComponent(source.buildId)}`);
|
|
197
|
-
if (web) {
|
|
198
|
-
if (web.projectName) {
|
|
199
|
-
this.webBuildProjects.set(source.buildId, web.projectName);
|
|
200
|
-
}
|
|
201
|
-
return source;
|
|
202
|
-
}
|
|
203
|
-
const meta = await this.app.buildMeta(source.buildId);
|
|
204
|
-
if (meta)
|
|
205
|
-
return { kind: "appBuild", buildId: source.buildId };
|
|
206
|
-
throw new Error(`No Automated Tests (web) or App Accessibility build with id "${source.buildId}". Pass the id the BrowserStack SDK printed as "testhub id" at the end of the run (40 lowercase letters and digits), or the report's dashboard link as reportUrl`);
|
|
207
|
-
}
|
|
208
|
-
/** An app build's issues, read inline from its scanned screens. */
|
|
209
|
-
appBuildReport(buildId, cursor = 0, maxCharacterLength) {
|
|
210
|
-
return this.app.report(buildId, cursor, maxCharacterLength);
|
|
211
|
-
}
|
|
212
|
-
/** Every issue of an app build at once, to compare it with another. */
|
|
213
|
-
appBuildIssues(buildId) {
|
|
214
|
-
return this.app.buildIssues(buildId);
|
|
215
|
-
}
|
|
216
|
-
/** The build before an app build in its project, if it has one. */
|
|
217
|
-
previousAppBuild(buildId) {
|
|
218
|
-
return this.app.previousBuild(buildId);
|
|
219
|
-
}
|
|
220
|
-
/** A manual (Workflow Analyzer) app report's issues, read inline from its screens. */
|
|
221
|
-
appReportIssues(reportId, cursor = 0, maxCharacterLength) {
|
|
222
|
-
return this.app.appReportIssues(reportId, cursor, maxCharacterLength);
|
|
223
|
-
}
|
|
224
|
-
listAppProjects(limit) {
|
|
225
|
-
return this.app.listAppProjects(limit);
|
|
226
|
-
}
|
|
227
|
-
listAppBuilds(projectName, buildName, limit) {
|
|
228
|
-
return this.app.listAppBuilds(projectName, buildName, limit);
|
|
229
|
-
}
|
|
230
|
-
listAppManualReports(name, limit) {
|
|
231
|
-
return this.app.listAppManualReports(name, limit);
|
|
232
|
-
}
|
|
233
|
-
/** Any one session of an Automate build; every session of an SDK run names the same build. */
|
|
234
|
-
async anyAutomateSession(automateBuildId) {
|
|
235
|
-
const resp = await apiClient.get({
|
|
236
|
-
url: `${sessionsListUrl(SessionType.Automate, automateBuildId)}?limit=1`,
|
|
237
|
-
headers: this.authHeaders(),
|
|
238
|
-
raise_error: false,
|
|
239
|
-
});
|
|
240
|
-
if (resp.status === 404) {
|
|
241
|
-
throw new Error(`No Automate build with id ${automateBuildId}`);
|
|
242
|
-
}
|
|
243
|
-
if (!resp.ok) {
|
|
244
|
-
throw new Error(`Could not list the sessions of Automate build ${automateBuildId}: HTTP ${resp.status}`);
|
|
245
|
-
}
|
|
246
|
-
const sessionId = resp.data?.[0]?.automation_session?.hashed_id;
|
|
247
|
-
if (!sessionId) {
|
|
248
|
-
throw new Error(`Automate build ${automateBuildId} has no sessions`);
|
|
249
|
-
}
|
|
250
|
-
return sessionId;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* The Accessibility build an Automate session ran in. A session run through the BrowserStack
|
|
254
|
-
* SDK belongs to the SDK's build, whose id is also the Accessibility build id, and the
|
|
255
|
-
* session's test-code record names it (build_uuid). A session run without the SDK has no
|
|
256
|
-
* such record, and an SDK run without accessibility testing has a build the Accessibility
|
|
257
|
-
* API does not know; both are said plainly instead of surfacing later as a bare 404.
|
|
258
|
-
*/
|
|
259
|
-
async buildOfAutomateSession(sessionId) {
|
|
260
|
-
const resp = await apiClient.get({
|
|
261
|
-
url: `${getAutomationBaseUrl()}/ext/v1/sessions/${encodeURIComponent(sessionId)}/testCode`,
|
|
262
|
-
headers: this.authHeaders(),
|
|
263
|
-
raise_error: false,
|
|
264
|
-
});
|
|
265
|
-
if (!resp.ok && resp.status !== 404) {
|
|
266
|
-
throw new Error(`Could not read Automate session ${sessionId}: HTTP ${resp.status}`);
|
|
267
|
-
}
|
|
268
|
-
const buildId = resp.ok
|
|
269
|
-
? resp.data?.build_uuid
|
|
270
|
-
: undefined;
|
|
271
|
-
if (!buildId) {
|
|
272
|
-
throw new Error(`Automate session ${sessionId} is not linked to a BrowserStack SDK build (it was not run through the SDK, or belongs to another account), so it has no accessibility results`);
|
|
273
|
-
}
|
|
274
|
-
const build = await apiClient.get({
|
|
275
|
-
url: `${API_BASE}/automated-tests/v1/builds/${encodeURIComponent(buildId)}`,
|
|
276
|
-
headers: this.authHeaders(),
|
|
277
|
-
raise_error: false,
|
|
278
|
-
});
|
|
279
|
-
if (build.status === 404) {
|
|
280
|
-
throw new Error(`This Automate run belongs to build ${buildId}, which has no accessibility results: accessibility testing was not on for it`);
|
|
281
|
-
}
|
|
282
|
-
if (!build.ok) {
|
|
283
|
-
throw new Error(`Could not read accessibility build ${buildId} of this Automate run: HTTP ${build.status}`);
|
|
284
|
-
}
|
|
285
|
-
return { kind: "build", buildId };
|
|
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");
|
|
286
99
|
}
|
|
287
100
|
/** Walk the newest scans for the one whose own link carries this thBuildId. */
|
|
288
101
|
async findScanRun(thBuildId, scanName) {
|
|
@@ -317,7 +130,7 @@ export class AccessibilityReportFetcher {
|
|
|
317
130
|
nextPage = data.pagination.nextPage;
|
|
318
131
|
}
|
|
319
132
|
if (sameName !== undefined) {
|
|
320
|
-
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}
|
|
133
|
+
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}`);
|
|
321
134
|
}
|
|
322
135
|
// nextPage is still set only when the walk stopped at the page cap, not at the end.
|
|
323
136
|
const searched = nextPage
|
|
@@ -325,89 +138,44 @@ export class AccessibilityReportFetcher {
|
|
|
325
138
|
: "";
|
|
326
139
|
throw new Error(`No Website Scanner scan${searched} matches this link. Pass scanId and scanRunId instead`);
|
|
327
140
|
}
|
|
328
|
-
|
|
329
|
-
* The CSV link of a report's issues. A finished export is reused until its link expires.
|
|
330
|
-
* Otherwise an export is started, or continued (`exportTaskId`, or the one an earlier call
|
|
331
|
-
* left running), and waited on for up to EXPORT_WAIT_MS; one still running then is handed
|
|
332
|
-
* back as an ExportPendingError carrying its task id.
|
|
333
|
-
*/
|
|
334
|
-
async getReportLink(source, options = {}) {
|
|
335
|
-
const key = `${this.auth?.username ?? ""}|${JSON.stringify(source)}`;
|
|
336
|
-
const known = reportExports.get(key);
|
|
337
|
-
if (known?.link && (known.expiresAt ?? 0) > Date.now())
|
|
338
|
-
return known.link;
|
|
141
|
+
async getReportLink(source) {
|
|
339
142
|
const urls = exportUrls(source);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
for (let attempt =
|
|
143
|
+
// Initiate CSV link generation
|
|
144
|
+
const initResp = await apiClient.get({
|
|
145
|
+
url: urls.start,
|
|
146
|
+
headers: this.authHeaders(),
|
|
147
|
+
raise_error: false,
|
|
148
|
+
});
|
|
149
|
+
const initData = initResp.data;
|
|
150
|
+
if (!initResp.ok || !initData?.success) {
|
|
151
|
+
throw new Error(startFailure(source, initResp.status, initData));
|
|
152
|
+
}
|
|
153
|
+
const taskId = initData.data.task_id;
|
|
154
|
+
// Poll for the generated CSV link (task is async, may take a few seconds)
|
|
155
|
+
const reportUrl = urls.poll(taskId);
|
|
156
|
+
const maxAttempts = 3;
|
|
157
|
+
const pollIntervalMs = 2000;
|
|
158
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
356
159
|
const reportResp = await apiClient.get({
|
|
357
|
-
url:
|
|
160
|
+
url: reportUrl,
|
|
358
161
|
headers: this.authHeaders(),
|
|
359
162
|
});
|
|
360
163
|
const reportData = reportResp.data;
|
|
361
164
|
if (!reportData.success) {
|
|
362
|
-
reportExports.delete(key);
|
|
363
165
|
throw new Error(`Failed to fetch report: ${reportData.error}`);
|
|
364
166
|
}
|
|
365
167
|
const link = reportData.data?.reportLink;
|
|
366
168
|
if (link) {
|
|
367
|
-
reportExports.set(key, { link, expiresAt: linkExpiry(link) });
|
|
368
169
|
return link;
|
|
369
170
|
}
|
|
370
|
-
|
|
371
|
-
if (/generation failed/i.test(message)) {
|
|
372
|
-
reportExports.delete(key);
|
|
373
|
-
throw new Error(`The report's CSV export failed: ${message}`);
|
|
374
|
-
}
|
|
375
|
-
const waited = Date.now() - started;
|
|
376
|
-
if (waited >= EXPORT_WAIT_MS)
|
|
377
|
-
throw new ExportPendingError(taskId, waited);
|
|
378
|
-
await options.onWait?.(waited);
|
|
379
|
-
const pause = EXPORT_POLL_MS[Math.min(attempt, EXPORT_POLL_MS.length - 1)];
|
|
380
|
-
await new Promise((resolve) => setTimeout(resolve, pause));
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
381
172
|
}
|
|
382
|
-
|
|
383
|
-
/** The run before `scanRunId` in its scan's history, or undefined for the first run. */
|
|
384
|
-
async previousScanRun(scanId, scanRunId) {
|
|
385
|
-
// The runs come a page at a time; the one before may be on a later page.
|
|
386
|
-
const runs = [];
|
|
387
|
-
const base = `${API_BASE}/website-scanner/v1/scans/${encodeURIComponent(scanId)}/scan_runs`;
|
|
388
|
-
let nextPage;
|
|
389
|
-
for (let page = 0; page < MAX_SCAN_RUN_PAGES; page++) {
|
|
390
|
-
const data = await this.getData(nextPage ? `${base}?nextPage=${encodeURIComponent(nextPage)}` : base);
|
|
391
|
-
runs.push(...(data?.scanRuns ?? []));
|
|
392
|
-
if (!data?.pagination?.hasNext || !data.pagination.nextPage)
|
|
393
|
-
break;
|
|
394
|
-
nextPage = data.pagination.nextPage;
|
|
395
|
-
}
|
|
396
|
-
const newestFirst = [...runs].sort((a, b) => b.scanDate.localeCompare(a.scanDate));
|
|
397
|
-
const at = newestFirst.findIndex((run) => String(run.id) === scanRunId);
|
|
398
|
-
return at >= 0 ? newestFirst[at + 1] : undefined;
|
|
399
|
-
}
|
|
400
|
-
/** The dashboard's new / retained / resolved counts against the run before this one. */
|
|
401
|
-
async changesSinceLastRun(scanId, scanRunId) {
|
|
402
|
-
const q = encodeURIComponent;
|
|
403
|
-
const changes = (await this.getData(`${API_BASE}/website-scanner/v1/scans/${q(scanId)}/scan_runs/${q(scanRunId)}`))?.changesSinceLastRun;
|
|
404
|
-
return changes ?? undefined;
|
|
173
|
+
throw new Error(`Report link was not available after ${maxAttempts} attempts. The CSV generation may still be in progress.`);
|
|
405
174
|
}
|
|
406
175
|
/**
|
|
407
176
|
* Where the report is in its processing, as the report's own status reads it: `completed`,
|
|
408
|
-
* or `queued` / `running` / `processing` / `finalizing` while issues are still being added
|
|
409
|
-
*
|
|
410
|
-
* when it cannot be read. A web build's overview carries no status, so a build's comes
|
|
177
|
+
* or `queued` / `running` / `processing` / `finalizing` while issues are still being added.
|
|
178
|
+
* Undefined when it cannot be read. A build's overview carries no status, so a build's comes
|
|
411
179
|
* from its project's build list (newest first), found by id.
|
|
412
180
|
*/
|
|
413
181
|
async reportStatus(source) {
|
|
@@ -420,12 +188,6 @@ export class AccessibilityReportFetcher {
|
|
|
420
188
|
return (await this.getData(`${API_BASE}/workflow-analyzer/v1/reports/${q(source.reportId)}`))?.summary?.status;
|
|
421
189
|
case "build":
|
|
422
190
|
return await this.buildStatus(source.buildId);
|
|
423
|
-
case "appBuild":
|
|
424
|
-
return (await this.app.buildMeta(source.buildId))?.status;
|
|
425
|
-
case "appReport": {
|
|
426
|
-
const detail = await this.app.manualReport(source.reportId);
|
|
427
|
-
return detail ? manualReportStatus(detail) : undefined;
|
|
428
|
-
}
|
|
429
191
|
}
|
|
430
192
|
}
|
|
431
193
|
catch {
|
|
@@ -434,8 +196,7 @@ export class AccessibilityReportFetcher {
|
|
|
434
196
|
}
|
|
435
197
|
async buildStatus(buildId) {
|
|
436
198
|
const q = encodeURIComponent;
|
|
437
|
-
const project = this.
|
|
438
|
-
(await this.getData(`${API_BASE}/automated-tests/v1/builds/${q(buildId)}`))?.projectName;
|
|
199
|
+
const project = (await this.getData(`${API_BASE}/automated-tests/v1/builds/${q(buildId)}`))?.projectName;
|
|
439
200
|
if (!project)
|
|
440
201
|
return undefined;
|
|
441
202
|
let nextPage;
|