@browserstack/mcp-server 1.0.10 → 1.0.12
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 +4 -2
- package/dist/lib/device-cache.js +52 -0
- package/dist/lib/instrumentation.js +51 -0
- package/dist/tools/accessibility.js +31 -28
- package/dist/tools/applive-utils/start-session.js +2 -2
- package/dist/tools/applive.js +6 -2
- package/dist/tools/automate.js +24 -13
- package/dist/tools/bstack-sdk.js +8 -9
- package/dist/tools/live-utils/desktop-filter.js +98 -0
- package/dist/tools/live-utils/mobile-filter.js +65 -0
- package/dist/tools/live-utils/start-session.js +64 -28
- package/dist/tools/live-utils/types.js +8 -0
- package/dist/tools/live-utils/version-resolver.js +48 -0
- package/dist/tools/live.js +68 -59
- package/dist/tools/observability.js +9 -1
- package/dist/tools/testmanagement-utils/create-testrun.js +90 -0
- package/dist/tools/testmanagement-utils/list-testcases.js +90 -0
- package/dist/tools/testmanagement-utils/list-testruns.js +68 -0
- package/dist/tools/testmanagement-utils/update-testrun.js +74 -0
- package/dist/tools/testmanagement.js +108 -0
- package/package.json +1 -1
- package/dist/tools/applive-utils/device-cache.js +0 -33
package/dist/config.js
CHANGED
|
@@ -8,11 +8,13 @@ if (!process.env.BROWSERSTACK_ACCESS_KEY ||
|
|
|
8
8
|
class Config {
|
|
9
9
|
browserstackUsername;
|
|
10
10
|
browserstackAccessKey;
|
|
11
|
-
|
|
11
|
+
DEV_MODE;
|
|
12
|
+
constructor(browserstackUsername, browserstackAccessKey, DEV_MODE) {
|
|
12
13
|
this.browserstackUsername = browserstackUsername;
|
|
13
14
|
this.browserstackAccessKey = browserstackAccessKey;
|
|
15
|
+
this.DEV_MODE = DEV_MODE;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
exports.Config = Config;
|
|
17
|
-
const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY);
|
|
19
|
+
const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY, process.env.DEV_MODE === "true");
|
|
18
20
|
exports.default = config;
|
|
@@ -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.getDevicesAndBrowsers = getDevicesAndBrowsers;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const CACHE_DIR = path_1.default.join(os_1.default.homedir(), ".browserstack", "combined_cache");
|
|
11
|
+
const CACHE_FILE = path_1.default.join(CACHE_DIR, "data.json");
|
|
12
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
13
|
+
const URLS = {
|
|
14
|
+
live: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
|
|
15
|
+
app_live: "https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Fetches and caches both BrowserStack datasets (live + app_live) with a shared TTL.
|
|
19
|
+
*/
|
|
20
|
+
async function getDevicesAndBrowsers(type) {
|
|
21
|
+
if (!fs_1.default.existsSync(CACHE_DIR)) {
|
|
22
|
+
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
let cache = {};
|
|
25
|
+
if (fs_1.default.existsSync(CACHE_FILE)) {
|
|
26
|
+
const stats = fs_1.default.statSync(CACHE_FILE);
|
|
27
|
+
if (Date.now() - stats.mtimeMs < TTL_MS) {
|
|
28
|
+
try {
|
|
29
|
+
cache = JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
30
|
+
return cache[type];
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error("Error parsing cache file:", error);
|
|
34
|
+
// Continue with fetching fresh data
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const [liveRes, appLiveRes] = await Promise.all([
|
|
39
|
+
fetch(URLS.live),
|
|
40
|
+
fetch(URLS.app_live),
|
|
41
|
+
]);
|
|
42
|
+
if (!liveRes.ok || !appLiveRes.ok) {
|
|
43
|
+
throw new Error(`Failed to fetch configuration from BrowserStack : live=${liveRes.statusText}, app_live=${appLiveRes.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
const [liveData, appLiveData] = await Promise.all([
|
|
46
|
+
liveRes.json(),
|
|
47
|
+
appLiveRes.json(),
|
|
48
|
+
]);
|
|
49
|
+
cache = { live: liveData, app_live: appLiveData };
|
|
50
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
|
|
51
|
+
return cache[type];
|
|
52
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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.trackMCP = trackMCP;
|
|
7
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
8
|
+
const config_1 = __importDefault(require("../config"));
|
|
9
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
function trackMCP(toolName, clientInfo, error) {
|
|
12
|
+
if (config_1.default.DEV_MODE) {
|
|
13
|
+
logger_1.default.info("Tracking MCP is disabled in dev mode");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
|
|
17
|
+
const isSuccess = !error;
|
|
18
|
+
const mcpClient = clientInfo?.name || "unknown";
|
|
19
|
+
// Log client information
|
|
20
|
+
if (clientInfo?.name) {
|
|
21
|
+
logger_1.default.info(`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
logger_1.default.info("Client connected: unknown client");
|
|
25
|
+
}
|
|
26
|
+
const event = {
|
|
27
|
+
event_type: "MCPInstrumentation",
|
|
28
|
+
event_properties: {
|
|
29
|
+
mcp_version: package_json_1.default.version,
|
|
30
|
+
tool_name: toolName,
|
|
31
|
+
mcp_client: mcpClient,
|
|
32
|
+
success: isSuccess,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
// Add error details if applicable
|
|
36
|
+
if (error) {
|
|
37
|
+
event.event_properties.error_message =
|
|
38
|
+
error instanceof Error ? error.message : String(error);
|
|
39
|
+
event.event_properties.error_type =
|
|
40
|
+
error instanceof Error ? error.constructor.name : "Unknown";
|
|
41
|
+
}
|
|
42
|
+
axios_1.default
|
|
43
|
+
.post(instrumentationEndpoint, event, {
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Authorization: `Basic ${Buffer.from(`${config_1.default.browserstackUsername}:${config_1.default.browserstackAccessKey}`).toString("base64")}`,
|
|
47
|
+
},
|
|
48
|
+
timeout: 2000,
|
|
49
|
+
})
|
|
50
|
+
.catch(() => { });
|
|
51
|
+
}
|
|
@@ -3,41 +3,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.default = addAccessibilityTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const accessibility_1 = require("./accessiblity-utils/accessibility");
|
|
6
|
+
const instrumentation_1 = require("../lib/instrumentation");
|
|
6
7
|
async function runAccessibilityScan(name, pageURL) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
throw new Error("Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists");
|
|
13
|
-
}
|
|
14
|
-
return {
|
|
15
|
-
content: [
|
|
16
|
-
{
|
|
17
|
-
type: "text",
|
|
18
|
-
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
return {
|
|
25
|
-
content: [
|
|
26
|
-
{
|
|
27
|
-
type: "text",
|
|
28
|
-
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
|
|
29
|
-
isError: true,
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
isError: true,
|
|
33
|
-
};
|
|
8
|
+
const response = await (0, accessibility_1.startAccessibilityScan)(name, [pageURL]);
|
|
9
|
+
const scanId = response.data?.id;
|
|
10
|
+
const scanRunId = response.data?.scanRunId;
|
|
11
|
+
if (!scanId || !scanRunId) {
|
|
12
|
+
throw new Error("Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists");
|
|
34
13
|
}
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
35
22
|
}
|
|
36
23
|
function addAccessibilityTools(server) {
|
|
37
24
|
server.tool("startAccessibilityScan", "Use this tool to start an accessibility scan for a list of URLs on BrowserStack.", {
|
|
38
25
|
name: zod_1.z.string().describe("Name of the accessibility scan"),
|
|
39
26
|
pageURL: zod_1.z.string().describe("The URL to scan for accessibility issues"),
|
|
40
27
|
}, async (args) => {
|
|
41
|
-
|
|
28
|
+
try {
|
|
29
|
+
(0, instrumentation_1.trackMCP)("startAccessibilityScan", server.server.getClientVersion());
|
|
30
|
+
return await runAccessibilityScan(args.name, args.pageURL);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
(0, instrumentation_1.trackMCP)("startAccessibilityScan", server.server.getClientVersion(), error);
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
|
|
39
|
+
isError: true,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
42
45
|
});
|
|
43
46
|
}
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.startSession = startSession;
|
|
7
7
|
const child_process_1 = __importDefault(require("child_process"));
|
|
8
8
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
-
const device_cache_1 = require("
|
|
9
|
+
const device_cache_1 = require("../../lib/device-cache");
|
|
10
10
|
const fuzzy_search_1 = require("./fuzzy-search");
|
|
11
11
|
const utils_1 = require("../../lib/utils");
|
|
12
12
|
const upload_app_1 = require("./upload-app");
|
|
@@ -19,7 +19,7 @@ const upload_app_1 = require("./upload-app");
|
|
|
19
19
|
async function startSession(args) {
|
|
20
20
|
const { appPath, desiredPlatform, desiredPhone } = args;
|
|
21
21
|
let { desiredPlatformVersion } = args;
|
|
22
|
-
const data = await (0, device_cache_1.
|
|
22
|
+
const data = await (0, device_cache_1.getDevicesAndBrowsers)("app_live");
|
|
23
23
|
const allDevices = data.mobile.flatMap((group) => group.devices.map((dev) => ({ ...dev, os: group.os })));
|
|
24
24
|
desiredPlatformVersion = resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
25
25
|
const filteredDevices = filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
package/dist/tools/applive.js
CHANGED
|
@@ -9,6 +9,7 @@ const zod_1 = require("zod");
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const start_session_1 = require("./applive-utils/start-session");
|
|
11
11
|
const logger_1 = __importDefault(require("../logger"));
|
|
12
|
+
const instrumentation_1 = require("../lib/instrumentation");
|
|
12
13
|
/**
|
|
13
14
|
* Launches an App Live Session on BrowserStack.
|
|
14
15
|
*/
|
|
@@ -70,14 +71,17 @@ function addAppLiveTools(server) {
|
|
|
70
71
|
.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
72
|
}, async (args) => {
|
|
72
73
|
try {
|
|
73
|
-
|
|
74
|
+
(0, instrumentation_1.trackMCP)("runAppLiveSession", server.server.getClientVersion());
|
|
75
|
+
return await startAppLiveSession(args);
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
78
|
+
logger_1.default.error("App live session failed: %s", error);
|
|
79
|
+
(0, instrumentation_1.trackMCP)("runAppLiveSession", server.server.getClientVersion(), error);
|
|
76
80
|
return {
|
|
77
81
|
content: [
|
|
78
82
|
{
|
|
79
83
|
type: "text",
|
|
80
|
-
text: `Failed to start
|
|
84
|
+
text: `Failed to start app live session: ${error instanceof Error ? error.message : String(error)}`,
|
|
81
85
|
isError: true,
|
|
82
86
|
},
|
|
83
87
|
],
|
package/dist/tools/automate.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.default = addAutomateTools;
|
|
|
8
8
|
const zod_1 = require("zod");
|
|
9
9
|
const logger_1 = __importDefault(require("../logger"));
|
|
10
10
|
const api_1 = require("../lib/api");
|
|
11
|
+
const instrumentation_1 = require("../lib/instrumentation");
|
|
11
12
|
/**
|
|
12
13
|
* Fetches failed network requests from a BrowserStack Automate session.
|
|
13
14
|
* Returns network requests that resulted in errors or failed to complete.
|
|
@@ -31,22 +32,32 @@ async function getNetworkFailures(args) {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
catch (error) {
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
};
|
|
35
|
+
logger_1.default.error("Failed to fetch network logs: %s", error);
|
|
36
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
46
37
|
}
|
|
47
38
|
}
|
|
48
39
|
function addAutomateTools(server) {
|
|
49
40
|
server.tool("getNetworkFailures", "Use this tool to fetch failed network requests from a BrowserStack Automate session.", {
|
|
50
41
|
sessionId: zod_1.z.string().describe("The Automate session ID."),
|
|
51
|
-
},
|
|
42
|
+
}, async (args) => {
|
|
43
|
+
try {
|
|
44
|
+
(0, instrumentation_1.trackMCP)("getNetworkFailures", server.server.getClientVersion());
|
|
45
|
+
return await getNetworkFailures(args);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
logger_1.default.error("Failed to fetch network logs: %s", errorMessage);
|
|
50
|
+
(0, instrumentation_1.trackMCP)("getNetworkFailures", server.server.getClientVersion(), error);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Failed to fetch network logs: ${errorMessage}`,
|
|
56
|
+
isError: true,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
isError: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
52
63
|
}
|
package/dist/tools/bstack-sdk.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.bootstrapProjectWithSDK = bootstrapProjectWithSDK;
|
|
|
4
4
|
exports.default = addSDKTools;
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const instructions_1 = require("./sdk-utils/instructions");
|
|
7
|
+
const instrumentation_1 = require("../lib/instrumentation");
|
|
7
8
|
/**
|
|
8
9
|
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
|
|
9
10
|
* This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies.
|
|
@@ -36,19 +37,17 @@ function addSDKTools(server) {
|
|
|
36
37
|
.array(zod_1.z.enum(["windows", "macos", "android", "ios"]))
|
|
37
38
|
.describe("The platforms the user wants to test on. Always ask this to the user, do not try to infer this."),
|
|
38
39
|
}, async (args) => {
|
|
39
|
-
const detectedBrowserAutomationFramework = args.detectedBrowserAutomationFramework;
|
|
40
|
-
const detectedTestingFramework = args.detectedTestingFramework;
|
|
41
|
-
const detectedLanguage = args.detectedLanguage;
|
|
42
|
-
const desiredPlatforms = args.desiredPlatforms;
|
|
43
40
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
(0, instrumentation_1.trackMCP)("runTestsOnBrowserStack", server.server.getClientVersion());
|
|
42
|
+
return await bootstrapProjectWithSDK({
|
|
43
|
+
detectedBrowserAutomationFramework: args.detectedBrowserAutomationFramework,
|
|
44
|
+
detectedTestingFramework: args.detectedTestingFramework,
|
|
45
|
+
detectedLanguage: args.detectedLanguage,
|
|
46
|
+
desiredPlatforms: args.desiredPlatforms,
|
|
49
47
|
});
|
|
50
48
|
}
|
|
51
49
|
catch (error) {
|
|
50
|
+
(0, instrumentation_1.trackMCP)("runTestsOnBrowserStack", server.server.getClientVersion(), error);
|
|
52
51
|
return {
|
|
53
52
|
content: [
|
|
54
53
|
{
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterDesktop = filterDesktop;
|
|
4
|
+
const device_cache_1 = require("../../lib/device-cache");
|
|
5
|
+
const version_resolver_1 = require("./version-resolver");
|
|
6
|
+
const fuzzy_1 = require("../../lib/fuzzy");
|
|
7
|
+
async function filterDesktop(args) {
|
|
8
|
+
const data = await (0, device_cache_1.getDevicesAndBrowsers)("live");
|
|
9
|
+
const allEntries = getAllDesktopEntries(data);
|
|
10
|
+
// Filter OS
|
|
11
|
+
const osList = filterByOS(allEntries, args.os);
|
|
12
|
+
// Filter browser
|
|
13
|
+
const browserList = filterByBrowser(osList, args.browser, args.os);
|
|
14
|
+
// Resolve OS version
|
|
15
|
+
const uniqueOSVersions = getUniqueOSVersions(browserList);
|
|
16
|
+
const chosenOS = resolveOSVersion(args.os, args.osVersion, uniqueOSVersions);
|
|
17
|
+
// Filter entries based on chosen OS version
|
|
18
|
+
const entriesForOS = filterByOSVersion(browserList, chosenOS);
|
|
19
|
+
// Resolve browser version
|
|
20
|
+
const browserVersions = entriesForOS.map((e) => e.browser_version);
|
|
21
|
+
const chosenBrowserVersion = (0, version_resolver_1.resolveVersion)(args.browserVersion, browserVersions);
|
|
22
|
+
// Find final entry
|
|
23
|
+
const finalEntry = entriesForOS.find((e) => e.browser_version === chosenBrowserVersion);
|
|
24
|
+
if (!finalEntry) {
|
|
25
|
+
throw new Error(`No entry for browser version "${args.browserVersion}".`);
|
|
26
|
+
}
|
|
27
|
+
// Add notes if versions were adjusted
|
|
28
|
+
addNotes(finalEntry, args, chosenOS, chosenBrowserVersion);
|
|
29
|
+
return finalEntry;
|
|
30
|
+
}
|
|
31
|
+
function getAllDesktopEntries(data) {
|
|
32
|
+
return data.desktop.flatMap((plat) => plat.browsers.map((b) => ({
|
|
33
|
+
os: plat.os,
|
|
34
|
+
os_version: plat.os_version,
|
|
35
|
+
browser: b.browser,
|
|
36
|
+
browser_version: b.browser_version,
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
function filterByOS(entries, os) {
|
|
40
|
+
const filtered = entries.filter((e) => e.os === os);
|
|
41
|
+
if (!filtered.length)
|
|
42
|
+
throw new Error(`No OS entries for "${os}".`);
|
|
43
|
+
return filtered;
|
|
44
|
+
}
|
|
45
|
+
function filterByBrowser(entries, browser, os) {
|
|
46
|
+
const filtered = entries.filter((e) => e.browser === browser);
|
|
47
|
+
if (!filtered.length)
|
|
48
|
+
throw new Error(`No browser "${browser}" on ${os}.`);
|
|
49
|
+
return filtered;
|
|
50
|
+
}
|
|
51
|
+
function getUniqueOSVersions(entries) {
|
|
52
|
+
return Array.from(new Set(entries.map((e) => e.os_version)));
|
|
53
|
+
}
|
|
54
|
+
function resolveOSVersion(os, requestedVersion, availableVersions) {
|
|
55
|
+
if (os === "OS X") {
|
|
56
|
+
return resolveMacOSVersion(requestedVersion, availableVersions);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// For Windows, use semantic versioning
|
|
60
|
+
return (0, version_resolver_1.resolveVersion)(requestedVersion, availableVersions);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function resolveMacOSVersion(requested, available) {
|
|
64
|
+
if (requested === "latest") {
|
|
65
|
+
return available[available.length - 1];
|
|
66
|
+
}
|
|
67
|
+
else if (requested === "oldest") {
|
|
68
|
+
return available[0];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Try fuzzy matching
|
|
72
|
+
const fuzzy = (0, fuzzy_1.customFuzzySearch)(available.map((v) => ({ os_version: v })), ["os_version"], requested, 1);
|
|
73
|
+
const matched = fuzzy.length ? fuzzy[0].os_version : requested;
|
|
74
|
+
// Fallback if not valid
|
|
75
|
+
return available.includes(matched) ? matched : available[0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function filterByOSVersion(entries, osVersion) {
|
|
79
|
+
return entries.filter((e) => e.os_version === osVersion);
|
|
80
|
+
}
|
|
81
|
+
function addNotes(entry, args, resolvedOS, resolvedBrowser) {
|
|
82
|
+
if (args.osVersion !== resolvedOS &&
|
|
83
|
+
args.osVersion !== "latest" &&
|
|
84
|
+
args.osVersion !== "oldest") {
|
|
85
|
+
entry.notes = `Note: OS version ${args.osVersion} was not found. Using "${resolvedOS}" instead.`;
|
|
86
|
+
}
|
|
87
|
+
if (args.browserVersion !== resolvedBrowser &&
|
|
88
|
+
args.browserVersion !== "latest" &&
|
|
89
|
+
args.browserVersion !== "oldest") {
|
|
90
|
+
if (!entry.notes) {
|
|
91
|
+
entry.notes = `Note: `;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
entry.notes += ` `;
|
|
95
|
+
}
|
|
96
|
+
entry.notes += `Browser version ${args.browserVersion} was not found. Using "${resolvedBrowser}" instead.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterMobile = filterMobile;
|
|
4
|
+
const device_cache_1 = require("../../lib/device-cache");
|
|
5
|
+
const version_resolver_1 = require("./version-resolver");
|
|
6
|
+
const fuzzy_1 = require("../../lib/fuzzy");
|
|
7
|
+
// Extract all mobile entries from the data
|
|
8
|
+
function getAllMobileEntries(data) {
|
|
9
|
+
return data.mobile.flatMap((grp) => grp.devices.map((d) => ({
|
|
10
|
+
os: grp.os,
|
|
11
|
+
os_version: d.os_version,
|
|
12
|
+
display_name: d.display_name,
|
|
13
|
+
notes: "",
|
|
14
|
+
})));
|
|
15
|
+
}
|
|
16
|
+
// Filter entries by OS
|
|
17
|
+
function filterByOS(entries, os) {
|
|
18
|
+
const candidates = entries.filter((d) => d.os === os);
|
|
19
|
+
if (!candidates.length)
|
|
20
|
+
throw new Error(`No mobile OS entries for "${os}".`);
|
|
21
|
+
return candidates;
|
|
22
|
+
}
|
|
23
|
+
// Find matching device with exact match validation
|
|
24
|
+
function findMatchingDevice(entries, deviceName, os) {
|
|
25
|
+
const matches = (0, fuzzy_1.customFuzzySearch)(entries, ["display_name"], deviceName, 5);
|
|
26
|
+
if (!matches.length)
|
|
27
|
+
throw new Error(`No devices matching "${deviceName}" on ${os}.`);
|
|
28
|
+
const exact = matches.find((m) => m.display_name.toLowerCase() === deviceName.toLowerCase());
|
|
29
|
+
if (!exact) {
|
|
30
|
+
const names = matches.map((m) => m.display_name).join(", ");
|
|
31
|
+
throw new Error(`Alternative Device/Device's found : ${names}. Please Select one.`);
|
|
32
|
+
}
|
|
33
|
+
const result = entries.filter((d) => d.display_name === exact.display_name);
|
|
34
|
+
if (!result.length)
|
|
35
|
+
throw new Error(`No device "${exact.display_name}" on ${os}.`);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
// Find the appropriate OS version
|
|
39
|
+
function findOSVersion(entries, requestedVersion) {
|
|
40
|
+
const versions = entries.map((d) => d.os_version);
|
|
41
|
+
const chosenVersion = (0, version_resolver_1.resolveVersion)(requestedVersion, versions);
|
|
42
|
+
const result = entries.filter((d) => d.os_version === chosenVersion);
|
|
43
|
+
if (!result.length)
|
|
44
|
+
throw new Error(`No entry for OS version "${requestedVersion}".`);
|
|
45
|
+
return { entries: result, chosenVersion };
|
|
46
|
+
}
|
|
47
|
+
// Create version note if needed
|
|
48
|
+
function createVersionNote(requestedVersion, actualVersion) {
|
|
49
|
+
if (actualVersion !== requestedVersion &&
|
|
50
|
+
requestedVersion !== "latest" &&
|
|
51
|
+
requestedVersion !== "oldest") {
|
|
52
|
+
return `Note: Os version ${requestedVersion} was not found. Using ${actualVersion} instead.`;
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
async function filterMobile(args) {
|
|
57
|
+
const data = await (0, device_cache_1.getDevicesAndBrowsers)("live");
|
|
58
|
+
const allEntries = getAllMobileEntries(data);
|
|
59
|
+
const osCandidates = filterByOS(allEntries, args.os);
|
|
60
|
+
const deviceCandidates = findMatchingDevice(osCandidates, args.device, args.os);
|
|
61
|
+
const { entries: versionCandidates, chosenVersion } = findOSVersion(deviceCandidates, args.osVersion);
|
|
62
|
+
const final = versionCandidates[0];
|
|
63
|
+
final.notes = createVersionNote(args.osVersion, chosenVersion);
|
|
64
|
+
return final;
|
|
65
|
+
}
|
|
@@ -4,35 +4,77 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.startBrowserSession = startBrowserSession;
|
|
7
|
-
const utils_1 = require("../../lib/utils");
|
|
8
7
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
8
|
const child_process_1 = __importDefault(require("child_process"));
|
|
9
|
+
const desktop_filter_1 = require("./desktop-filter");
|
|
10
|
+
const mobile_filter_1 = require("./mobile-filter");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
const local_1 = require("../../lib/local");
|
|
13
|
+
/**
|
|
14
|
+
* Prepares local tunnel setup based on URL type
|
|
15
|
+
*/
|
|
16
|
+
async function prepareLocalTunnel(url) {
|
|
17
|
+
const isLocal = (0, local_1.isLocalURL)(url);
|
|
18
|
+
if (isLocal) {
|
|
19
|
+
await (0, local_1.ensureLocalBinarySetup)();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await (0, local_1.killExistingBrowserStackLocalProcesses)();
|
|
23
|
+
}
|
|
24
|
+
return isLocal;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Entrypoint: detects platformType & delegates.
|
|
28
|
+
*/
|
|
10
29
|
async function startBrowserSession(args) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
|
|
30
|
+
const entry = args.platformType === types_1.PlatformType.DESKTOP
|
|
31
|
+
? await (0, desktop_filter_1.filterDesktop)(args)
|
|
32
|
+
: await (0, mobile_filter_1.filterMobile)(args);
|
|
33
|
+
const isLocal = await prepareLocalTunnel(args.url);
|
|
34
|
+
const url = args.platformType === types_1.PlatformType.DESKTOP
|
|
35
|
+
? buildDesktopUrl(args, entry, isLocal)
|
|
36
|
+
: buildMobileUrl(args, entry, isLocal);
|
|
37
|
+
openBrowser(url);
|
|
38
|
+
return entry.notes ? `${url}, ${entry.notes}` : url;
|
|
39
|
+
}
|
|
40
|
+
function buildDesktopUrl(args, e, isLocal) {
|
|
21
41
|
const params = new URLSearchParams({
|
|
22
|
-
os:
|
|
23
|
-
os_version:
|
|
24
|
-
browser:
|
|
25
|
-
browser_version:
|
|
42
|
+
os: e.os,
|
|
43
|
+
os_version: e.os_version,
|
|
44
|
+
browser: e.browser,
|
|
45
|
+
browser_version: e.browser_version,
|
|
46
|
+
url: args.url,
|
|
26
47
|
scale_to_fit: "true",
|
|
27
|
-
url: sanitizedArgs.url,
|
|
28
48
|
resolution: "responsive-mode",
|
|
29
49
|
speed: "1",
|
|
30
|
-
local:
|
|
50
|
+
local: isLocal ? "true" : "false",
|
|
51
|
+
start: "true",
|
|
52
|
+
});
|
|
53
|
+
return `https://live.browserstack.com/dashboard#${params.toString()}`;
|
|
54
|
+
}
|
|
55
|
+
function buildMobileUrl(args, d, isLocal) {
|
|
56
|
+
const os_map = {
|
|
57
|
+
android: "Android",
|
|
58
|
+
ios: "iOS",
|
|
59
|
+
winphone: "Winphone",
|
|
60
|
+
};
|
|
61
|
+
const os = os_map[d.os] || d.os;
|
|
62
|
+
const params = new URLSearchParams({
|
|
63
|
+
os: os,
|
|
64
|
+
os_version: d.os_version,
|
|
65
|
+
device: d.display_name,
|
|
66
|
+
device_browser: args.browser,
|
|
67
|
+
url: args.url,
|
|
68
|
+
scale_to_fit: "true",
|
|
69
|
+
speed: "1",
|
|
70
|
+
local: isLocal ? "true" : "false",
|
|
31
71
|
start: "true",
|
|
32
72
|
});
|
|
33
|
-
|
|
73
|
+
return `https://live.browserstack.com/dashboard#${params.toString()}`;
|
|
74
|
+
}
|
|
75
|
+
// ——— Open a browser window ———
|
|
76
|
+
function openBrowser(launchUrl) {
|
|
34
77
|
try {
|
|
35
|
-
// Use platform-specific commands with proper escaping
|
|
36
78
|
const command = process.platform === "darwin"
|
|
37
79
|
? ["open", launchUrl]
|
|
38
80
|
: process.platform === "win32"
|
|
@@ -43,16 +85,10 @@ async function startBrowserSession(args) {
|
|
|
43
85
|
stdio: "ignore",
|
|
44
86
|
detached: true,
|
|
45
87
|
});
|
|
46
|
-
|
|
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
|
|
88
|
+
child.on("error", (err) => logger_1.default.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
|
|
51
89
|
child.unref();
|
|
52
|
-
return launchUrl;
|
|
53
90
|
}
|
|
54
|
-
catch (
|
|
55
|
-
logger_1.default.error(`Failed to
|
|
56
|
-
return launchUrl;
|
|
91
|
+
catch (err) {
|
|
92
|
+
logger_1.default.error(`Failed to launch browser: ${err}. URL: ${launchUrl}`);
|
|
57
93
|
}
|
|
58
94
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlatformType = void 0;
|
|
4
|
+
var PlatformType;
|
|
5
|
+
(function (PlatformType) {
|
|
6
|
+
PlatformType["DESKTOP"] = "desktop";
|
|
7
|
+
PlatformType["MOBILE"] = "mobile";
|
|
8
|
+
})(PlatformType || (exports.PlatformType = PlatformType = {}));
|