@browserstack/mcp-server 1.2.4 → 1.2.5
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/README.md +9 -5
- package/dist/lib/apiClient.d.ts +7 -5
- package/dist/lib/apiClient.js +76 -15
- package/dist/lib/device-cache.d.ts +3 -1
- package/dist/lib/device-cache.js +4 -0
- package/dist/lib/instrumentation.js +6 -3
- package/dist/lib/utils.d.ts +75 -2
- package/dist/lib/utils.js +20 -0
- package/dist/lib/version-resolver.js +26 -14
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +7 -1
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +46 -26
- package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -1
- package/dist/tools/appautomate-utils/appium-sdk/constants.js +24 -3
- package/dist/tools/appautomate-utils/appium-sdk/handler.js +16 -2
- package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +2 -0
- package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +63 -29
- package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -1
- package/dist/tools/appautomate-utils/appium-sdk/types.js +10 -1
- package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
- package/dist/tools/appautomate-utils/native-execution/constants.js +24 -2
- package/dist/tools/appautomate.js +15 -2
- package/dist/tools/percy-sdk.js +30 -3
- package/dist/tools/run-percy-scan.js +1 -1
- package/dist/tools/sdk-utils/bstack/configUtils.d.ts +7 -4
- package/dist/tools/sdk-utils/bstack/configUtils.js +67 -20
- package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +1 -1
- package/dist/tools/sdk-utils/bstack/sdkHandler.js +10 -2
- package/dist/tools/sdk-utils/common/constants.d.ts +5 -4
- package/dist/tools/sdk-utils/common/constants.js +7 -6
- package/dist/tools/sdk-utils/common/device-validator.d.ts +25 -0
- package/dist/tools/sdk-utils/common/device-validator.js +368 -0
- package/dist/tools/sdk-utils/common/schema.d.ts +24 -4
- package/dist/tools/sdk-utils/common/schema.js +57 -3
- package/dist/tools/sdk-utils/common/utils.js +2 -1
- package/dist/tools/sdk-utils/handler.d.ts +1 -0
- package/dist/tools/sdk-utils/handler.js +40 -12
- package/dist/tools/sdk-utils/percy-bstack/handler.js +5 -1
- package/dist/tools/sdk-utils/percy-web/handler.js +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,9 +35,12 @@ Manage, execute, debug tests, and even fix code using plain English prompts.
|
|
|
35
35
|
#### Reduced context switching:
|
|
36
36
|
Stay in flow—keep all project context in one place and trigger actions directly from your IDE or LLM.
|
|
37
37
|
|
|
38
|
-
## ⚡️ One Click MCP Setup
|
|
38
|
+
## ⚡️ One Click MCP Setup
|
|
39
|
+
|
|
40
|
+
Click on the buttons below to install MCP in your respective IDE:
|
|
41
|
+
|
|
42
|
+
<a href="http://mcp.browserstack.com/one-click-setup?client=vscode"><img src="assets/one-click-vs-code.png" alt="Install in VS Code" width="160" height="80"></a> <a href="http://mcp.browserstack.com/one-click-setup?client=cursor"><img src="assets/one-click-cursor.png" alt="Install in Cursor" width="150" height="70"></a>
|
|
39
43
|
|
|
40
|
-
[](http://mcp.browserstack.com/one-click-setup?client=vscode) [](http://mcp.browserstack.com/one-click-setup?client=cursor)
|
|
41
44
|
#### Note : Ensure you are using Node version >= `18.0`
|
|
42
45
|
- Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
|
|
43
46
|
- To Upgrade Node :
|
|
@@ -156,8 +159,9 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and
|
|
|
156
159
|
|
|
157
160
|
### **One Click MCP Setup**
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
Click on the buttons below to install MCP in your respective IDE:
|
|
160
163
|
|
|
164
|
+
<a href="http://mcp.browserstack.com/one-click-setup?client=vscode"><img src="assets/one-click-vs-code.png" alt="Install in VS Code" width="160" height="80"></a> <a href="http://mcp.browserstack.com/one-click-setup?client=cursor"><img src="assets/one-click-cursor.png" alt="Install in Cursor" width="150" height="70"></a>
|
|
161
165
|
|
|
162
166
|
### **Alternate ways to Setup MCP server**
|
|
163
167
|
|
|
@@ -418,14 +422,14 @@ As of now we support 20 tools.
|
|
|
418
422
|
**Prompt example**
|
|
419
423
|
|
|
420
424
|
```text
|
|
421
|
-
Take a screenshot of my app on Google Pixel 6 with Android
|
|
425
|
+
Take a screenshot of my app on Google Pixel 6 with Android 12 while testing on App Automate. App file path: /Users/xyz/app-debug.apk
|
|
422
426
|
```
|
|
423
427
|
|
|
424
428
|
15. `runAppTestsOnBrowserStack` — Run automated mobile tests (Espresso/XCUITest, etc.) on real devices.
|
|
425
429
|
**Prompt example**
|
|
426
430
|
|
|
427
431
|
```text
|
|
428
|
-
Run Espresso tests from /tests/checkout.zip on Galaxy S21 and Pixel 6 with Android
|
|
432
|
+
Run Espresso tests from /tests/checkout.zip on Galaxy S21 and Pixel 6 with Android 12. App path is /apps/beta-release.apk under project 'Checkout Flow'
|
|
429
433
|
```
|
|
430
434
|
|
|
431
435
|
---
|
package/dist/lib/apiClient.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ type RequestOptions = {
|
|
|
4
4
|
headers?: Record<string, string>;
|
|
5
5
|
params?: Record<string, string | number>;
|
|
6
6
|
body?: any;
|
|
7
|
+
timeout?: number;
|
|
7
8
|
raise_error?: boolean;
|
|
8
9
|
};
|
|
9
10
|
declare class ApiResponse<T = any> {
|
|
@@ -20,12 +21,13 @@ declare class ApiResponse<T = any> {
|
|
|
20
21
|
declare class ApiClient {
|
|
21
22
|
private instance;
|
|
22
23
|
private get axiosAgent();
|
|
24
|
+
private validateUrl;
|
|
23
25
|
private requestWrapper;
|
|
24
|
-
get<T = any>({ url, headers, params, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
25
|
-
post<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
26
|
-
put<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
27
|
-
patch<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
28
|
-
delete<T = any>({ url, headers, params, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
26
|
+
get<T = any>({ url, headers, params, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
27
|
+
post<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
28
|
+
put<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
29
|
+
patch<T = any>({ url, headers, body, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
30
|
+
delete<T = any>({ url, headers, params, timeout, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
29
31
|
}
|
|
30
32
|
export declare const apiClient: ApiClient;
|
|
31
33
|
export type { ApiResponse, RequestOptions };
|
package/dist/lib/apiClient.js
CHANGED
|
@@ -4,6 +4,7 @@ const { HttpsProxyAgent } = httpsProxyAgentPkg;
|
|
|
4
4
|
import * as https from "https";
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import config from "../config.js";
|
|
7
|
+
import { isDataUrlPayloadTooLarge } from "../lib/utils.js";
|
|
7
8
|
class ApiResponse {
|
|
8
9
|
_response;
|
|
9
10
|
constructor(response) {
|
|
@@ -77,8 +78,38 @@ class ApiClient {
|
|
|
77
78
|
get axiosAgent() {
|
|
78
79
|
return getAxiosAgent();
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
validateUrl(url, options) {
|
|
81
82
|
try {
|
|
83
|
+
const parsedUrl = new URL(url);
|
|
84
|
+
// Default safe limits
|
|
85
|
+
const maxContentLength = options?.maxContentLength ?? 20 * 1024 * 1024; // 20MB
|
|
86
|
+
const maxBodyLength = options?.maxBodyLength ?? 20 * 1024 * 1024; // 20MB
|
|
87
|
+
const maxUrlLength = 8000; // cutoff for URLs
|
|
88
|
+
// Check overall URL length
|
|
89
|
+
if (url.length > maxUrlLength) {
|
|
90
|
+
throw new Error(`URL length exceeds maxUrlLength (${maxUrlLength} chars)`);
|
|
91
|
+
}
|
|
92
|
+
if (parsedUrl.protocol === "data:") {
|
|
93
|
+
// Either reject completely OR check payload size
|
|
94
|
+
if (isDataUrlPayloadTooLarge(url, maxContentLength)) {
|
|
95
|
+
throw new Error("data: URI payload too large or invalid");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
99
|
+
throw new Error(`Unsupported URL scheme: ${parsedUrl.protocol}`);
|
|
100
|
+
}
|
|
101
|
+
if (options?.data &&
|
|
102
|
+
Buffer.byteLength(JSON.stringify(options.data), "utf8") > maxBodyLength) {
|
|
103
|
+
throw new Error(`Request body exceeds maxBodyLength (${maxBodyLength} bytes)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`Invalid URL: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async requestWrapper(fn, url, config, raise_error = true) {
|
|
111
|
+
try {
|
|
112
|
+
this.validateUrl(url, config);
|
|
82
113
|
const res = await fn(this.axiosAgent);
|
|
83
114
|
return new ApiResponse(res);
|
|
84
115
|
}
|
|
@@ -89,20 +120,50 @@ class ApiClient {
|
|
|
89
120
|
throw error;
|
|
90
121
|
}
|
|
91
122
|
}
|
|
92
|
-
async get({ url, headers, params, raise_error = true, }) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return this.requestWrapper((
|
|
100
|
-
}
|
|
101
|
-
async
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
async get({ url, headers, params, timeout, raise_error = true, }) {
|
|
124
|
+
const config = {
|
|
125
|
+
headers,
|
|
126
|
+
params,
|
|
127
|
+
timeout,
|
|
128
|
+
httpsAgent: this.axiosAgent,
|
|
129
|
+
};
|
|
130
|
+
return this.requestWrapper(() => this.instance.get(url, config), url, config, raise_error);
|
|
131
|
+
}
|
|
132
|
+
async post({ url, headers, body, timeout, raise_error = true, }) {
|
|
133
|
+
const config = {
|
|
134
|
+
headers,
|
|
135
|
+
timeout,
|
|
136
|
+
httpsAgent: this.axiosAgent,
|
|
137
|
+
data: body,
|
|
138
|
+
};
|
|
139
|
+
return this.requestWrapper(() => this.instance.post(url, config.data, config), url, config, raise_error);
|
|
140
|
+
}
|
|
141
|
+
async put({ url, headers, body, timeout, raise_error = true, }) {
|
|
142
|
+
const config = {
|
|
143
|
+
headers,
|
|
144
|
+
timeout,
|
|
145
|
+
httpsAgent: this.axiosAgent,
|
|
146
|
+
data: body,
|
|
147
|
+
};
|
|
148
|
+
return this.requestWrapper(() => this.instance.put(url, config.data, config), url, config, raise_error);
|
|
149
|
+
}
|
|
150
|
+
async patch({ url, headers, body, timeout, raise_error = true, }) {
|
|
151
|
+
const config = {
|
|
152
|
+
headers,
|
|
153
|
+
timeout,
|
|
154
|
+
httpsAgent: this.axiosAgent,
|
|
155
|
+
data: body,
|
|
156
|
+
};
|
|
157
|
+
return this.requestWrapper(() => this.instance.patch(url, config.data, config), url, config, raise_error);
|
|
158
|
+
}
|
|
159
|
+
async delete({ url, headers, params, timeout, raise_error = true, }) {
|
|
160
|
+
const config = {
|
|
161
|
+
headers,
|
|
162
|
+
params,
|
|
163
|
+
timeout,
|
|
164
|
+
httpsAgent: this.axiosAgent,
|
|
165
|
+
};
|
|
166
|
+
return this.requestWrapper(() => this.instance.delete(url, config), url, config, raise_error);
|
|
106
167
|
}
|
|
107
168
|
}
|
|
108
169
|
export const apiClient = new ApiClient();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare enum BrowserStackProducts {
|
|
2
2
|
LIVE = "live",
|
|
3
3
|
APP_LIVE = "app_live",
|
|
4
|
-
APP_AUTOMATE = "app_automate"
|
|
4
|
+
APP_AUTOMATE = "app_automate",
|
|
5
|
+
SELENIUM_AUTOMATE = "selenium_automate",
|
|
6
|
+
PLAYWRIGHT_AUTOMATE = "playwright_automate"
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
|
package/dist/lib/device-cache.js
CHANGED
|
@@ -12,11 +12,15 @@ export var BrowserStackProducts;
|
|
|
12
12
|
BrowserStackProducts["LIVE"] = "live";
|
|
13
13
|
BrowserStackProducts["APP_LIVE"] = "app_live";
|
|
14
14
|
BrowserStackProducts["APP_AUTOMATE"] = "app_automate";
|
|
15
|
+
BrowserStackProducts["SELENIUM_AUTOMATE"] = "selenium_automate";
|
|
16
|
+
BrowserStackProducts["PLAYWRIGHT_AUTOMATE"] = "playwright_automate";
|
|
15
17
|
})(BrowserStackProducts || (BrowserStackProducts = {}));
|
|
16
18
|
const URLS = {
|
|
17
19
|
[BrowserStackProducts.LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
|
|
18
20
|
[BrowserStackProducts.APP_LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
|
|
19
21
|
[BrowserStackProducts.APP_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_automate.json",
|
|
22
|
+
[BrowserStackProducts.SELENIUM_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/automate.json",
|
|
23
|
+
[BrowserStackProducts.PLAYWRIGHT_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/playwright.json",
|
|
20
24
|
};
|
|
21
25
|
/**
|
|
22
26
|
* Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
|
|
@@ -3,7 +3,7 @@ import { getBrowserStackAuth } from "./get-auth.js";
|
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const packageJson = require("../../package.json");
|
|
6
|
-
import
|
|
6
|
+
import { apiClient } from "./apiClient.js";
|
|
7
7
|
import globalConfig from "../config.js";
|
|
8
8
|
export function trackMCP(toolName, clientInfo, error, config) {
|
|
9
9
|
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
|
|
@@ -38,13 +38,16 @@ export function trackMCP(toolName, clientInfo, error, config) {
|
|
|
38
38
|
const authString = getBrowserStackAuth(config);
|
|
39
39
|
authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
.post(
|
|
41
|
+
apiClient
|
|
42
|
+
.post({
|
|
43
|
+
url: instrumentationEndpoint,
|
|
44
|
+
body: event,
|
|
43
45
|
headers: {
|
|
44
46
|
"Content-Type": "application/json",
|
|
45
47
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
46
48
|
},
|
|
47
49
|
timeout: 2000,
|
|
50
|
+
raise_error: false,
|
|
48
51
|
})
|
|
49
52
|
.catch(() => { });
|
|
50
53
|
}
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,9 +1,82 @@
|
|
|
1
1
|
import type { ApiResponse } from "./apiClient.js";
|
|
2
2
|
import { BrowserStackConfig } from "./types.js";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
4
|
export declare function sanitizeUrlParam(param: string): string;
|
|
6
5
|
export declare function maybeCompressBase64(base64: string): Promise<string>;
|
|
7
6
|
export declare function assertOkResponse(response: Response | ApiResponse, action: string): Promise<void>;
|
|
8
7
|
export declare function fetchFromBrowserStackAPI(url: string, config: BrowserStackConfig): Promise<any>;
|
|
9
|
-
export declare function handleMCPError(toolName: string, server: McpServer, config: BrowserStackConfig, error: unknown):
|
|
8
|
+
export declare function handleMCPError(toolName: string, server: McpServer, config: BrowserStackConfig, error: unknown): {
|
|
9
|
+
[x: string]: unknown;
|
|
10
|
+
content: ({
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
_meta?: {
|
|
15
|
+
[x: string]: unknown;
|
|
16
|
+
} | undefined;
|
|
17
|
+
} | {
|
|
18
|
+
[x: string]: unknown;
|
|
19
|
+
type: "image";
|
|
20
|
+
data: string;
|
|
21
|
+
mimeType: string;
|
|
22
|
+
_meta?: {
|
|
23
|
+
[x: string]: unknown;
|
|
24
|
+
} | undefined;
|
|
25
|
+
} | {
|
|
26
|
+
[x: string]: unknown;
|
|
27
|
+
type: "audio";
|
|
28
|
+
data: string;
|
|
29
|
+
mimeType: string;
|
|
30
|
+
_meta?: {
|
|
31
|
+
[x: string]: unknown;
|
|
32
|
+
} | undefined;
|
|
33
|
+
} | {
|
|
34
|
+
[x: string]: unknown;
|
|
35
|
+
type: "resource_link";
|
|
36
|
+
name: string;
|
|
37
|
+
uri: string;
|
|
38
|
+
_meta?: {
|
|
39
|
+
[x: string]: unknown;
|
|
40
|
+
} | undefined;
|
|
41
|
+
mimeType?: string | undefined;
|
|
42
|
+
title?: string | undefined;
|
|
43
|
+
description?: string | undefined;
|
|
44
|
+
icons?: {
|
|
45
|
+
[x: string]: unknown;
|
|
46
|
+
src: string;
|
|
47
|
+
mimeType?: string | undefined;
|
|
48
|
+
sizes?: string | undefined;
|
|
49
|
+
}[] | undefined;
|
|
50
|
+
} | {
|
|
51
|
+
[x: string]: unknown;
|
|
52
|
+
type: "resource";
|
|
53
|
+
resource: {
|
|
54
|
+
[x: string]: unknown;
|
|
55
|
+
text: string;
|
|
56
|
+
uri: string;
|
|
57
|
+
_meta?: {
|
|
58
|
+
[x: string]: unknown;
|
|
59
|
+
} | undefined;
|
|
60
|
+
mimeType?: string | undefined;
|
|
61
|
+
} | {
|
|
62
|
+
[x: string]: unknown;
|
|
63
|
+
uri: string;
|
|
64
|
+
blob: string;
|
|
65
|
+
_meta?: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
} | undefined;
|
|
68
|
+
mimeType?: string | undefined;
|
|
69
|
+
};
|
|
70
|
+
_meta?: {
|
|
71
|
+
[x: string]: unknown;
|
|
72
|
+
} | undefined;
|
|
73
|
+
})[];
|
|
74
|
+
_meta?: {
|
|
75
|
+
[x: string]: unknown;
|
|
76
|
+
} | undefined;
|
|
77
|
+
structuredContent?: {
|
|
78
|
+
[x: string]: unknown;
|
|
79
|
+
} | undefined;
|
|
80
|
+
isError?: boolean | undefined;
|
|
81
|
+
};
|
|
82
|
+
export declare function isDataUrlPayloadTooLarge(dataUrl: string, maxBytes: number): boolean;
|
package/dist/lib/utils.js
CHANGED
|
@@ -51,3 +51,23 @@ export function handleMCPError(toolName, server, config, error) {
|
|
|
51
51
|
const readableToolName = toolName.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
52
52
|
return errorContent(`Failed to ${readableToolName}: ${errorMessage}. Please open an issue on GitHub if the problem persists`);
|
|
53
53
|
}
|
|
54
|
+
export function isDataUrlPayloadTooLarge(dataUrl, maxBytes) {
|
|
55
|
+
const commaIndex = dataUrl.indexOf(",");
|
|
56
|
+
if (commaIndex === -1)
|
|
57
|
+
return true; // malformed
|
|
58
|
+
const meta = dataUrl.slice(0, commaIndex);
|
|
59
|
+
const payload = dataUrl.slice(commaIndex + 1);
|
|
60
|
+
const isBase64 = /;base64$/i.test(meta);
|
|
61
|
+
if (!isBase64) {
|
|
62
|
+
try {
|
|
63
|
+
const decoded = decodeURIComponent(payload);
|
|
64
|
+
return Buffer.byteLength(decoded, "utf8") > maxBytes;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
|
|
71
|
+
const decodedBytes = Math.floor((payload.length * 3) / 4) - padding;
|
|
72
|
+
return decodedBytes > maxBytes;
|
|
73
|
+
}
|
|
@@ -20,26 +20,38 @@ export function resolveVersion(requested, available) {
|
|
|
20
20
|
const lex = uniq.slice().sort();
|
|
21
21
|
return requested === "latest" ? lex[lex.length - 1] : lex[0];
|
|
22
22
|
}
|
|
23
|
-
// exact?
|
|
23
|
+
// exact match?
|
|
24
24
|
if (uniq.includes(requested)) {
|
|
25
25
|
return requested;
|
|
26
26
|
}
|
|
27
|
-
//
|
|
27
|
+
// Try major version matching (e.g., "14" matches "14.0", "14.1", etc.)
|
|
28
28
|
const reqNum = parseFloat(requested);
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
if (!isNaN(reqNum)) {
|
|
30
|
+
const majorVersionMatches = uniq.filter((v) => {
|
|
31
|
+
const vNum = parseFloat(v);
|
|
32
|
+
return !isNaN(vNum) && Math.floor(vNum) === Math.floor(reqNum);
|
|
33
|
+
});
|
|
34
|
+
if (majorVersionMatches.length > 0) {
|
|
35
|
+
// If multiple matches, prefer the most common format or latest
|
|
36
|
+
const exactMatch = majorVersionMatches.find((v) => v === `${Math.floor(reqNum)}.0`);
|
|
37
|
+
if (exactMatch) {
|
|
38
|
+
return exactMatch;
|
|
39
39
|
}
|
|
40
|
+
// Return the first match (usually the most common format)
|
|
41
|
+
return majorVersionMatches[0];
|
|
40
42
|
}
|
|
41
|
-
return best.v;
|
|
42
43
|
}
|
|
43
|
-
//
|
|
44
|
+
// Fuzzy matching: find the closest version
|
|
45
|
+
const reqNumForFuzzy = parseFloat(requested);
|
|
46
|
+
if (!isNaN(reqNumForFuzzy)) {
|
|
47
|
+
const numericVersions = uniq
|
|
48
|
+
.map((v) => ({ v, n: parseFloat(v) }))
|
|
49
|
+
.filter((x) => !isNaN(x.n))
|
|
50
|
+
.sort((a, b) => Math.abs(a.n - reqNumForFuzzy) - Math.abs(b.n - reqNumForFuzzy));
|
|
51
|
+
if (numericVersions.length > 0) {
|
|
52
|
+
return numericVersions[0].v;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Fallback: return the first available version
|
|
44
56
|
return uniq[0];
|
|
45
57
|
}
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { ValidatedEnvironment } from "../../sdk-utils/common/device-validator.js";
|
|
2
|
+
export declare function generateAppBrowserStackYMLInstructions(config: {
|
|
3
|
+
validatedEnvironments?: ValidatedEnvironment[];
|
|
4
|
+
platforms?: string[];
|
|
5
|
+
testingFramework?: string;
|
|
6
|
+
projectName?: string;
|
|
7
|
+
}, username: string, accessKey: string, appPath?: string): string;
|
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
// Configuration utilities for BrowserStack App SDK
|
|
2
1
|
import { APP_DEVICE_CONFIGS, AppSDKSupportedTestingFrameworkEnum, DEFAULT_APP_PATH, createStep, } from "./index.js";
|
|
3
|
-
export function generateAppBrowserStackYMLInstructions(
|
|
4
|
-
if (testingFramework ===
|
|
5
|
-
|
|
6
|
-
testingFramework ===
|
|
2
|
+
export function generateAppBrowserStackYMLInstructions(config, username, accessKey, appPath = DEFAULT_APP_PATH) {
|
|
3
|
+
if (config.testingFramework ===
|
|
4
|
+
AppSDKSupportedTestingFrameworkEnum.nightwatch ||
|
|
5
|
+
config.testingFramework ===
|
|
6
|
+
AppSDKSupportedTestingFrameworkEnum.webdriverio ||
|
|
7
|
+
config.testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby) {
|
|
7
8
|
return "";
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return "";
|
|
15
|
-
return devices
|
|
16
|
-
.map((device) => ` - platformName: ${platform}
|
|
17
|
-
deviceName: ${device.deviceName}
|
|
18
|
-
platformVersion: "${device.platformVersion}"`)
|
|
19
|
-
.join("\n");
|
|
20
|
-
})
|
|
21
|
-
.filter(Boolean)
|
|
22
|
-
.join("\n");
|
|
23
|
-
// Construct YAML content
|
|
10
|
+
const platformConfigs = generatePlatformConfigs(config);
|
|
11
|
+
const projectName = config.projectName || "BrowserStack Sample";
|
|
12
|
+
const buildName = config.projectName
|
|
13
|
+
? `${config.projectName}-AppAutomate-Build`
|
|
14
|
+
: "bstack-demo";
|
|
24
15
|
const configContent = `\`\`\`yaml
|
|
25
16
|
userName: ${username}
|
|
26
17
|
accessKey: ${accessKey}
|
|
@@ -29,8 +20,9 @@ platforms:
|
|
|
29
20
|
${platformConfigs}
|
|
30
21
|
parallelsPerPlatform: 1
|
|
31
22
|
browserstackLocal: true
|
|
32
|
-
|
|
33
|
-
projectName:
|
|
23
|
+
// TODO: replace projectName and buildName according to actual project
|
|
24
|
+
projectName: ${projectName}
|
|
25
|
+
buildName: ${buildName}
|
|
34
26
|
debug: true
|
|
35
27
|
networkLogs: true
|
|
36
28
|
percy: false
|
|
@@ -43,8 +35,36 @@ accessibility: false
|
|
|
43
35
|
- You can upload your app using BrowserStack's App Upload API or manually through the dashboard
|
|
44
36
|
- Set \`browserstackLocal: true\` if you need to test with local/staging servers
|
|
45
37
|
- Adjust \`parallelsPerPlatform\` based on your subscription limits`;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
const stepTitle = "Update browserstack.yml file with App Automate configuration:";
|
|
39
|
+
const stepDescription = `Create or update the browserstack.yml file in your project root with the following content:
|
|
40
|
+
${configContent}`;
|
|
41
|
+
return createStep(stepTitle, stepDescription);
|
|
42
|
+
}
|
|
43
|
+
function generatePlatformConfigs(config) {
|
|
44
|
+
if (config.validatedEnvironments && config.validatedEnvironments.length > 0) {
|
|
45
|
+
return config.validatedEnvironments
|
|
46
|
+
.filter((env) => env.platform === "android" || env.platform === "ios")
|
|
47
|
+
.map((env) => {
|
|
48
|
+
return ` - platformName: ${env.platform}
|
|
49
|
+
deviceName: "${env.deviceName}"
|
|
50
|
+
platformVersion: "${env.osVersion}"`;
|
|
51
|
+
})
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
else if (config.platforms && config.platforms.length > 0) {
|
|
55
|
+
return config.platforms
|
|
56
|
+
.map((platform) => {
|
|
57
|
+
const devices = APP_DEVICE_CONFIGS[platform];
|
|
58
|
+
if (!devices)
|
|
59
|
+
return "";
|
|
60
|
+
return devices
|
|
61
|
+
.map((device) => ` - platformName: ${platform}
|
|
62
|
+
deviceName: ${device.deviceName}
|
|
63
|
+
platformVersion: "${device.platformVersion}"`)
|
|
64
|
+
.join("\n");
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join("\n");
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
50
70
|
}
|
|
@@ -17,7 +17,7 @@ export declare const SETUP_APP_AUTOMATE_SCHEMA: {
|
|
|
17
17
|
detectedFramework: z.ZodNativeEnum<typeof AppSDKSupportedFrameworkEnum>;
|
|
18
18
|
detectedTestingFramework: z.ZodNativeEnum<typeof AppSDKSupportedTestingFrameworkEnum>;
|
|
19
19
|
detectedLanguage: z.ZodNativeEnum<typeof AppSDKSupportedLanguageEnum>;
|
|
20
|
-
|
|
20
|
+
devices: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodTuple<[z.ZodLiteral<AppSDKSupportedPlatformEnum.android>, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<AppSDKSupportedPlatformEnum.ios>, z.ZodString, z.ZodString], null>]>, "many">>;
|
|
21
21
|
appPath: z.ZodString;
|
|
22
22
|
project: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
23
23
|
};
|
|
@@ -29,9 +29,30 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
|
|
|
29
29
|
detectedLanguage: z
|
|
30
30
|
.nativeEnum(AppSDKSupportedLanguageEnum)
|
|
31
31
|
.describe("The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'"),
|
|
32
|
-
|
|
33
|
-
.array(z.
|
|
34
|
-
|
|
32
|
+
devices: z
|
|
33
|
+
.array(z.union([
|
|
34
|
+
// Android: [android, deviceName, osVersion]
|
|
35
|
+
z.tuple([
|
|
36
|
+
z
|
|
37
|
+
.literal(AppSDKSupportedPlatformEnum.android)
|
|
38
|
+
.describe("Platform identifier: 'android'"),
|
|
39
|
+
z
|
|
40
|
+
.string()
|
|
41
|
+
.describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
|
|
42
|
+
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
|
|
43
|
+
]),
|
|
44
|
+
// iOS: [ios, deviceName, osVersion]
|
|
45
|
+
z.tuple([
|
|
46
|
+
z
|
|
47
|
+
.literal(AppSDKSupportedPlatformEnum.ios)
|
|
48
|
+
.describe("Platform identifier: 'ios'"),
|
|
49
|
+
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
|
|
50
|
+
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
|
|
51
|
+
]),
|
|
52
|
+
]))
|
|
53
|
+
.max(3)
|
|
54
|
+
.default([])
|
|
55
|
+
.describe("Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]"),
|
|
35
56
|
appPath: z
|
|
36
57
|
.string()
|
|
37
58
|
.describe("Path to the mobile app file (.apk for Android, .ipa for iOS). Can be a local file path or a BrowserStack app URL (bs://)"),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
|
|
3
|
+
import { validateAppAutomateDevices } from "../../sdk-utils/common/device-validator.js";
|
|
3
4
|
import { getAppUploadInstruction, validateSupportforAppAutomate, } from "./utils.js";
|
|
4
5
|
import { getAppSDKPrefixCommand, generateAppBrowserStackYMLInstructions, } from "./index.js";
|
|
5
6
|
import { formatAppInstructionsWithNumbers, getAppInstructionsForProjectConfiguration, SETUP_APP_AUTOMATE_SCHEMA, } from "./index.js";
|
|
@@ -11,18 +12,31 @@ export async function setupAppAutomateHandler(rawInput, config) {
|
|
|
11
12
|
// Use variables for all major input properties
|
|
12
13
|
const testingFramework = input.detectedTestingFramework;
|
|
13
14
|
const language = input.detectedLanguage;
|
|
14
|
-
const
|
|
15
|
+
const inputDevices = input.devices ?? [];
|
|
15
16
|
const appPath = input.appPath;
|
|
16
17
|
const framework = input.detectedFramework;
|
|
17
18
|
//Validating if supported framework or not
|
|
18
19
|
validateSupportforAppAutomate(framework, language, testingFramework);
|
|
20
|
+
// Use default mobile devices when array is empty
|
|
21
|
+
const devices = inputDevices.length === 0
|
|
22
|
+
? [["android", "Samsung Galaxy S24", "latest"]]
|
|
23
|
+
: inputDevices;
|
|
24
|
+
// Validate devices against real BrowserStack device data
|
|
25
|
+
const validatedEnvironments = await validateAppAutomateDevices(devices);
|
|
26
|
+
// Extract platforms for backward compatibility (if needed)
|
|
27
|
+
const platforms = validatedEnvironments.map((env) => env.platform);
|
|
19
28
|
// Step 1: Generate SDK setup command
|
|
20
29
|
const sdkCommand = getAppSDKPrefixCommand(language, testingFramework, username, accessKey, appPath);
|
|
21
30
|
if (sdkCommand) {
|
|
22
31
|
instructions.push({ content: sdkCommand, type: "setup" });
|
|
23
32
|
}
|
|
24
33
|
// Step 2: Generate browserstack.yml configuration
|
|
25
|
-
const configInstructions = generateAppBrowserStackYMLInstructions(
|
|
34
|
+
const configInstructions = generateAppBrowserStackYMLInstructions({
|
|
35
|
+
validatedEnvironments,
|
|
36
|
+
platforms,
|
|
37
|
+
testingFramework,
|
|
38
|
+
projectName: input.project,
|
|
39
|
+
}, username, accessKey, appPath);
|
|
26
40
|
if (configInstructions) {
|
|
27
41
|
instructions.push({ content: configInstructions, type: "config" });
|
|
28
42
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export declare const MAVEN_ARCHETYPE_GROUP_ID = "com.browserstack";
|
|
2
2
|
export declare const MAVEN_ARCHETYPE_ARTIFACT_ID = "junit-archetype-integrate";
|
|
3
3
|
export declare const MAVEN_ARCHETYPE_VERSION = "1.0";
|
|
4
|
+
export declare const JAVA_APP_FRAMEWORK_VERSION_MAP: Record<string, string>;
|
|
4
5
|
export declare const JAVA_APP_FRAMEWORK_MAP: Record<string, string>;
|
|
5
6
|
export declare const GRADLE_APP_SETUP_INSTRUCTIONS = "\n**For Gradle setup:**\n1. Add browserstack-java-sdk to dependencies:\n compileOnly 'com.browserstack:browserstack-java-sdk:latest.release'\n\n2. Add browserstackSDK path variable:\n def browserstackSDKArtifact = configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find { it.name == 'browserstack-java-sdk' }\n\n3. Add javaagent to gradle tasks:\n jvmArgs \"-javaagent:${browserstackSDKArtifact.file}\"\n";
|
|
6
7
|
export declare function getJavaAppInstructions(): string;
|
|
7
8
|
export declare function getJavaAppFrameworkForMaven(framework: string): string;
|
|
9
|
+
export declare function getJavaAppFrameworkVersion(framework: string): string;
|
|
8
10
|
export declare function getJavaSDKCommand(framework: string, username: string, accessKey: string, appPath?: string): string;
|