Skip to content

Commit feb6f2b

Browse files
authored
Merge pull request #114 from ruturaj-browserstack/app-live-automate-enablement-remote-mcp
Fix : Added browserstackAppUrl for remote mcp in the app specific tools
2 parents ae1a149 + b320c5f commit feb6f2b

File tree

6 files changed

+153
-53
lines changed

6 files changed

+153
-53
lines changed

src/tools/appautomate-utils/appautomate.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,17 @@ export function resolveVersion(
9090
export function validateArgs(args: {
9191
desiredPlatform: string;
9292
desiredPlatformVersion: string;
93-
appPath: string;
93+
appPath?: string;
9494
desiredPhone: string;
95+
browserstackAppUrl?: string;
9596
}): void {
96-
const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone } =
97-
args;
97+
const {
98+
desiredPlatform,
99+
desiredPlatformVersion,
100+
appPath,
101+
desiredPhone,
102+
browserstackAppUrl,
103+
} = args;
98104

99105
if (!desiredPlatform || !desiredPhone) {
100106
throw new Error(
@@ -108,16 +114,19 @@ export function validateArgs(args: {
108114
);
109115
}
110116

111-
if (!appPath) {
112-
throw new Error("You must provide an appPath.");
117+
if (!appPath && !browserstackAppUrl) {
118+
throw new Error("Either appPath or browserstackAppUrl must be provided");
113119
}
114120

115-
if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
116-
throw new Error("You must provide a valid Android app path (.apk).");
117-
}
121+
// Only validate app path format if appPath is provided
122+
if (appPath) {
123+
if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
124+
throw new Error("You must provide a valid Android app path (.apk).");
125+
}
118126

119-
if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
120-
throw new Error("You must provide a valid iOS app path (.ipa).");
127+
if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
128+
throw new Error("You must provide a valid iOS app path (.ipa).");
129+
}
121130
}
122131
}
123132

