Skip to content

Commit 61847da

Browse files
committed
feat: introduce validations for BrowserStack devices
1 parent 3a61131 commit 61847da

File tree

5 files changed

+215
-19
lines changed

5 files changed

+215
-19
lines changed

src/tools/appautomate-utils/appium-sdk/config-generator.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DEFAULT_APP_PATH,
66
createStep,
77
} from "./index.js";
8+
import { ValidatedEnvironment } from "../../sdk-utils/common/device-validator.js";
89

910
export function generateAppBrowserStackYMLInstructions(
1011
platforms: string[],
@@ -71,3 +72,56 @@ accessibility: false
7172
${configContent}`,
7273
);
7374
}
75+
76+
/**
77+
* Generate App Automate browserstack.yml from validated device configurations
78+
*/
79+
export function generateAppAutomateYML(
80+
validatedEnvironments: ValidatedEnvironment[],
81+
username: string,
82+
accessKey: string,
83+
appPath: string = DEFAULT_APP_PATH,
84+
projectName: string,
85+
): string {
86+
// Generate platform configurations from validated environments
87+
const platformConfigs = validatedEnvironments
88+
.filter((env) => env.platform === "android" || env.platform === "ios")
89+
.map((env) => {
90+
return ` - platformName: ${env.platform}
91+
deviceName: "${env.deviceName}"
92+
platformVersion: "${env.osVersion}"`;
93+
})
94+
.join("\n");
95+
96+
// Construct YAML content with validated data
97+
const configContent = `\`\`\`yaml
98+
userName: ${username}
99+
accessKey: ${accessKey}
100+
app: ${appPath}
101+
platforms:
102+
${platformConfigs}
103+
parallelsPerPlatform: 1
104+
browserstackLocal: true
105+
buildName: ${projectName}-AppAutomate-Build
106+
projectName: ${projectName}
107+
debug: true
108+
networkLogs: true
109+
percy: false
110+
percyCaptureMode: auto
111+
accessibility: false
112+
\`\`\`
113+
114+
**Important notes:**
115+
- Replace \`app: ${appPath}\` with the path to your actual app file (e.g., \`./SampleApp.apk\` for Android or \`./SampleApp.ipa\` for iOS)
116+
- You can upload your app using BrowserStack's App Upload API or manually through the dashboard
117+
- Set \`browserstackLocal: true\` if you need to test with local/staging servers
118+
- Adjust \`parallelsPerPlatform\` based on your subscription limits`;
119+
120+
// Return formatted step for instructions
121+
return createStep(
122+
"Update browserstack.yml file with validated App Automate configuration:",
123+
`Create or update the browserstack.yml file in your project root with your validated device configurations:
124+
125+
${configContent}`,
126+
);
127+
}

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

