Skip to content

Commit b2dccfe

Browse files
Merge pull request #148 from tech-sushant/devices-validation
feat: introduce validations for BrowserStack devices
2 parents 2f52076 + 7e3d764 commit b2dccfe

File tree

14 files changed

+1016
-110
lines changed

14 files changed

+1016
-110
lines changed

src/lib/device-cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export enum BrowserStackProducts {
1313
LIVE = "live",
1414
APP_LIVE = "app_live",
1515
APP_AUTOMATE = "app_automate",
16+
SELENIUM_AUTOMATE = "selenium_automate",
17+
PLAYWRIGHT_AUTOMATE = "playwright_automate",
1618
}
1719

1820
const URLS: Record<BrowserStackProducts, string> = {
@@ -22,6 +24,10 @@ const URLS: Record<BrowserStackProducts, string> = {
2224
"https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
2325
[BrowserStackProducts.APP_AUTOMATE]:
2426
"https://www.browserstack.com/list-of-browsers-and-platforms/app_automate.json",
27+
[BrowserStackProducts.SELENIUM_AUTOMATE]:
28+
"https://www.browserstack.com/list-of-browsers-and-platforms/automate.json",
29+
[BrowserStackProducts.PLAYWRIGHT_AUTOMATE]:
30+
"https://www.browserstack.com/list-of-browsers-and-platforms/playwright.json",
2531
};
2632

2733
/**

src/lib/version-resolver.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
export function resolveVersion(requested: string, available: string[]): string {
77
// strip duplicates & sort
88
const uniq = Array.from(new Set(available));
9+
910
// pick min/max
1011
if (requested === "latest" || requested === "oldest") {
1112
// try numeric
@@ -21,29 +22,48 @@ export function resolveVersion(requested: string, available: string[]): string {
2122
return requested === "latest" ? lex[lex.length - 1] : lex[0];
2223
}
2324

24-
// exact?
25+
// exact match?
2526
if (uniq.includes(requested)) {
2627
return requested;
2728
}
2829

29-
// try closest numeric
30+
// Try major version matching (e.g., "14" matches "14.0", "14.1", etc.)
3031
const reqNum = parseFloat(requested);
31-
const nums = uniq
32-
.map((v) => ({ v, n: parseFloat(v) }))
33-
.filter((x) => !isNaN(x.n));
34-
if (!isNaN(reqNum) && nums.length) {
35-
let best = nums[0],
36-
bestDiff = Math.abs(nums[0].n - reqNum);
37-
for (const x of nums) {
38-
const d = Math.abs(x.n - reqNum);
39-
if (d < bestDiff) {
40-
best = x;
41-
bestDiff = d;
32+
if (!isNaN(reqNum)) {
33+
const majorVersionMatches = uniq.filter((v) => {
34+
const vNum = parseFloat(v);
35+
return !isNaN(vNum) && Math.floor(vNum) === Math.floor(reqNum);
36+
});
37+
38+
if (majorVersionMatches.length > 0) {
39+
// If multiple matches, prefer the most common format or latest
40+
const exactMatch = majorVersionMatches.find(
41+
(v) => v === `${Math.floor(reqNum)}.0`,
42+
);
43+
if (exactMatch) {
44+
return exactMatch;
4245
}
46+
// Return the first match (usually the most common format)
47+
return majorVersionMatches[0];
48+
}
49+
}
50+
51+
// Fuzzy matching: find the closest version
52+
const reqNumForFuzzy = parseFloat(requested);
53+
if (!isNaN(reqNumForFuzzy)) {
54+
const numericVersions = uniq
55+
.map((v) => ({ v, n: parseFloat(v) }))
56+
.filter((x) => !isNaN(x.n))
57+
.sort(
58+
(a, b) =>
59+
Math.abs(a.n - reqNumForFuzzy) - Math.abs(b.n - reqNumForFuzzy),
60+
);
61+
62+
if (numericVersions.length > 0) {
63+
return numericVersions[0].v;
4364
}
44-
return best.v;
4565
}
4666

47-
// final fallback
67+
// Fallback: return the first available version
4868
return uniq[0];
4969
}
Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,39 @@
1-
// Configuration utilities for BrowserStack App SDK
21
import {
32
APP_DEVICE_CONFIGS,
43
AppSDKSupportedTestingFrameworkEnum,
54
DEFAULT_APP_PATH,
65
createStep,
76
} from "./index.js";
7+
import { ValidatedEnvironment } from "../../sdk-utils/common/device-validator.js";
88

99
export function generateAppBrowserStackYMLInstructions(
10-
platforms: string[],
10+
config: {
11+
validatedEnvironments?: ValidatedEnvironment[];
12+
platforms?: string[];
13+
testingFramework?: string;
14+
projectName?: string;
15+
},
1116
username: string,
1217
accessKey: string,
1318
appPath: string = DEFAULT_APP_PATH,
14-
testingFramework: string,
1519
): string {
1620
if (
17-
testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch ||
18-
testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio ||
19-
testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby
21+
config.testingFramework ===
22+
AppSDKSupportedTestingFrameworkEnum.nightwatch ||
23+
config.testingFramework ===
24+
AppSDKSupportedTestingFrameworkEnum.webdriverio ||
25+
config.testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby
2026
) {
2127
return "";
2228
}
2329

24-
// Generate platform and device configurations
25-
const platformConfigs = platforms
26-
.map((platform) => {
27-
const devices =
28-
APP_DEVICE_CONFIGS[platform as keyof typeof APP_DEVICE_CONFIGS];
29-
if (!devices) return "";
30+
const platformConfigs = generatePlatformConfigs(config);
3031

31-
return devices
32-
.map(
33-
(device) => ` - platformName: ${platform}
34-
deviceName: ${device.deviceName}
35-
platformVersion: "${device.platformVersion}"`,
36-
)
37-
.join("\n");
38-
})
39-
.filter(Boolean)
40-
.join("\n");
32+
const projectName = config.projectName || "BrowserStack Sample";
33+
const buildName = config.projectName
34+
? `${config.projectName}-AppAutomate-Build`
35+
: "bstack-demo";
4136

42-
// Construct YAML content
4337
const configContent = `\`\`\`yaml
4438
userName: ${username}
4539
accessKey: ${accessKey}
@@ -48,8 +42,9 @@ platforms:
4842
${platformConfigs}
4943
parallelsPerPlatform: 1
5044
browserstackLocal: true
51-
buildName: bstack-demo
52-
projectName: BrowserStack Sample
45+
// TODO: replace projectName and buildName according to actual project
46+
projectName: ${projectName}
47+
buildName: ${buildName}
5348
debug: true
5449
networkLogs: true
5550
percy: false
@@ -63,11 +58,46 @@ accessibility: false
6358
- Set \`browserstackLocal: true\` if you need to test with local/staging servers
6459
- Adjust \`parallelsPerPlatform\` based on your subscription limits`;
6560

66-
// Return formatted step for instructions
67-
return createStep(
68-
"Update browserstack.yml file with App Automate configuration:",
69-
`Create or update the browserstack.yml file in your project root with the following content:
61+
const stepTitle =
62+
"Update browserstack.yml file with App Automate configuration:";
63+
64+
const stepDescription = `Create or update the browserstack.yml file in your project root with the following content:
65+
${configContent}`;
66+
67+
return createStep(stepTitle, stepDescription);
68+
}
69+
70+
function generatePlatformConfigs(config: {
71+
validatedEnvironments?: ValidatedEnvironment[];
72+
platforms?: string[];
73+
}): string {
74+
if (config.validatedEnvironments && config.validatedEnvironments.length > 0) {
75+
return config.validatedEnvironments
76+
.filter((env) => env.platform === "android" || env.platform === "ios")
77+
.map((env) => {
78+
return ` - platformName: ${env.platform}
79+
deviceName: "${env.deviceName}"
80+
platformVersion: "${env.osVersion}"`;
81+
})
82+
.join("\n");
83+
} else if (config.platforms && config.platforms.length > 0) {
84+
return config.platforms
85+
.map((platform) => {
86+
const devices =
87+
APP_DEVICE_CONFIGS[platform as keyof typeof APP_DEVICE_CONFIGS];
88+
if (!devices) return "";
89+
90+
return devices
91+
.map(
92+
(device) => ` - platformName: ${platform}
93+
deviceName: ${device.deviceName}
94+
platformVersion: "${device.platformVersion}"`,
95+
)
96+
.join("\n");
97+
})
98+
.filter(Boolean)
99+
.join("\n");
100+
}
70101

71-
${configContent}`,
72-
);
102+
return "";
73103
}

src/tools/appautomate-utils/appium-sdk/constants.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,35 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
4949
"The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'",
5050
),
5151

52-
desiredPlatforms: z
53-
.array(z.nativeEnum(AppSDKSupportedPlatformEnum))
52+
devices: z
53+
.array(
54+
z.union([
55+
// Android: [android, deviceName, osVersion]
56+
z.tuple([
57+
z
58+
.literal(AppSDKSupportedPlatformEnum.android)
59+
.describe("Platform identifier: 'android'"),
60+
z
61+
.string()
62+
.describe(
63+
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
64+
),
65+
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
66+
]),
67+
// iOS: [ios, deviceName, osVersion]
68+
z.tuple([
69+
z
70+
.literal(AppSDKSupportedPlatformEnum.ios)
71+
.describe("Platform identifier: 'ios'"),
72+
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
73+
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
74+
]),
75+
]),
76+
)
77+
.max(3)
78+
.default([])
5479
.describe(
55-
"The mobile platforms the user wants to test on. Always ask this to the user, do not try to infer this. Example: ['android', 'ios']",
80+
"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']]",
5681
),
5782

5883
appPath: z

src/tools/appautomate-utils/appium-sdk/handler.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { BrowserStackConfig } from "../../../lib/types.js";
44
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
5+
import { validateAppAutomateDevices } from "../../sdk-utils/common/device-validator.js";
6+
57
import {
68
getAppUploadInstruction,
79
validateSupportforAppAutomate,
@@ -36,13 +38,25 @@ export async function setupAppAutomateHandler(
3638
const testingFramework =
3739
input.detectedTestingFramework as AppSDKSupportedTestingFramework;
3840
const language = input.detectedLanguage as AppSDKSupportedLanguage;
39-
const platforms = (input.desiredPlatforms as string[]) ?? ["android"];
41+
const inputDevices = (input.devices as Array<Array<string>>) ?? [];
4042
const appPath = input.appPath as string;
4143
const framework = input.detectedFramework as SupportedFramework;
4244

4345
//Validating if supported framework or not
4446
validateSupportforAppAutomate(framework, language, testingFramework);
4547

48+
// Use default mobile devices when array is empty
49+
const devices =
50+
inputDevices.length === 0
51+
? [["android", "Samsung Galaxy S24", "latest"]]
52+
: inputDevices;
53+
54+
// Validate devices against real BrowserStack device data
55+
const validatedEnvironments = await validateAppAutomateDevices(devices);
56+
57+
// Extract platforms for backward compatibility (if needed)
58+
const platforms = validatedEnvironments.map((env) => env.platform);
59+
4660
// Step 1: Generate SDK setup command
4761
const sdkCommand = getAppSDKPrefixCommand(
4862
language,
@@ -58,11 +72,15 @@ export async function setupAppAutomateHandler(
5872

5973
// Step 2: Generate browserstack.yml configuration
6074
const configInstructions = generateAppBrowserStackYMLInstructions(
61-
platforms,
75+
{
76+
validatedEnvironments,
77+
platforms,
78+
testingFramework,
79+
projectName: input.project as string,
80+
},
6281
username,
6382
accessKey,
6483
appPath,
65-
testingFramework,
6684
);
6785

6886
if (configInstructions) {

src/tools/appautomate-utils/native-execution/constants.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from "zod";
22
import { AppTestPlatform } from "./types.js";
3+
import { AppSDKSupportedPlatformEnum } from "../appium-sdk/types.js";
34

45
export const RUN_APP_AUTOMATE_DESCRIPTION = `Execute pre-built native mobile test suites (Espresso for Android, XCUITest for iOS) by direct upload to BrowserStack. ONLY for compiled .apk/.ipa test files. This is NOT for SDK integration or Appium tests. For Appium-based testing with SDK setup, use 'setupBrowserStackAppAutomateTests' instead.`;
56

@@ -29,9 +30,34 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
2930
"If in other directory, provide existing test file path",
3031
),
3132
devices: z
32-
.array(z.string())
33+
.array(
34+
z.union([
35+
// Android: [android, deviceName, osVersion]
36+
z.tuple([
37+
z
38+
.literal(AppSDKSupportedPlatformEnum.android)
39+
.describe("Platform identifier: 'android'"),
40+
z
41+
.string()
42+
.describe(
43+
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
44+
),
45+
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
46+
]),
47+
// iOS: [ios, deviceName, osVersion]
48+
z.tuple([
49+
z
50+
.literal(AppSDKSupportedPlatformEnum.ios)
51+
.describe("Platform identifier: 'ios'"),
52+
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
53+
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
54+
]),
55+
]),
56+
)
57+
.max(3)
58+
.default([])
3359
.describe(
34-
"List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'iPhone 12 Pro-16.0'].",
60+
"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']]",
3561
),
3662
project: z
3763
.string()

0 commit comments

Comments
 (0)