src/tools/appautomate.ts

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,21 @@ enum Platform {
5353
async function takeAppScreenshot(args: {
5454
desiredPlatform: Platform;
5555
desiredPlatformVersion: string;
56-
appPath: string;
56+
appPath?: string;
5757
desiredPhone: string;
58+
browserstackAppUrl?: string;
5859
config: BrowserStackConfig;
5960
}): Promise<CallToolResult> {
6061
let driver;
6162
try {
6263
validateArgs(args);
63-
const { desiredPlatform, desiredPhone, appPath, config } = args;
64+
const {
65+
desiredPlatform,
66+
desiredPhone,
67+
appPath,
68+
browserstackAppUrl,
69+
config,
70+
} = args;
6471
let { desiredPlatformVersion } = args;
6572

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

101-
const app_url = await uploadApp(appPath, username, password);
102-
logger.info(`App uploaded. URL: ${app_url}`);
108+
let app_url: string;
109+
if (browserstackAppUrl) {
110+
app_url = browserstackAppUrl;
111+
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
112+
} else {
113+
if (!appPath) {
114+
throw new Error(
115+
"appPath is required when browserstackAppUrl is not provided",
116+
);
117+
}
118+
app_url = await uploadApp(appPath, username, password);
119+
logger.info(`App uploaded. URL: ${app_url}`);
120+
}
103121

104122
const capabilities = {
105123
platformName: desiredPlatform,
@@ -157,22 +175,54 @@ async function takeAppScreenshot(args: {
157175
//Runs AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.
158176
async function runAppTestsOnBrowserStack(
159177
args: {
160-
appPath: string;
161-
testSuitePath: string;
178+
appPath?: string;
179+
testSuitePath?: string;
180+
browserstackAppUrl?: string;
181+
browserstackTestSuiteUrl?: string;
162182
devices: string[];
163183
project: string;
164184
detectedAutomationFramework: string;
165185
},
166186
config: BrowserStackConfig,
167187
): Promise<CallToolResult> {
188+
// Validate that either paths or URLs are provided for both app and test suite
189+
if (!args.browserstackAppUrl && !args.appPath) {
190+
throw new Error(
191+
"appPath is required when browserstackAppUrl is not provided",
192+
);
193+
}
194+
if (!args.browserstackTestSuiteUrl && !args.testSuitePath) {
195+
throw new Error(
196+
"testSuitePath is required when browserstackTestSuiteUrl is not provided",
197+
);
198+
}
199+
168200
switch (args.detectedAutomationFramework) {
169201
case AppTestPlatform.ESPRESSO: {
170202
try {
171-
const app_url = await uploadEspressoApp(args.appPath, config);
172-
const test_suite_url = await uploadEspressoTestSuite(
173-
args.testSuitePath,
174-
config,
175-
);
203+
let app_url: string;
204+
if (args.browserstackAppUrl) {
205+
app_url = args.browserstackAppUrl;
206+
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
207+
} else {
208+
app_url = await uploadEspressoApp(args.appPath!, config);
209+
logger.info(`App uploaded. URL: ${app_url}`);
210+
}
211+
212+
let test_suite_url: string;
213+
if (args.browserstackTestSuiteUrl) {
214+
test_suite_url = args.browserstackTestSuiteUrl;
215+
logger.info(
216+
`Using provided BrowserStack test suite URL: ${test_suite_url}`,
217+
);
218+
} else {
219+
test_suite_url = await uploadEspressoTestSuite(
220+
args.testSuitePath!,
221+
config,
222+
);
223+
logger.info(`Test suite uploaded. URL: ${test_suite_url}`);
224+
}
225+
176226
const build_id = await triggerEspressoBuild(
177227
app_url,
178228
test_suite_url,
@@ -195,11 +245,29 @@ async function runAppTestsOnBrowserStack(
195245
}
196246
case AppTestPlatform.XCUITEST: {
197247
try {
198-
const app_url = await uploadXcuiApp(args.appPath, config);
199-
const test_suite_url = await uploadXcuiTestSuite(
200-
args.testSuitePath,
201-
config,
202-
);
248+
let app_url: string;
249+
if (args.browserstackAppUrl) {
250+
app_url = args.browserstackAppUrl;
251+
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
252+
} else {
253+
app_url = await uploadXcuiApp(args.appPath!, config);
254+
logger.info(`App uploaded. URL: ${app_url}`);
255+
}
256+
257+
let test_suite_url: string;
258+
if (args.browserstackTestSuiteUrl) {
259+
test_suite_url = args.browserstackTestSuiteUrl;
260+
logger.info(
261+
`Using provided BrowserStack test suite URL: ${test_suite_url}`,
262+
);
263+
} else {
264+
test_suite_url = await uploadXcuiTestSuite(
265+
args.testSuitePath!,
266+
config,
267+
);
268+
logger.info(`Test suite uploaded. URL: ${test_suite_url}`);
269+
}
270+
203271
const build_id = await triggerXcuiBuild(
204272
app_url,
205273
test_suite_url,

src/tools/applive-utils/start-session.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import { BrowserStackConfig } from "../../lib/types.js";
1414
import envConfig from "../../config.js";
1515

1616
interface StartSessionArgs {
17-
appPath: string;
17+
appPath?: string;
1818
desiredPlatform: "android" | "ios";
1919
desiredPhone: string;
2020
desiredPlatformVersion: string;
21+
browserstackAppUrl?: string;
2122
}
2223

2324
interface StartSessionOptions {
@@ -31,8 +32,13 @@ export async function startSession(
3132
args: StartSessionArgs,
3233
options: StartSessionOptions,
3334
): Promise<string> {
34-
const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion } =
35-
args;
35+
const {
36+
appPath,
37+
desiredPlatform,
38+
desiredPhone,
39+
desiredPlatformVersion,
40+
browserstackAppUrl,
41+
} = args;
3642
const { config } = options;
3743

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

74-
// 6) Upload app
75-
const authString = getBrowserStackAuth(config);
76-
const [username, password] = authString.split(":");
77-
const { app_url } = await uploadApp(appPath, username, password);
78-
logger.info(`App uploaded: ${app_url}`);
80+
// 6) Upload app or use provided URL
81+
let app_url: string;
82+
if (browserstackAppUrl) {
83+
app_url = browserstackAppUrl;
84+
logger.info(`Using provided BrowserStack app URL: ${app_url}`);
85+
} else {
86+
if (!appPath) {
87+
throw new Error(
88+
"appPath is required when browserstackAppUrl is not provided",
89+
);
90+
}
91+
const authString = getBrowserStackAuth(config);
92+
const [username, password] = authString.split(":");
93+
const result = await uploadApp(appPath, username, password);
94+
app_url = result.app_url;
95+
logger.info(`App uploaded: ${app_url}`);
96+
}
7997

8098
if (!app_url) {
8199
throw new Error("Failed to upload app. Please try again.");

src/tools/applive.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,44 @@ export async function startAppLiveSession(
1414
args: {
1515
desiredPlatform: string;
1616
desiredPlatformVersion: string;
17-
appPath: string;
17+
appPath?: string;
1818
desiredPhone: string;
19+
browserstackAppUrl?: string;
1920
},
2021
config: BrowserStackConfig,
2122
): Promise<CallToolResult> {
2223
if (!args.desiredPlatform) {
2324
throw new Error("You must provide a desiredPlatform.");
2425
}
2526

26-
if (!args.appPath) {
27-
throw new Error("You must provide a appPath.");
27+
if (!args.appPath && !args.browserstackAppUrl) {
28+
throw new Error("You must provide either appPath or browserstackAppUrl.");
2829
}
2930

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

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

38-
if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
39-
throw new Error("You must provide a valid iOS app path.");
40-
}
41+
if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
42+
throw new Error("You must provide a valid iOS app path.");
43+
}
4144

42-
// check if the app path exists && is readable
43-
try {
44-
if (!fs.existsSync(args.appPath)) {
45-
throw new Error("The app path does not exist.");
45+
// check if the app path exists && is readable
46+
try {
47+
if (!fs.existsSync(args.appPath)) {
48+
throw new Error("The app path does not exist.");
49+
}
50+
fs.accessSync(args.appPath, fs.constants.R_OK);
51+
} catch (error) {
52+
logger.error("The app path does not exist or is not readable: %s", error);
53+
throw new Error("The app path does not exist or is not readable.");
4654
}
47-
fs.accessSync(args.appPath, fs.constants.R_OK);
48-
} catch (error) {
49-
logger.error("The app path does not exist or is not readable: %s", error);
50-
throw new Error("The app path does not exist or is not readable.");
5155
}
5256

5357
const launchUrl = await startSession(
@@ -56,6 +60,7 @@ export async function startAppLiveSession(
5660
desiredPlatform: args.desiredPlatform as "android" | "ios",
5761
desiredPhone: args.desiredPhone,
5862
desiredPlatformVersion: args.desiredPlatformVersion,
63+
browserstackAppUrl: args.browserstackAppUrl,
5964
},
6065
{ config },
6166
);

tests/tools/appautomate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('appautomate utils', () => {
6666

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

7272
it('should fail if phone is not provided', () => {

tests/tools/applive.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('startAppLiveSession', () => {
8989

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

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

0 commit comments

Comments
 (0)