From 8cabf4a325a229481021c4c03712acba185d448a Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 4 Jun 2025 16:57:35 +0530 Subject: [PATCH 1/5] feat: add support for running AppAutomate tests on BrowserStack --- src/tools/appautomate-utils/appautomate.ts | 88 +++++++++++++++++-- src/tools/appautomate-utils/types.ts | 5 ++ src/tools/appautomate.ts | 98 +++++++++++++++++++++- 3 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 src/tools/appautomate-utils/types.ts diff --git a/src/tools/appautomate-utils/appautomate.ts b/src/tools/appautomate-utils/appautomate.ts index a52e622..596dfcc 100644 --- a/src/tools/appautomate-utils/appautomate.ts +++ b/src/tools/appautomate-utils/appautomate.ts @@ -4,6 +4,11 @@ import config from "../../config.js"; import FormData from "form-data"; import { customFuzzySearch } from "../../lib/fuzzy.js"; +const auth = { + username: config.browserstackUsername, + password: config.browserstackAccessKey, +}; + interface Device { device: string; display_name: string; @@ -138,13 +143,8 @@ export async function uploadApp(appPath: string): Promise { "https://api-cloud.browserstack.com/app-automate/upload", formData, { - headers: { - ...formData.getHeaders(), - }, - auth: { - username: config.browserstackUsername, - password: config.browserstackAccessKey, - }, + headers: formData.getHeaders(), + auth, }, ); @@ -154,3 +154,77 @@ export async function uploadApp(appPath: string): Promise { throw new Error(`Failed to upload app: ${response.data}`); } } + +// Helper to upload a file to a given BrowserStack endpoint and return a specific property from the response. +async function uploadFileToBrowserStack( + filePath: string, + endpoint: string, + responseKey: string, +): Promise { + if (!fs.existsSync(filePath)) { + throw new Error(`File not found at path: ${filePath}`); + } + + const formData = new FormData(); + formData.append("file", fs.createReadStream(filePath)); + + const response = await axios.post(endpoint, formData, { + headers: formData.getHeaders(), + auth, + }); + + if (response.data[responseKey]) { + return response.data[responseKey]; + } + + throw new Error(`Failed to upload file: ${JSON.stringify(response.data)}`); +} + +//Uploads an Android app (.apk or .aab) to BrowserStack Espresso endpoint and returns the app_url +export async function uploadEspressoApp(appPath: string): Promise { + return uploadFileToBrowserStack( + appPath, + "https://api-cloud.browserstack.com/app-automate/espresso/v2/app", + "app_url", + ); +} + +//Uploads an Espresso test suite (.apk) to BrowserStack and returns the test_suite_url +export async function uploadEspressoTestSuite( + testSuitePath: string, +): Promise { + return uploadFileToBrowserStack( + testSuitePath, + "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite", + "test_suite_url", + ); +} + +// Triggers an Espresso test run on BrowserStack and returns the build_id +export async function triggerEspressoBuild( + app_url: string, + test_suite_url: string, + devices: string[], + project: string, +): Promise { + const response = await axios.post( + "https://api-cloud.browserstack.com/app-automate/espresso/v2/build", + { + app: app_url, + testSuite: test_suite_url, + devices, + project, + }, + { + auth, + }, + ); + + if (response.data.build_id) { + return response.data.build_id; + } + + throw new Error( + `Failed to trigger Espresso build: ${JSON.stringify(response.data)}`, + ); +} diff --git a/src/tools/appautomate-utils/types.ts b/src/tools/appautomate-utils/types.ts new file mode 100644 index 0000000..16af726 --- /dev/null +++ b/src/tools/appautomate-utils/types.ts @@ -0,0 +1,5 @@ +export enum AppTestPlatform { + ESPRESSO = "espresso", + APPIUM = "appium", + XCUITEST = "xcuitest", +} diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 786b9db..06aff29 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -6,6 +6,7 @@ import config from "../config.js"; import { trackMCP } from "../lib/instrumentation.js"; import { maybeCompressBase64 } from "../lib/utils.js"; import { remote } from "webdriverio"; +import { AppTestPlatform } from "./appautomate-utils/types.js"; import { getDevicesAndBrowsers, @@ -18,6 +19,9 @@ import { resolveVersion, validateArgs, uploadApp, + uploadEspressoApp, + uploadEspressoTestSuite, + triggerEspressoBuild, } from "./appautomate-utils/appautomate.js"; // Types @@ -136,9 +140,51 @@ async function takeAppScreenshot(args: { } } -/** - * Registers the `takeAppScreenshot` tool with the MCP server. - */ +//Runs AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run. +async function runAppTestsOnBrowserStack(args: { + appPath: string; + testSuitePath: string; + devices: string[]; + project: string; + detectedAutomationFramework: string; +}): Promise { + + switch (args.detectedAutomationFramework) { + case AppTestPlatform.ESPRESSO: { + try { + const app_url = await uploadEspressoApp(args.appPath); + const test_suite_url = await uploadEspressoTestSuite( + args.testSuitePath, + ); + const build_id = await triggerEspressoBuild( + app_url, + test_suite_url, + args.devices, + args.project, + ); + + return { + content: [ + { + type: "text", + text: `āœ… Test run started successfully!\n\nšŸ”§ Build ID: ${build_id}\nšŸ”— View your build: https://app-automate.browserstack.com/builds/${build_id}`, + }, + ], + }; + } catch (err) { + logger.error("Error running App Automate test", err); + throw err; + } + } + + default: + throw new Error( + `Unsupported automation framework: ${args.detectedAutomationFramework}`, + ); + } +} + +// Registers automation tools with the MCP server. export default function addAppAutomationTools(server: McpServer) { server.tool( "takeAppScreenshot", @@ -182,4 +228,50 @@ export default function addAppAutomationTools(server: McpServer) { } }, ); + + server.tool( + "runAppTestsOnBrowserStack", + "Run AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.", + { + appPath: z + .string() + .describe("Path to the .apk or .aab file for your app."), + testSuitePath: z + .string() + .describe("Path to the Espresso test suite .apk file."), + devices: z + .array(z.string()) + .describe( + "List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'Google Pixel 3-9.0'].", + ), + project: z + .string() + .optional() + .default("Espresso Test") + .describe("Project name for organizing test runs on BrowserStack."), + detectedAutomationFramework: z + .string() + .describe( + "The automation framework used in the project, such as 'espresso' or 'appium'.", + ), + }, + async (args) => { + try { + return await runAppTestsOnBrowserStack(args); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error running App Automate test: ${errorMessage}`, + isError: true, + }, + ], + isError: true, + }; + } + }, + ); } From f26a676a2406f1973e332ee2c3aa7d0dda80f90c Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 4 Jun 2025 17:05:22 +0530 Subject: [PATCH 2/5] fix: add tracking for runAppTestsOnBrowserStack function execution and errors --- src/tools/appautomate.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 06aff29..5fa650e 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -148,7 +148,6 @@ async function runAppTestsOnBrowserStack(args: { project: string; detectedAutomationFramework: string; }): Promise { - switch (args.detectedAutomationFramework) { case AppTestPlatform.ESPRESSO: { try { @@ -257,8 +256,17 @@ export default function addAppAutomationTools(server: McpServer) { }, async (args) => { try { + trackMCP( + "runAppTestsOnBrowserStack", + server.server.getClientVersion()!, + ); return await runAppTestsOnBrowserStack(args); } catch (error) { + trackMCP( + "runAppTestsOnBrowserStack", + server.server.getClientVersion()!, + error, + ); const errorMessage = error instanceof Error ? error.message : "Unknown error"; return { From 3ca644809a37a7c42ac9859c2fb2bb0163c6f2fa Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 4 Jun 2025 17:08:38 +0530 Subject: [PATCH 3/5] fix: remove redundant isError property --- src/tools/appautomate.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 5fa650e..757b1f4 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -273,8 +273,7 @@ export default function addAppAutomationTools(server: McpServer) { content: [ { type: "text", - text: `Error running App Automate test: ${errorMessage}`, - isError: true, + text: `Error running App Automate test: ${errorMessage}` }, ], isError: true, From 57026ac6d66714897383f3af52b06247270fa3a8 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 27 Jun 2025 01:41:09 +0530 Subject: [PATCH 4/5] add support for XCUITest on BrowserStack --- src/tools/appautomate-utils/appautomate.ts | 49 ++++++++++++++++++++++ src/tools/appautomate.ts | 47 +++++++++++++++++---- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/tools/appautomate-utils/appautomate.ts b/src/tools/appautomate-utils/appautomate.ts index 596dfcc..0083893 100644 --- a/src/tools/appautomate-utils/appautomate.ts +++ b/src/tools/appautomate-utils/appautomate.ts @@ -200,6 +200,26 @@ export async function uploadEspressoTestSuite( ); } +//Uploads an iOS app (.ipa) to BrowserStack XCUITest endpoint and returns the app_url +export async function uploadXcuiApp(appPath: string): Promise { + return uploadFileToBrowserStack( + appPath, + "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/app", + "app_url", + ); +} + +//Uploads an XCUITest test suite (.zip) to BrowserStack and returns the test_suite_url +export async function uploadXcuiTestSuite( + testSuitePath: string, +): Promise { + return uploadFileToBrowserStack( + testSuitePath, + "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/test-suite", + "test_suite_url", + ); +} + // Triggers an Espresso test run on BrowserStack and returns the build_id export async function triggerEspressoBuild( app_url: string, @@ -228,3 +248,32 @@ export async function triggerEspressoBuild( `Failed to trigger Espresso build: ${JSON.stringify(response.data)}`, ); } + +// Triggers an XCUITest run on BrowserStack and returns the build_id +export async function triggerXcuiBuild( + app_url: string, + test_suite_url: string, + devices: string[], + project: string, +): Promise { + const response = await axios.post( + "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/build", + { + app: app_url, + testSuite: test_suite_url, + devices, + project, + }, + { + auth, + }, + ); + + if (response.data.build_id) { + return response.data.build_id; + } + + throw new Error( + `Failed to trigger XCUITest build: ${JSON.stringify(response.data)}`, + ); +} diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 757b1f4..abec2d2 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -22,6 +22,9 @@ import { uploadEspressoApp, uploadEspressoTestSuite, triggerEspressoBuild, + uploadXcuiApp, + uploadXcuiTestSuite, + triggerXcuiBuild, } from "./appautomate-utils/appautomate.js"; // Types @@ -166,7 +169,7 @@ async function runAppTestsOnBrowserStack(args: { content: [ { type: "text", - text: `āœ… Test run started successfully!\n\nšŸ”§ Build ID: ${build_id}\nšŸ”— View your build: https://app-automate.browserstack.com/builds/${build_id}`, + text: `āœ… Espresso run started successfully!\n\nšŸ”§ Build ID: ${build_id}\nšŸ”— View your build: https://app-automate.browserstack.com/builds/${build_id}`, }, ], }; @@ -175,10 +178,32 @@ async function runAppTestsOnBrowserStack(args: { throw err; } } - + case AppTestPlatform.XCUITEST: { + try { + const app_url = await uploadXcuiApp(args.appPath); + const test_suite_url = await uploadXcuiTestSuite(args.testSuitePath); + const build_id = await triggerXcuiBuild( + app_url, + test_suite_url, + args.devices, + args.project, + ); + return { + content: [ + { + type: "text", + text: `āœ… XCUITest run started successfully!\n\nšŸ”§ Build ID: ${build_id}\nšŸ”— View your build: https://app-automate.browserstack.com/builds/${build_id}`, + }, + ], + }; + } catch (err) { + logger.error("Error running XCUITest App Automate test", err); + throw err; + } + } default: throw new Error( - `Unsupported automation framework: ${args.detectedAutomationFramework}`, + `Unsupported automation framework: ${args.detectedAutomationFramework}. If you need support for this framework, please open an issue at Github`, ); } } @@ -230,18 +255,22 @@ export default function addAppAutomationTools(server: McpServer) { server.tool( "runAppTestsOnBrowserStack", - "Run AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.", + "Run AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run. Supports both Espresso (Android) and XCUITest (iOS).", { appPath: z .string() - .describe("Path to the .apk or .aab file for your app."), + .describe( + "Path to the .apk/.aab (Espresso) or .ipa (XCUITest) file for your app. Export on your own in local IDEs.", + ), testSuitePath: z .string() - .describe("Path to the Espresso test suite .apk file."), + .describe( + "Path to the Espresso test suite .apk or XCUITest .zip file. Export on your own in local IDEs.", + ), devices: z .array(z.string()) .describe( - "List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'Google Pixel 3-9.0'].", + "List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'iPhone 12 Pro-16.0'].", ), project: z .string() @@ -251,7 +280,7 @@ export default function addAppAutomationTools(server: McpServer) { detectedAutomationFramework: z .string() .describe( - "The automation framework used in the project, such as 'espresso' or 'appium'.", + "The automation framework used in the project, such as 'espresso' (Android) or 'xcuitest' (iOS).", ), }, async (args) => { @@ -273,7 +302,7 @@ export default function addAppAutomationTools(server: McpServer) { content: [ { type: "text", - text: `Error running App Automate test: ${errorMessage}` + text: `Error running App Automate test: ${errorMessage}`, }, ], isError: true, From 97859ed4f390fa688293e9ab26be03f32d7a20ef Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 1 Jul 2025 11:11:01 +0530 Subject: [PATCH 5/5] fix: enhance runAppTestsOnBrowserStack tool description and update app/test path instructions --- src/tools/appautomate.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index abec2d2..556c45d 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -255,17 +255,31 @@ export default function addAppAutomationTools(server: McpServer) { server.tool( "runAppTestsOnBrowserStack", - "Run AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run. Supports both Espresso (Android) and XCUITest (iOS).", + "Run AppAutomate tests on BrowserStack by uploading app and test suite. If running from Android Studio or Xcode, the tool will help export app and test files automatically. For other environments, you'll need to provide the paths to your pre-built app and test files.", { appPath: z .string() .describe( - "Path to the .apk/.aab (Espresso) or .ipa (XCUITest) file for your app. Export on your own in local IDEs.", + "Path to your application file:\n" + + "If in development IDE directory:\n" + + "• For Android: 'gradle assembleDebug'\n" + + "• For iOS:\n" + + " xcodebuild clean -scheme YOUR_SCHEME && \\\n" + + " xcodebuild archive -scheme YOUR_SCHEME -configuration Release -archivePath build/app.xcarchive && \\\n" + + " xcodebuild -exportArchive -archivePath build/app.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist\n\n" + + "If in other directory, provide existing app path" ), testSuitePath: z .string() .describe( - "Path to the Espresso test suite .apk or XCUITest .zip file. Export on your own in local IDEs.", + "Path to your test suite file:\n" + + "If in development IDE directory:\n" + + "• For Android: 'gradle assembleAndroidTest'\n" + + "• For iOS:\n" + + " xcodebuild test-without-building -scheme YOUR_SCHEME -destination 'generic/platform=iOS' && \\\n" + + " cd ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug-iphonesimulator/ && \\\n" + + " zip -r Tests.zip *.xctestrun *-Runner.app\n\n" + + "If in other directory, provide existing test file path" ), devices: z .array(z.string()) @@ -275,7 +289,7 @@ export default function addAppAutomationTools(server: McpServer) { project: z .string() .optional() - .default("Espresso Test") + .default("BStack-AppAutomate-Suite") .describe("Project name for organizing test runs on BrowserStack."), detectedAutomationFramework: z .string()