Skip to content

Fix : Added browserstackAppUrl for remote mcp in the app specific tools #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

29 changes: 19 additions & 10 deletions src/tools/appautomate-utils/appautomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,17 @@ export function resolveVersion(
export function validateArgs(args: {
desiredPlatform: string;
desiredPlatformVersion: string;
appPath: string;
appPath?: string;
desiredPhone: string;
browserstackAppUrl?: string;
}): void {
const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone } =
args;
const {
desiredPlatform,
desiredPlatformVersion,
appPath,
desiredPhone,
browserstackAppUrl,
} = args;

if (!desiredPlatform || !desiredPhone) {
throw new Error(
Expand All @@ -108,16 +114,19 @@ export function validateArgs(args: {
);
}

if (!appPath) {
throw new Error("You must provide an appPath.");
if (!appPath && !browserstackAppUrl) {
throw new Error("Either appPath or browserstackAppUrl must be provided");
}

if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
throw new Error("You must provide a valid Android app path (.apk).");
}
// Only validate app path format if appPath is provided
if (appPath) {
if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
throw new Error("You must provide a valid Android app path (.apk).");
}

if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
throw new Error("You must provide a valid iOS app path (.ipa).");
if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
throw new Error("You must provide a valid iOS app path (.ipa).");
}
}
}

