@browserstack/mcp-server 1.0.8 → 1.0.9
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/config.js +18 -0
- package/dist/index.js +45 -0
- package/dist/lib/api.js +70 -0
- package/dist/lib/error.js +26 -0
- package/dist/lib/fuzzy.js +51 -0
- package/dist/lib/local.js +109 -0
- package/dist/lib/utils.js +7 -0
- package/dist/logger.js +40 -0
- package/dist/tools/accessibility.js +43 -0
- package/dist/tools/accessiblity-utils/accessibility.js +82 -0
- package/dist/tools/applive-utils/constants.js +82 -0
- package/dist/tools/applive-utils/device-cache.js +33 -0
- package/dist/tools/applive-utils/fuzzy-search.js +11 -0
- package/dist/tools/applive-utils/start-session.js +159 -0
- package/dist/tools/applive-utils/types.js +2 -0
- package/dist/tools/applive-utils/upload-app.js +68 -0
- package/dist/tools/applive.js +88 -0
- package/dist/tools/automate.js +52 -0
- package/dist/tools/bstack-sdk.js +64 -0
- package/dist/tools/live-utils/start-session.js +58 -0
- package/dist/tools/live.js +97 -0
- package/dist/tools/observability.js +58 -0
- package/dist/tools/sdk-utils/constants.js +64 -0
- package/dist/tools/sdk-utils/instructions.js +64 -0
- package/dist/tools/sdk-utils/types.js +2 -0
- package/dist/tools/testmanagement-utils/create-project-folder.js +107 -0
- package/dist/tools/testmanagement-utils/create-testcase.js +115 -0
- package/dist/tools/testmanagement.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startSession = startSession;
|
|
7
|
+
const child_process_1 = __importDefault(require("child_process"));
|
|
8
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
+
const device_cache_1 = require("./device-cache");
|
|
10
|
+
const fuzzy_search_1 = require("./fuzzy-search");
|
|
11
|
+
const utils_1 = require("../../lib/utils");
|
|
12
|
+
const upload_app_1 = require("./upload-app");
|
|
13
|
+
/**
|
|
14
|
+
* Starts an App Live session after filtering, fuzzy matching, and launching.
|
|
15
|
+
* @param args - The arguments for starting the session.
|
|
16
|
+
* @returns The launch URL for the session.
|
|
17
|
+
* @throws Will throw an error if no devices are found or if the app URL is invalid.
|
|
18
|
+
*/
|
|
19
|
+
async function startSession(args) {
|
|
20
|
+
const { appPath, desiredPlatform, desiredPhone } = args;
|
|
21
|
+
let { desiredPlatformVersion } = args;
|
|
22
|
+
const data = await (0, device_cache_1.getAppLiveData)();
|
|
23
|
+
const allDevices = data.mobile.flatMap((group) => group.devices.map((dev) => ({ ...dev, os: group.os })));
|
|
24
|
+
desiredPlatformVersion = resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
25
|
+
const filteredDevices = filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
26
|
+
const matches = await (0, fuzzy_search_1.fuzzySearchDevices)(filteredDevices, desiredPhone);
|
|
27
|
+
const selectedDevice = validateAndSelectDevice(matches, desiredPhone, desiredPlatform, desiredPlatformVersion);
|
|
28
|
+
const { app_url } = await (0, upload_app_1.uploadApp)(appPath);
|
|
29
|
+
validateAppUrl(app_url);
|
|
30
|
+
const launchUrl = constructLaunchUrl(app_url, selectedDevice, desiredPlatform, desiredPlatformVersion);
|
|
31
|
+
openBrowser(launchUrl);
|
|
32
|
+
return launchUrl;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the platform version based on the desired platform and version.
|
|
36
|
+
* @param allDevices - The list of all devices.
|
|
37
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
38
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
39
|
+
* @returns The resolved platform version.
|
|
40
|
+
* @throws Will throw an error if the platform version is not valid.
|
|
41
|
+
*/
|
|
42
|
+
function resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion) {
|
|
43
|
+
if (desiredPlatformVersion === "latest" ||
|
|
44
|
+
desiredPlatformVersion === "oldest") {
|
|
45
|
+
const filtered = allDevices.filter((d) => d.os === desiredPlatform);
|
|
46
|
+
filtered.sort((a, b) => {
|
|
47
|
+
const versionA = parseFloat(a.os_version);
|
|
48
|
+
const versionB = parseFloat(b.os_version);
|
|
49
|
+
return desiredPlatformVersion === "latest"
|
|
50
|
+
? versionB - versionA
|
|
51
|
+
: versionA - versionB;
|
|
52
|
+
});
|
|
53
|
+
return filtered[0].os_version;
|
|
54
|
+
}
|
|
55
|
+
return desiredPlatformVersion;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Filters devices based on the desired platform and version.
|
|
59
|
+
* @param allDevices - The list of all devices.
|
|
60
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
61
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
62
|
+
* @returns The filtered list of devices.
|
|
63
|
+
* @throws Will throw an error if the platform version is not valid.
|
|
64
|
+
*/
|
|
65
|
+
function filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion) {
|
|
66
|
+
return allDevices.filter((d) => {
|
|
67
|
+
if (d.os !== desiredPlatform)
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
const versionA = parseFloat(d.os_version);
|
|
71
|
+
const versionB = parseFloat(desiredPlatformVersion);
|
|
72
|
+
return versionA === versionB;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return d.os_version === desiredPlatformVersion;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validates the selected device and handles multiple matches.
|
|
81
|
+
* @param matches - The list of device matches.
|
|
82
|
+
* @param desiredPhone - The desired phone name.
|
|
83
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
84
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
85
|
+
* @returns The selected device entry.
|
|
86
|
+
*/
|
|
87
|
+
function validateAndSelectDevice(matches, desiredPhone, desiredPlatform, desiredPlatformVersion) {
|
|
88
|
+
if (matches.length === 0) {
|
|
89
|
+
throw new Error(`No devices found matching "${desiredPhone}" for ${desiredPlatform} ${desiredPlatformVersion}`);
|
|
90
|
+
}
|
|
91
|
+
const exactMatch = matches.find((d) => d.display_name.toLowerCase() === desiredPhone.toLowerCase());
|
|
92
|
+
if (exactMatch) {
|
|
93
|
+
return exactMatch;
|
|
94
|
+
}
|
|
95
|
+
else if (matches.length >= 1) {
|
|
96
|
+
const names = matches.map((d) => d.display_name).join(", ");
|
|
97
|
+
const error_message = matches.length === 1
|
|
98
|
+
? `Alternative device found: ${names}. Would you like to use it?`
|
|
99
|
+
: `Multiple devices found: ${names}. Please select one.`;
|
|
100
|
+
throw new Error(`${error_message}`);
|
|
101
|
+
}
|
|
102
|
+
return matches[0];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validates the app URL.
|
|
106
|
+
* @param appUrl - The app URL to validate.
|
|
107
|
+
* @throws Will throw an error if the app URL is not valid.
|
|
108
|
+
*/
|
|
109
|
+
function validateAppUrl(appUrl) {
|
|
110
|
+
if (!appUrl.match("bs://")) {
|
|
111
|
+
throw new Error("The app path is not a valid BrowserStack app URL.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Constructs the launch URL for the App Live session.
|
|
116
|
+
* @param appUrl - The app URL.
|
|
117
|
+
* @param device - The selected device entry.
|
|
118
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
119
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
120
|
+
* @returns The constructed launch URL.
|
|
121
|
+
*/
|
|
122
|
+
function constructLaunchUrl(appUrl, device, desiredPlatform, desiredPlatformVersion) {
|
|
123
|
+
const deviceParam = (0, utils_1.sanitizeUrlParam)(device.display_name.replace(/\s+/g, "+"));
|
|
124
|
+
const params = new URLSearchParams({
|
|
125
|
+
os: desiredPlatform,
|
|
126
|
+
os_version: desiredPlatformVersion,
|
|
127
|
+
app_hashed_id: appUrl.split("bs://").pop() || "",
|
|
128
|
+
scale_to_fit: "true",
|
|
129
|
+
speed: "1",
|
|
130
|
+
start: "true",
|
|
131
|
+
});
|
|
132
|
+
return `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Opens the launch URL in the default browser.
|
|
136
|
+
* @param launchUrl - The URL to open.
|
|
137
|
+
* @throws Will throw an error if the browser fails to open.
|
|
138
|
+
*/
|
|
139
|
+
function openBrowser(launchUrl) {
|
|
140
|
+
try {
|
|
141
|
+
const command = process.platform === "darwin"
|
|
142
|
+
? ["open", launchUrl]
|
|
143
|
+
: process.platform === "win32"
|
|
144
|
+
? ["cmd", "/c", "start", launchUrl]
|
|
145
|
+
: ["xdg-open", launchUrl];
|
|
146
|
+
// nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
|
|
147
|
+
const child = child_process_1.default.spawn(command[0], command.slice(1), {
|
|
148
|
+
stdio: "ignore",
|
|
149
|
+
detached: true,
|
|
150
|
+
});
|
|
151
|
+
child.on("error", (error) => {
|
|
152
|
+
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
153
|
+
});
|
|
154
|
+
child.unref();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.uploadApp = uploadApp;
|
|
40
|
+
const axios_1 = __importStar(require("axios"));
|
|
41
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
42
|
+
const fs_1 = __importDefault(require("fs"));
|
|
43
|
+
const config_1 = __importDefault(require("../../config"));
|
|
44
|
+
async function uploadApp(filePath) {
|
|
45
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
46
|
+
throw new Error(`File not found at path: ${filePath}`);
|
|
47
|
+
}
|
|
48
|
+
const formData = new form_data_1.default();
|
|
49
|
+
formData.append("file", fs_1.default.createReadStream(filePath));
|
|
50
|
+
try {
|
|
51
|
+
const response = await axios_1.default.post("https://api-cloud.browserstack.com/app-live/upload", formData, {
|
|
52
|
+
headers: {
|
|
53
|
+
...formData.getHeaders(),
|
|
54
|
+
},
|
|
55
|
+
auth: {
|
|
56
|
+
username: config_1.default.browserstackUsername,
|
|
57
|
+
password: config_1.default.browserstackAccessKey,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
return response.data;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof axios_1.AxiosError) {
|
|
64
|
+
throw new Error(`Failed to upload app: ${error.response?.data?.message || error.message}`);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startAppLiveSession = startAppLiveSession;
|
|
7
|
+
exports.default = addAppLiveTools;
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const start_session_1 = require("./applive-utils/start-session");
|
|
11
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
12
|
+
/**
|
|
13
|
+
* Launches an App Live Session on BrowserStack.
|
|
14
|
+
*/
|
|
15
|
+
async function startAppLiveSession(args) {
|
|
16
|
+
if (!args.desiredPlatform) {
|
|
17
|
+
throw new Error("You must provide a desiredPlatform.");
|
|
18
|
+
}
|
|
19
|
+
if (!args.appPath) {
|
|
20
|
+
throw new Error("You must provide a appPath.");
|
|
21
|
+
}
|
|
22
|
+
if (!args.desiredPhone) {
|
|
23
|
+
throw new Error("You must provide a desiredPhone.");
|
|
24
|
+
}
|
|
25
|
+
if (args.desiredPlatform === "android" && !args.appPath.endsWith(".apk")) {
|
|
26
|
+
throw new Error("You must provide a valid Android app path.");
|
|
27
|
+
}
|
|
28
|
+
if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
|
|
29
|
+
throw new Error("You must provide a valid iOS app path.");
|
|
30
|
+
}
|
|
31
|
+
// check if the app path exists && is readable
|
|
32
|
+
try {
|
|
33
|
+
if (!fs_1.default.existsSync(args.appPath)) {
|
|
34
|
+
throw new Error("The app path does not exist.");
|
|
35
|
+
}
|
|
36
|
+
fs_1.default.accessSync(args.appPath, fs_1.default.constants.R_OK);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger_1.default.error("The app path does not exist or is not readable: %s", error);
|
|
40
|
+
throw new Error("The app path does not exist or is not readable.");
|
|
41
|
+
}
|
|
42
|
+
const launchUrl = await (0, start_session_1.startSession)({
|
|
43
|
+
appPath: args.appPath,
|
|
44
|
+
desiredPlatform: args.desiredPlatform,
|
|
45
|
+
desiredPhone: args.desiredPhone,
|
|
46
|
+
desiredPlatformVersion: args.desiredPlatformVersion,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `Successfully started a session. If a browser window did not open automatically, use the following URL to start the session: ${launchUrl}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function addAppLiveTools(server) {
|
|
58
|
+
server.tool("runAppLiveSession", "Use this tool when user wants to manually check their app on a particular mobile device using BrowserStack's cloud infrastructure. Can be used to debug crashes, slow performance, etc.", {
|
|
59
|
+
desiredPhone: zod_1.z
|
|
60
|
+
.string()
|
|
61
|
+
.describe("The full name of the device to run the app on. Example: 'iPhone 12 Pro' or 'Samsung Galaxy S20' or 'Google Pixel 6'. Always ask the user for the device they want to use, do not assume it. "),
|
|
62
|
+
desiredPlatformVersion: zod_1.z
|
|
63
|
+
.string()
|
|
64
|
+
.describe("Specifies the platform version to run the app on. For example, use '12.0' for Android or '16.0' for iOS. If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'."),
|
|
65
|
+
desiredPlatform: zod_1.z
|
|
66
|
+
.enum(["android", "ios"])
|
|
67
|
+
.describe("Which platform to run on, examples: 'android', 'ios'. Set this based on the app path provided."),
|
|
68
|
+
appPath: zod_1.z
|
|
69
|
+
.string()
|
|
70
|
+
.describe("The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it."),
|
|
71
|
+
}, async (args) => {
|
|
72
|
+
try {
|
|
73
|
+
return startAppLiveSession(args);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `Failed to start an app live session. Error: ${error}. Please open an issue on GitHub if the problem persists`,
|
|
81
|
+
isError: true,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getNetworkFailures = getNetworkFailures;
|
|
7
|
+
exports.default = addAutomateTools;
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
10
|
+
const api_1 = require("../lib/api");
|
|
11
|
+
/**
|
|
12
|
+
* Fetches failed network requests from a BrowserStack Automate session.
|
|
13
|
+
* Returns network requests that resulted in errors or failed to complete.
|
|
14
|
+
*/
|
|
15
|
+
async function getNetworkFailures(args) {
|
|
16
|
+
try {
|
|
17
|
+
const failureLogs = await (0, api_1.retrieveNetworkFailures)(args.sessionId);
|
|
18
|
+
logger_1.default.info("Successfully fetched failure network logs for session: %s", args.sessionId);
|
|
19
|
+
// Check if there are any failures
|
|
20
|
+
const hasFailures = failureLogs.totalFailures > 0;
|
|
21
|
+
const text = hasFailures
|
|
22
|
+
? `${failureLogs.totalFailures} network failure(s) found for session :\n\n${JSON.stringify(failureLogs.failures, null, 2)}`
|
|
23
|
+
: `No network failures found for session`;
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
|
35
|
+
logger_1.default.error("Failed to fetch network logs: %s", errorMessage);
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `Failed to fetch network logs: ${errorMessage}`,
|
|
41
|
+
isError: true,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function addAutomateTools(server) {
|
|
49
|
+
server.tool("getNetworkFailures", "Use this tool to fetch failed network requests from a BrowserStack Automate session.", {
|
|
50
|
+
sessionId: zod_1.z.string().describe("The Automate session ID."),
|
|
51
|
+
}, getNetworkFailures);
|
|
52
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bootstrapProjectWithSDK = bootstrapProjectWithSDK;
|
|
4
|
+
exports.default = addSDKTools;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const instructions_1 = require("./sdk-utils/instructions");
|
|
7
|
+
/**
|
|
8
|
+
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
|
|
9
|
+
* This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies.
|
|
10
|
+
*/
|
|
11
|
+
async function bootstrapProjectWithSDK({ detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage, desiredPlatforms, }) {
|
|
12
|
+
const instructions = (0, instructions_1.generateBrowserStackYMLInstructions)(desiredPlatforms);
|
|
13
|
+
const instructionsForProjectConfiguration = (0, instructions_1.getInstructionsForProjectConfiguration)(detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage);
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `${instructions}\n\n After creating the browserstack.yml file above, do the following: ${instructionsForProjectConfiguration}`,
|
|
19
|
+
isError: false,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function addSDKTools(server) {
|
|
25
|
+
server.tool("runTestsOnBrowserStack", "Use this tool to get instructions for running tests on BrowserStack.", {
|
|
26
|
+
detectedBrowserAutomationFramework: zod_1.z
|
|
27
|
+
.string()
|
|
28
|
+
.describe("The automation framework configured in the project. Example: 'playwright', 'selenium'"),
|
|
29
|
+
detectedTestingFramework: zod_1.z
|
|
30
|
+
.string()
|
|
31
|
+
.describe("The testing framework used in the project. Example: 'jest', 'pytest'"),
|
|
32
|
+
detectedLanguage: zod_1.z
|
|
33
|
+
.string()
|
|
34
|
+
.describe("The programming language used in the project. Example: 'nodejs', 'python'"),
|
|
35
|
+
desiredPlatforms: zod_1.z
|
|
36
|
+
.array(zod_1.z.enum(["windows", "macos", "android", "ios"]))
|
|
37
|
+
.describe("The platforms the user wants to test on. Always ask this to the user, do not try to infer this."),
|
|
38
|
+
}, async (args) => {
|
|
39
|
+
const detectedBrowserAutomationFramework = args.detectedBrowserAutomationFramework;
|
|
40
|
+
const detectedTestingFramework = args.detectedTestingFramework;
|
|
41
|
+
const detectedLanguage = args.detectedLanguage;
|
|
42
|
+
const desiredPlatforms = args.desiredPlatforms;
|
|
43
|
+
try {
|
|
44
|
+
return bootstrapProjectWithSDK({
|
|
45
|
+
detectedBrowserAutomationFramework,
|
|
46
|
+
detectedTestingFramework,
|
|
47
|
+
detectedLanguage,
|
|
48
|
+
desiredPlatforms,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: `Failed to bootstrap project with BrowserStack SDK. Error: ${error}. Please open an issue on GitHub if the problem persists`,
|
|
57
|
+
isError: true,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startBrowserSession = startBrowserSession;
|
|
7
|
+
const utils_1 = require("../../lib/utils");
|
|
8
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
+
const child_process_1 = __importDefault(require("child_process"));
|
|
10
|
+
async function startBrowserSession(args) {
|
|
11
|
+
// Sanitize all input parameters
|
|
12
|
+
const sanitizedArgs = {
|
|
13
|
+
browser: (0, utils_1.sanitizeUrlParam)(args.browser),
|
|
14
|
+
os: (0, utils_1.sanitizeUrlParam)(args.os),
|
|
15
|
+
osVersion: (0, utils_1.sanitizeUrlParam)(args.osVersion),
|
|
16
|
+
url: (0, utils_1.sanitizeUrlParam)(args.url),
|
|
17
|
+
browserVersion: (0, utils_1.sanitizeUrlParam)(args.browserVersion),
|
|
18
|
+
isLocal: args.isLocal,
|
|
19
|
+
};
|
|
20
|
+
// Construct URL with encoded parameters
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
os: sanitizedArgs.os,
|
|
23
|
+
os_version: sanitizedArgs.osVersion,
|
|
24
|
+
browser: sanitizedArgs.browser,
|
|
25
|
+
browser_version: sanitizedArgs.browserVersion,
|
|
26
|
+
scale_to_fit: "true",
|
|
27
|
+
url: sanitizedArgs.url,
|
|
28
|
+
resolution: "responsive-mode",
|
|
29
|
+
speed: "1",
|
|
30
|
+
local: sanitizedArgs.isLocal ? "true" : "false",
|
|
31
|
+
start: "true",
|
|
32
|
+
});
|
|
33
|
+
const launchUrl = `https://live.browserstack.com/dashboard#${params.toString()}`;
|
|
34
|
+
try {
|
|
35
|
+
// Use platform-specific commands with proper escaping
|
|
36
|
+
const command = process.platform === "darwin"
|
|
37
|
+
? ["open", launchUrl]
|
|
38
|
+
: process.platform === "win32"
|
|
39
|
+
? ["cmd", "/c", "start", launchUrl]
|
|
40
|
+
: ["xdg-open", launchUrl];
|
|
41
|
+
// nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
|
|
42
|
+
const child = child_process_1.default.spawn(command[0], command.slice(1), {
|
|
43
|
+
stdio: "ignore",
|
|
44
|
+
detached: true,
|
|
45
|
+
});
|
|
46
|
+
// Handle process errors
|
|
47
|
+
child.on("error", (error) => {
|
|
48
|
+
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
49
|
+
});
|
|
50
|
+
// Unref the child process to allow the parent to exit
|
|
51
|
+
child.unref();
|
|
52
|
+
return launchUrl;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
56
|
+
return launchUrl;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startBrowserLiveSession = startBrowserLiveSession;
|
|
7
|
+
exports.default = addBrowserLiveTools;
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
const start_session_1 = require("./live-utils/start-session");
|
|
10
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
11
|
+
const local_1 = require("../lib/local");
|
|
12
|
+
/**
|
|
13
|
+
* Launches a Browser Live Session on BrowserStack.
|
|
14
|
+
*/
|
|
15
|
+
async function startBrowserLiveSession(args) {
|
|
16
|
+
if (!args.desiredBrowser) {
|
|
17
|
+
throw new Error("You must provide a desiredBrowser.");
|
|
18
|
+
}
|
|
19
|
+
if (!args.desiredURL) {
|
|
20
|
+
throw new Error("You must provide a desiredURL.");
|
|
21
|
+
}
|
|
22
|
+
if (!args.desiredOS) {
|
|
23
|
+
throw new Error("You must provide a desiredOS.");
|
|
24
|
+
}
|
|
25
|
+
if (!args.desiredOSVersion) {
|
|
26
|
+
throw new Error("You must provide a desiredOSVersion.");
|
|
27
|
+
}
|
|
28
|
+
if (!args.desiredBrowserVersion) {
|
|
29
|
+
throw new Error("You must provide a desiredBrowserVersion.");
|
|
30
|
+
}
|
|
31
|
+
// Validate URL format
|
|
32
|
+
try {
|
|
33
|
+
new URL(args.desiredURL);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger_1.default.error("Invalid URL format: %s", error);
|
|
37
|
+
throw new Error("The provided URL is invalid.");
|
|
38
|
+
}
|
|
39
|
+
const isLocal = (0, local_1.isLocalURL)(args.desiredURL);
|
|
40
|
+
if (isLocal) {
|
|
41
|
+
await (0, local_1.ensureLocalBinarySetup)();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await (0, local_1.killExistingBrowserStackLocalProcesses)();
|
|
45
|
+
}
|
|
46
|
+
const launchUrl = await (0, start_session_1.startBrowserSession)({
|
|
47
|
+
browser: args.desiredBrowser,
|
|
48
|
+
os: args.desiredOS,
|
|
49
|
+
osVersion: args.desiredOSVersion,
|
|
50
|
+
url: args.desiredURL,
|
|
51
|
+
browserVersion: args.desiredBrowserVersion,
|
|
52
|
+
isLocal,
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `Successfully started a browser session. If a browser window did not open automatically, use the following URL to start the session: ${launchUrl}`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function addBrowserLiveTools(server) {
|
|
64
|
+
server.tool("runBrowserLiveSession", "Use this tool when user wants to manually check their website on a particular browser and OS combination using BrowserStack's cloud infrastructure. Can be used to debug layout issues, compatibility problems, etc.", {
|
|
65
|
+
desiredBrowser: zod_1.z
|
|
66
|
+
.enum(["Chrome", "IE", "Firefox", "Safari", "Edge"])
|
|
67
|
+
.describe("The browser to run the test on. Example: 'Chrome', 'IE', 'Safari', 'Edge'. Always ask the user for the browser they want to use, do not assume it."),
|
|
68
|
+
desiredOSVersion: zod_1.z
|
|
69
|
+
.string()
|
|
70
|
+
.describe("The OS version to run the browser on. Example: '10' for Windows, '12' for macOS, '14' for iOS"),
|
|
71
|
+
desiredOS: zod_1.z
|
|
72
|
+
.enum(["Windows", "OS X"])
|
|
73
|
+
.describe("The operating system to run the browser on. Example: 'Windows', 'OS X'"),
|
|
74
|
+
desiredURL: zod_1.z
|
|
75
|
+
.string()
|
|
76
|
+
.describe("The URL of the website to test. This can be a local URL (e.g., http://localhost:3000) or a public URL. Always ask the user for the URL, do not assume it."),
|
|
77
|
+
desiredBrowserVersion: zod_1.z
|
|
78
|
+
.string()
|
|
79
|
+
.describe("The version of the browser to use. Example: 133.0, 134.0, 87.0"),
|
|
80
|
+
}, async (args) => {
|
|
81
|
+
try {
|
|
82
|
+
return startBrowserLiveSession(args);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Failed to start a browser live session. Error: ${error}. Please open an issue on GitHub if the problem persists`,
|
|
90
|
+
isError: true,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getFailuresInLastRun = getFailuresInLastRun;
|
|
4
|
+
exports.default = addObservabilityTools;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
async function getFailuresInLastRun(buildName, projectName) {
|
|
8
|
+
const buildsData = await (0, api_1.getLatestO11YBuildInfo)(buildName, projectName);
|
|
9
|
+
const observabilityUrl = buildsData.observability_url;
|
|
10
|
+
if (!observabilityUrl) {
|
|
11
|
+
throw new Error("No observability URL found in build data, this is likely because the build is not yet available on BrowserStack Observability.");
|
|
12
|
+
}
|
|
13
|
+
let overview = "No overview available";
|
|
14
|
+
if (buildsData.unique_errors?.overview?.insight) {
|
|
15
|
+
overview = buildsData.unique_errors.overview.insight;
|
|
16
|
+
}
|
|
17
|
+
let details = "No error details available";
|
|
18
|
+
if (buildsData.unique_errors?.top_unique_errors?.length > 0) {
|
|
19
|
+
details = buildsData.unique_errors.top_unique_errors
|
|
20
|
+
.map((error) => error.error)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: `Observability URL: ${observabilityUrl}\nOverview: ${overview}\nError Details: ${details}`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function addObservabilityTools(server) {
|
|
34
|
+
server.tool("getFailuresInLastRun", "Use this tool to debug failures in the last run of the test suite on BrowserStack. Use only when browserstack.yml file is present in the project root.", {
|
|
35
|
+
buildName: zod_1.z
|
|
36
|
+
.string()
|
|
37
|
+
.describe("Name of the build to get failures for. This is the 'build' key in the browserstack.yml file. If not sure, ask the user for the build name."),
|
|
38
|
+
projectName: zod_1.z
|
|
39
|
+
.string()
|
|
40
|
+
.describe("Name of the project to get failures for. This is the 'projectName' key in the browserstack.yml file. If not sure, ask the user for the project name."),
|
|
41
|
+
}, async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
return getFailuresInLastRun(args.buildName, args.projectName);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `Failed to get failures in the last run. Error: ${error}. Please open an issue on GitHub if this is an issue with BrowserStack`,
|
|
51
|
+
isError: true,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|