Lines changed: 26 additions & 7 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 { generateAppAutomateYML } from "./config-generator.js";
6+
57
import {
68
getAppUploadInstruction,
79
validateSupportforAppAutomate,
@@ -43,6 +45,10 @@ export async function setupAppAutomateHandler(
4345
//Validating if supported framework or not
4446
validateSupportforAppAutomate(framework, language, testingFramework);
4547

48+
// For app automate, we don't have individual device validation like in automate
49+
// The platforms array already contains the desired platforms
50+
const validatedEnvironments: any[] = [];
51+
4652
// Step 1: Generate SDK setup command
4753
const sdkCommand = getAppSDKPrefixCommand(
4854
language,
@@ -57,13 +63,26 @@ export async function setupAppAutomateHandler(
5763
}
5864

5965
// Step 2: Generate browserstack.yml configuration
60-
const configInstructions = generateAppBrowserStackYMLInstructions(
61-
platforms,
62-
username,
63-
accessKey,
64-
appPath,
65-
testingFramework,
66-
);
66+
let configInstructions;
67+
if (validatedEnvironments.length > 0) {
68+
// Use validated environments for YML generation
69+
configInstructions = generateAppAutomateYML(
70+
validatedEnvironments,
71+
username,
72+
accessKey,
73+
appPath,
74+
input.project as string,
75+
);
76+
} else {
77+
// Fallback to original method
78+
configInstructions = generateAppBrowserStackYMLInstructions(
79+
platforms,
80+
username,
81+
accessKey,
82+
appPath,
83+
testingFramework,
84+
);
85+
}
6786

6887
if (configInstructions) {
6988
instructions.push({ content: configInstructions, type: "config" });

src/tools/sdk-utils/bstack/configUtils.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Utilities for generating BrowserStack configuration files.
33
*/
4+
import { ValidatedEnvironment } from "../common/device-validator.js";
45

56
export function generateBrowserStackYMLInstructions(
67
desiredPlatforms: string[],
@@ -70,3 +71,98 @@ Create a browserstack.yml file in the project root. The file should be in the fo
7071
\`\`\`
7172
\n`;
7273
}
74+
75+
/**
76+
* Generate browserstack.yml content from validated device configurations
77+
*/
78+
export function generateBrowserStackYMLFromValidatedEnvironments(
79+
validatedEnvironments: ValidatedEnvironment[],
80+
enablePercy: boolean = false,
81+
projectName: string,
82+
) {
83+
// Generate platforms array from validated environments
84+
const platforms = validatedEnvironments.map((env) => {
85+
if (env.platform === "windows" || env.platform === "macos") {
86+
// Desktop configuration
87+
return {
88+
os: env.platform === "windows" ? "Windows" : "OS X",
89+
osVersion: env.osVersion,
90+
browserName: env.browser,
91+
browserVersion: env.browserVersion || "latest",
92+
};
93+
} else {
94+
// Mobile configuration (android/ios)
95+
return {
96+
deviceName: env.deviceName,
97+
osVersion: env.osVersion,
98+
browserName: env.browser,
99+
};
100+
}
101+
});
102+
103+
// Convert platforms to YAML format
104+
const platformsYAML = platforms
105+
.map((platform) => {
106+
if (platform.deviceName) {
107+
// Mobile platform
108+
return ` - deviceName: "${platform.deviceName}"
109+
osVersion: "${platform.osVersion}"
110+
browserName: ${platform.browserName}`;
111+
} else {
112+
// Desktop platform
113+
return ` - os: ${platform.os}
114+
osVersion: "${platform.osVersion}"
115+
browserName: ${platform.browserName}
116+
browserVersion: ${platform.browserVersion}`;
117+
}
118+
})
119+
.join("\n");
120+
121+
let ymlContent = `
122+
# ======================
123+
# BrowserStack Reporting
124+
# ======================
125+
projectName: ${projectName}
126+
buildName: ${projectName}-Build
127+
128+
# =======================================
129+
# Platforms (Browsers / Devices to test)
130+
# =======================================
131+
# Auto-generated from validated device configurations
132+
platforms:
133+
${platformsYAML}
134+
135+
# =======================
136+
# Parallels per Platform
137+
# =======================
138+
parallelsPerPlatform: 1
139+
140+
# =================
141+
# Local Testing
142+
# =================
143+
browserstackLocal: true
144+
145+
# ===================
146+
# Debugging features
147+
# ===================
148+
debug: true
149+
testObservability: true`;
150+
151+
if (enablePercy) {
152+
ymlContent += `
153+
154+
# =====================
155+
# Percy Visual Testing
156+
# =====================
157+
percy: true
158+
percyCaptureMode: manual`;
159+
}
160+
161+
return `
162+
---STEP---
163+
Create a browserstack.yml file in the project root with your validated device configurations:
164+
165+
\`\`\`yaml${ymlContent}
166+
\`\`\`
167+
\n`;
168+
}

src/tools/sdk-utils/bstack/sdkHandler.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js";
33
import { RunTestsOnBrowserStackInput } from "../common/schema.js";
44
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
55
import { getSDKPrefixCommand } from "./commands.js";
6-
import { generateBrowserStackYMLInstructions } from "./configUtils.js";
6+
import { generateBrowserStackYMLFromValidatedEnvironments } from "./configUtils.js";
77
import { getInstructionsForProjectConfiguration } from "../common/instructionUtils.js";
88
import { BrowserStackConfig } from "../../../lib/types.js";
99
import { validateDevices } from "../common/device-validator.js";
@@ -27,7 +27,7 @@ export async function runBstackSDKOnly(
2727
| Array<Array<string>>
2828
| undefined;
2929

30-
await validateDevices(
30+
const validatedEnvironments = await validateDevices(
3131
tupleTargets || [],
3232
input.detectedBrowserAutomationFramework,
3333
);
@@ -86,8 +86,8 @@ export async function runBstackSDKOnly(
8686
});
8787
}
8888

89-
const ymlInstructions = generateBrowserStackYMLInstructions(
90-
(tupleTargets || []).map((tuple) => tuple.join(" ")),
89+
const ymlInstructions = generateBrowserStackYMLFromValidatedEnvironments(
90+
validatedEnvironments,
9191
false,
9292
input.projectName,
9393
);

src/tools/sdk-utils/common/device-validator.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,12 @@ async function validateMobileEnvironment(
357357
5,
358358
);
359359
if (deviceMatches.length === 0) {
360-
throw new Error(`No ${platform} devices matching "${deviceName}"`);
360+
throw new Error(
361+
`No ${platform} devices matching "${deviceName}". Available devices: ${platformEntries
362+
.map((d) => d.display_name || d.device || "unknown")
363+
.slice(0, 5)
364+
.join(", ")}`,
365+
);
361366
}
362367

363368
const exactMatch = deviceMatches.find(
@@ -390,19 +395,41 @@ async function validateMobileEnvironment(
390395
// Validate browser if provided
391396
let validatedBrowser = browser || defaultBrowser;
392397
if (browser && osFiltered.length > 0) {
398+
// Extract browsers more carefully - handle different possible structures
393399
const availableBrowsers = [
394400
...new Set(
395-
osFiltered.flatMap((d) => d.browsers?.map((b: any) => b.browser) || []),
401+
osFiltered.flatMap((d) => {
402+
if (d.browsers && Array.isArray(d.browsers)) {
403+
// If browsers is an array of objects with browser property
404+
return d.browsers
405+
.map((b: any) => {
406+
// Use display_name for user-friendly browser names, fallback to browser field
407+
return b.display_name || b.browser || b.browserName || b.name;
408+
})
409+
.filter(Boolean);
410+
} else if (d.browser) {
411+
// If there's just a browser property directly
412+
return [d.browser];
413+
}
414+
// For mobile devices, provide default browsers if none found
415+
return platform === "android" ? ["chrome"] : ["safari"];
416+
}),
396417
),
397-
] as string[];
418+
].filter(Boolean) as string[];
398419

399420
if (availableBrowsers.length > 0) {
400-
validatedBrowser = validateBrowser(browser, availableBrowsers);
421+
try {
422+
validatedBrowser = validateBrowser(browser, availableBrowsers);
423+
} catch (error) {
424+
// Add more context to browser validation errors
425+
throw new Error(
426+
`Failed to validate browser "${browser}" for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". ${error instanceof Error ? error.message : String(error)}`,
427+
);
428+
}
401429
} else {
402-
// If no browsers available for this device/OS combination, throw error
403-
throw new Error(
404-
`Browser "${browser}" not available for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". No browsers found for this configuration.`,
405-
);
430+
// For mobile, if no specific browsers found, just use the requested browser
431+
// as most mobile devices support standard browsers
432+
validatedBrowser = browser || defaultBrowser;
406433
}
407434
}
408435

0 commit comments

Comments
 (0)