Expand Down
100 changes: 84 additions & 16 deletions src/tools/appautomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,21 @@ enum Platform {
async function takeAppScreenshot(args: {
desiredPlatform: Platform;
desiredPlatformVersion: string;
appPath: string;
appPath?: string;
desiredPhone: string;
browserstackAppUrl?: string;
config: BrowserStackConfig;
}): Promise<CallToolResult> {
let driver;
try {
validateArgs(args);
const { desiredPlatform, desiredPhone, appPath, config } = args;
const {
desiredPlatform,
desiredPhone,
appPath,
browserstackAppUrl,
config,
} = args;
let { desiredPlatformVersion } = args;

const platforms = (
Expand Down Expand Up @@ -98,8 +105,19 @@ async function takeAppScreenshot(args: {
const authString = getBrowserStackAuth(config);
const [username, password] = authString.split(":");

const app_url = await uploadApp(appPath, username, password);
logger.info(`App uploaded. URL: ${app_url}`);
let app_url: string;
if (browserstackAppUrl) {
app_url = browserstackAppUrl;
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
} else {
if (!appPath) {
throw new Error(
"appPath is required when browserstackAppUrl is not provided",
);
}
app_url = await uploadApp(appPath, username, password);
logger.info(`App uploaded. URL: ${app_url}`);
}

const capabilities = {
platformName: desiredPlatform,
Expand Down Expand Up @@ -157,22 +175,54 @@ async function takeAppScreenshot(args: {
//Runs AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.
async function runAppTestsOnBrowserStack(
args: {
appPath: string;
testSuitePath: string;
appPath?: string;
testSuitePath?: string;
browserstackAppUrl?: string;
browserstackTestSuiteUrl?: string;
devices: string[];
project: string;
detectedAutomationFramework: string;
},
config: BrowserStackConfig,
): Promise<CallToolResult> {
// Validate that either paths or URLs are provided for both app and test suite
if (!args.browserstackAppUrl && !args.appPath) {
throw new Error(
"appPath is required when browserstackAppUrl is not provided",
);
}
if (!args.browserstackTestSuiteUrl && !args.testSuitePath) {
throw new Error(
"testSuitePath is required when browserstackTestSuiteUrl is not provided",
);
}

switch (args.detectedAutomationFramework) {
case AppTestPlatform.ESPRESSO: {
try {
const app_url = await uploadEspressoApp(args.appPath, config);
const test_suite_url = await uploadEspressoTestSuite(
args.testSuitePath,
config,
);
let app_url: string;
if (args.browserstackAppUrl) {
app_url = args.browserstackAppUrl;
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
} else {
app_url = await uploadEspressoApp(args.appPath!, config);
logger.info(`App uploaded. URL: ${app_url}`);
}

let test_suite_url: string;
if (args.browserstackTestSuiteUrl) {
test_suite_url = args.browserstackTestSuiteUrl;
logger.info(
`Using provided BrowserStack test suite URL: ${test_suite_url}`,
);
} else {
test_suite_url = await uploadEspressoTestSuite(
args.testSuitePath!,
config,
);
logger.info(`Test suite uploaded. URL: ${test_suite_url}`);
}

const build_id = await triggerEspressoBuild(
app_url,
test_suite_url,
Expand All @@ -195,11 +245,29 @@ async function runAppTestsOnBrowserStack(
}
case AppTestPlatform.XCUITEST: {
try {
const app_url = await uploadXcuiApp(args.appPath, config);
const test_suite_url = await uploadXcuiTestSuite(
args.testSuitePath,
config,
);
let app_url: string;
if (args.browserstackAppUrl) {
app_url = args.browserstackAppUrl;
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
} else {
app_url = await uploadXcuiApp(args.appPath!, config);
logger.info(`App uploaded. URL: ${app_url}`);
}

let test_suite_url: string;
if (args.browserstackTestSuiteUrl) {
test_suite_url = args.browserstackTestSuiteUrl;
logger.info(
`Using provided BrowserStack test suite URL: ${test_suite_url}`,
);
} else {
test_suite_url = await uploadXcuiTestSuite(
args.testSuitePath!,
config,
);
logger.info(`Test suite uploaded. URL: ${test_suite_url}`);
}

const build_id = await triggerXcuiBuild(
app_url,
test_suite_url,
Expand Down
34 changes: 26 additions & 8 deletions src/tools/applive-utils/start-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import { BrowserStackConfig } from "../../lib/types.js";
import envConfig from "../../config.js";

interface StartSessionArgs {
appPath: string;
appPath?: string;
desiredPlatform: "android" | "ios";
desiredPhone: string;
desiredPlatformVersion: string;
browserstackAppUrl?: string;
}

interface StartSessionOptions {
Expand All @@ -31,8 +32,13 @@ export async function startSession(
args: StartSessionArgs,
options: StartSessionOptions,
): Promise<string> {
const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion } =
args;
const {
appPath,
desiredPlatform,
desiredPhone,
desiredPlatformVersion,
browserstackAppUrl,
} = args;
const { config } = options;

// 1) Fetch devices for APP_LIVE
Expand Down Expand Up @@ -71,11 +77,23 @@ export async function startSession(
note = `\n Note: The requested version "${desiredPlatformVersion}" is not available. Using "${version}" instead.`;
}

// 6) Upload app
const authString = getBrowserStackAuth(config);
const [username, password] = authString.split(":");
const { app_url } = await uploadApp(appPath, username, password);
logger.info(`App uploaded: ${app_url}`);
// 6) Upload app or use provided URL
let app_url: string;
if (browserstackAppUrl) {
app_url = browserstackAppUrl;
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
} else {
if (!appPath) {
throw new Error(
"appPath is required when browserstackAppUrl is not provided",
);
}
const authString = getBrowserStackAuth(config);
const [username, password] = authString.split(":");
const result = await uploadApp(appPath, username, password);
app_url = result.app_url;
logger.info(`App uploaded: ${app_url}`);
}

if (!app_url) {
throw new Error("Failed to upload app. Please try again.");
Expand Down
39 changes: 22 additions & 17 deletions src/tools/applive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,44 @@ export async function startAppLiveSession(
args: {
desiredPlatform: string;
desiredPlatformVersion: string;
appPath: string;
appPath?: string;
desiredPhone: string;
browserstackAppUrl?: string;
},
config: BrowserStackConfig,
): Promise<CallToolResult> {
if (!args.desiredPlatform) {
throw new Error("You must provide a desiredPlatform.");
}

if (!args.appPath) {
throw new Error("You must provide a appPath.");
if (!args.appPath && !args.browserstackAppUrl) {
throw new Error("You must provide either appPath or browserstackAppUrl.");
}

if (!args.desiredPhone) {
throw new Error("You must provide a desiredPhone.");
}

if (args.desiredPlatform === "android" && !args.appPath.endsWith(".apk")) {
throw new Error("You must provide a valid Android app path.");
}
// Only validate app path if it's provided (not using browserstackAppUrl)
if (args.appPath) {
if (args.desiredPlatform === "android" && !args.appPath.endsWith(".apk")) {
throw new Error("You must provide a valid Android app path.");
}

if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
throw new Error("You must provide a valid iOS app path.");
}
if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
throw new Error("You must provide a valid iOS app path.");
}

// check if the app path exists && is readable
try {
if (!fs.existsSync(args.appPath)) {
throw new Error("The app path does not exist.");
// check if the app path exists && is readable
try {
if (!fs.existsSync(args.appPath)) {
throw new Error("The app path does not exist.");
}
fs.accessSync(args.appPath, fs.constants.R_OK);
} catch (error) {
logger.error("The app path does not exist or is not readable: %s", error);
throw new Error("The app path does not exist or is not readable.");
}
fs.accessSync(args.appPath, fs.constants.R_OK);
} catch (error) {
logger.error("The app path does not exist or is not readable: %s", error);
throw new Error("The app path does not exist or is not readable.");
}

const launchUrl = await startSession(
Expand All @@ -56,6 +60,7 @@ export async function startAppLiveSession(
desiredPlatform: args.desiredPlatform as "android" | "ios",
desiredPhone: args.desiredPhone,
desiredPlatformVersion: args.desiredPlatformVersion,
browserstackAppUrl: args.browserstackAppUrl,
},
{ config },
);
Expand Down
2 changes: 1 addition & 1 deletion tests/tools/appautomate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('appautomate utils', () => {

it('should fail if app path is not provided', () => {
const args = { ...validAndroidArgs, appPath: '' };
expect(() => validateArgs(args)).toThrow('You must provide an appPath');
expect(() => validateArgs(args)).toThrow('Either appPath or browserstackAppUrl must be provided');
});

it('should fail if phone is not provided', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/tools/applive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('startAppLiveSession', () => {

it('should fail if app path is not provided', async () => {
const args = { ...validAndroidArgs, appPath: '' };
await expect(startAppLiveSession(args, mockConfig)).rejects.toThrow('You must provide a appPath');
await expect(startAppLiveSession(args, mockConfig)).rejects.toThrow('You must provide either appPath or browserstackAppUrl.');
});

it('should fail if phone is not provided', async () => {
Expand Down