Skip to content

Commit d2e2374

Browse files
Merge pull request #130 from tech-sushant/app-automate
feat: Add support in App Automate for Appium
2 parents cf44ae7 + 7517950 commit d2e2374

File tree

19 files changed

+1537
-72
lines changed

19 files changed

+1537
-72
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Configuration utilities for BrowserStack App SDK
2+
import {
3+
APP_DEVICE_CONFIGS,
4+
AppSDKSupportedTestingFrameworkEnum,
5+
DEFAULT_APP_PATH,
6+
createStep,
7+
} from "./index.js";
8+
9+
export function generateAppBrowserStackYMLInstructions(
10+
platforms: string[],
11+
username: string,
12+
accessKey: string,
13+
appPath: string = DEFAULT_APP_PATH,
14+
testingFramework: string,
15+
): string {
16+
if (
17+
testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch ||
18+
testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio ||
19+
testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby
20+
) {
21+
return "";
22+
}
23+
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+
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");
41+
42+
// Construct YAML content
43+
const configContent = `\`\`\`yaml
44+
userName: ${username}
45+
accessKey: ${accessKey}
46+
app: ${appPath}
47+
platforms:
48+
${platformConfigs}
49+
parallelsPerPlatform: 1
50+
browserstackLocal: true
51+
buildName: bstack-demo
52+
projectName: BrowserStack Sample
53+
debug: true
54+
networkLogs: true
55+
percy: false
56+
percyCaptureMode: auto
57+
accessibility: false
58+
\`\`\`
59+
60+
**Important notes:**
61+
- Replace \`app: ${appPath}\` with the path to your actual app file (e.g., \`./SampleApp.apk\` for Android or \`./SampleApp.ipa\` for iOS)
62+
- You can upload your app using BrowserStack's App Upload API or manually through the dashboard
63+
- Set \`browserstackLocal: true\` if you need to test with local/staging servers
64+
- Adjust \`parallelsPerPlatform\` based on your subscription limits`;
65+
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:
70+
71+
${configContent}`,
72+
);
73+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { z } from "zod";
2+
import {
3+
AppSDKSupportedFrameworkEnum,
4+
AppSDKSupportedTestingFrameworkEnum,
5+
AppSDKSupportedLanguageEnum,
6+
AppSDKSupportedPlatformEnum,
7+
} from "./index.js";
8+
9+
// App Automate specific device configurations
10+
export const APP_DEVICE_CONFIGS = {
11+
android: [
12+
{ deviceName: "Samsung Galaxy S22 Ultra", platformVersion: "12.0" },
13+
{ deviceName: "Google Pixel 7 Pro", platformVersion: "13.0" },
14+
{ deviceName: "OnePlus 9", platformVersion: "11.0" },
15+
],
16+
ios: [
17+
{ deviceName: "iPhone 14", platformVersion: "16" },
18+
{ deviceName: "iPhone 13", platformVersion: "15" },
19+
{ deviceName: "iPad Air 4", platformVersion: "14" },
20+
],
21+
};
22+
23+
// Step delimiter for parsing instructions
24+
export const STEP_DELIMITER = "---STEP---";
25+
26+
// Default app path for examples
27+
export const DEFAULT_APP_PATH = "bs://sample.app";
28+
29+
// Tool description and schema for setupBrowserStackAppAutomateTests
30+
export const SETUP_APP_AUTOMATE_DESCRIPTION =
31+
"Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead.";
32+
33+
export const SETUP_APP_AUTOMATE_SCHEMA = {
34+
detectedFramework: z
35+
.nativeEnum(AppSDKSupportedFrameworkEnum)
36+
.describe(
37+
"The mobile automation framework configured in the project. Example: 'appium'",
38+
),
39+
40+
detectedTestingFramework: z
41+
.nativeEnum(AppSDKSupportedTestingFrameworkEnum)
42+
.describe(
43+
"The testing framework used in the project. Be precise with framework selection Example: 'testng', 'behave', 'pytest', 'robot'",
44+
),
45+
46+
detectedLanguage: z
47+
.nativeEnum(AppSDKSupportedLanguageEnum)
48+
.describe(
49+
"The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'",
50+
),
51+
52+
desiredPlatforms: z
53+
.array(z.nativeEnum(AppSDKSupportedPlatformEnum))
54+
.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']",
56+
),
57+
58+
appPath: z
59+
.string()
60+
.describe(
61+
"Path to the mobile app file (.apk for Android, .ipa for iOS). Can be a local file path or a BrowserStack app URL (bs://)",
62+
),
63+
project: z
64+
.string()
65+
.optional()
66+
.default("BStack-AppAutomate-Suite")
67+
.describe("Project name for organizing test runs on BrowserStack."),
68+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { STEP_DELIMITER } from "./constants.js";
3+
4+
export function formatFinalAppInstructions(
5+
formattedInstructions: string,
6+
): CallToolResult {
7+
const fullInstructions = `
8+
⚠️ IMPORTANT: DO NOT SKIP ANY STEP
9+
All the setup steps described in this file MUST be executed regardless of any existing configuration or setup.
10+
This ensures proper BrowserStack App Automate SDK setup.
11+
Each step is compulsory and sequence needs to be maintained.
12+
13+
${formattedInstructions}`;
14+
15+
return {
16+
content: [
17+
{
18+
type: "text",
19+
text: fullInstructions,
20+
isError: false,
21+
},
22+
],
23+
};
24+
}
25+
26+
export function createStep(title: string, content: string): string {
27+
return `${STEP_DELIMITER}
28+
${title}
29+
30+
${content}`;
31+
}
32+
33+
export function combineInstructions(...instructionParts: string[]): string {
34+
return instructionParts.filter(Boolean).join("\n\n");
35+
}
36+
37+
export function formatEnvCommands(
38+
username: string,
39+
accessKey: string,
40+
isWindows: boolean,
41+
): string {
42+
if (isWindows) {
43+
return `\`\`\`cmd
44+
setx BROWSERSTACK_USERNAME "${username}"
45+
setx BROWSERSTACK_ACCESS_KEY "${accessKey}"
46+
\`\`\``;
47+
}
48+
return `\`\`\`bash
49+
export BROWSERSTACK_USERNAME=${username}
50+
export BROWSERSTACK_ACCESS_KEY=${accessKey}
51+
\`\`\``;
52+
}
53+
54+
export function createEnvStep(
55+
username: string,
56+
accessKey: string,
57+
isWindows: boolean,
58+
platformLabel: string,
59+
title: string = "Set BrowserStack credentials as environment variables:",
60+
): string {
61+
return createStep(
62+
title,
63+
`**${platformLabel}:**
64+
${formatEnvCommands(username, accessKey, isWindows)}`,
65+
);
66+
}
67+
68+
export function formatMultiLineCommand(
69+
command: string,
70+
isWindows: boolean = process.platform === "win32",
71+
): string {
72+
if (isWindows) {
73+
// For Windows, keep commands on single line
74+
return command.replace(/\s*\\\s*\n\s*/g, " ");
75+
}
76+
return command;
77+
}
78+
79+
export function formatAppInstructionsWithNumbers(instructions: string): string {
80+
const steps = instructions
81+
.split(STEP_DELIMITER)
82+
.filter((step) => step.trim());
83+
84+
return steps
85+
.map((step, index) => `**Step ${index + 1}:**\n${step.trim()}`)
86+
.join("\n\n");
87+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { BrowserStackConfig } from "../../../lib/types.js";
4+
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
5+
import {
6+
getAppUploadInstruction,
7+
validateSupportforAppAutomate,
8+
SupportedFramework,
9+
} from "./utils.js";
10+
11+
import {
12+
getAppSDKPrefixCommand,
13+
generateAppBrowserStackYMLInstructions,
14+
} from "./index.js";
15+
16+
import {
17+
AppSDKSupportedLanguage,
18+
AppSDKSupportedTestingFramework,
19+
AppSDKInstruction,
20+
formatAppInstructionsWithNumbers,
21+
getAppInstructionsForProjectConfiguration,
22+
SETUP_APP_AUTOMATE_SCHEMA,
23+
} from "./index.js";
24+
25+
export async function setupAppAutomateHandler(
26+
rawInput: unknown,
27+
config: BrowserStackConfig,
28+
): Promise<CallToolResult> {
29+
const input = z.object(SETUP_APP_AUTOMATE_SCHEMA).parse(rawInput);
30+
const auth = getBrowserStackAuth(config);
31+
const [username, accessKey] = auth.split(":");
32+
33+
const instructions: AppSDKInstruction[] = [];
34+
35+
// Use variables for all major input properties
36+
const testingFramework =
37+
input.detectedTestingFramework as AppSDKSupportedTestingFramework;
38+
const language = input.detectedLanguage as AppSDKSupportedLanguage;
39+
const platforms = (input.desiredPlatforms as string[]) ?? ["android"];
40+
const appPath = input.appPath as string;
41+
const framework = input.detectedFramework as SupportedFramework;
42+
43+
//Validating if supported framework or not
44+
validateSupportforAppAutomate(framework, language, testingFramework);
45+
46+
// Step 1: Generate SDK setup command
47+
const sdkCommand = getAppSDKPrefixCommand(
48+
language,
49+
testingFramework,
50+
username,
51+
accessKey,
52+
appPath,
53+
);
54+
55+
if (sdkCommand) {
56+
instructions.push({ content: sdkCommand, type: "setup" });
57+
}
58+
59+
// Step 2: Generate browserstack.yml configuration
60+
const configInstructions = generateAppBrowserStackYMLInstructions(
61+
platforms,
62+
username,
63+
accessKey,
64+
appPath,
65+
testingFramework,
66+
);
67+
68+
if (configInstructions) {
69+
instructions.push({ content: configInstructions, type: "config" });
70+
}
71+
72+
// Step 3: Generate app upload instruction
73+
const appUploadInstruction = await getAppUploadInstruction(
74+
appPath,
75+
username,
76+
accessKey,
77+
testingFramework,
78+
);
79+
80+
if (appUploadInstruction) {
81+
instructions.push({ content: appUploadInstruction, type: "setup" });
82+
}
83+
84+
// Step 4: Generate project configuration and run instructions
85+
const projectInstructions = getAppInstructionsForProjectConfiguration(
86+
framework,
87+
testingFramework,
88+
language,
89+
);
90+
91+
if (projectInstructions) {
92+
instructions.push({ content: projectInstructions, type: "run" });
93+
}
94+
95+
const combinedInstructions = instructions
96+
.map((instruction) => instruction.content)
97+
.join("\n\n");
98+
99+
return {
100+
content: [
101+
{
102+
type: "text",
103+
text: formatAppInstructionsWithNumbers(combinedInstructions),
104+
isError: false,
105+
},
106+
],
107+
isError: false,
108+
};
109+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Barrel exports for App BrowserStack module
2+
export {
3+
getAppSDKPrefixCommand,
4+
getAppInstructionsForProjectConfiguration,
5+
} from "./instructions.js";
6+
export { generateAppBrowserStackYMLInstructions } from "./config-generator.js";
7+
8+
export * from "./types.js";
9+
export * from "./constants.js";
10+
export * from "./utils.js";
11+
export * from "./instructions.js";
12+
export * from "./formatter.js";

0 commit comments

Comments
 (0)