Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 3 additions & 26 deletions src/tools/appautomate-utils/appium-sdk/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
AppSDKSupportedFrameworkEnum,
AppSDKSupportedTestingFrameworkEnum,
AppSDKSupportedLanguageEnum,
AppSDKSupportedPlatformEnum,
} from "./index.js";
import { MobileDeviceSchema } from "../../sdk-utils/common/schema.js";

// App Automate specific device configurations
export const APP_DEVICE_CONFIGS = {
Expand Down Expand Up @@ -50,34 +50,11 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
),

devices: z
.array(
z.union([
// Android: [android, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.android)
.describe("Platform identifier: 'android'"),
z
.string()
.describe(
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
),
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
]),
// iOS: [ios, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.ios)
.describe("Platform identifier: 'ios'"),
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
]),
]),
)
.array(MobileDeviceSchema)
.max(3)
.default([])
.describe(
"Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]",
"Mobile device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14' }] or [{ platform: 'ios', deviceName: 'iPhone 15', osVersion: '17' }].",
),

appPath: z
Expand Down
17 changes: 10 additions & 7 deletions src/tools/appautomate-utils/appium-sdk/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { BrowserStackConfig } from "../../../lib/types.js";
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
import { validateAppAutomateDevices } from "../../sdk-utils/common/device-validator.js";
import {
validateAppAutomateDevices,
convertMobileDevicesToTuples,
DEFAULT_MOBILE_DEVICE,
} from "../../sdk-utils/common/device-validator.js";

import {
getAppUploadInstruction,
Expand Down Expand Up @@ -38,18 +42,17 @@ export async function setupAppAutomateHandler(
const testingFramework =
input.detectedTestingFramework as AppSDKSupportedTestingFramework;
const language = input.detectedLanguage as AppSDKSupportedLanguage;
const inputDevices = (input.devices as Array<Array<string>>) ?? [];
const appPath = input.appPath as string;
const framework = input.detectedFramework as SupportedFramework;

//Validating if supported framework or not
validateSupportforAppAutomate(framework, language, testingFramework);

// Use default mobile devices when array is empty
const devices =
inputDevices.length === 0
? [["android", "Samsung Galaxy S24", "latest"]]
: inputDevices;
// Convert device objects to tuples for validator
const devices: Array<Array<string>> =
input.devices.length === 0
? DEFAULT_MOBILE_DEVICE
: convertMobileDevicesToTuples(input.devices);

// Validate devices against real BrowserStack device data
const validatedEnvironments = await validateAppAutomateDevices(devices);
Expand Down
29 changes: 3 additions & 26 deletions src/tools/appautomate-utils/native-execution/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import { AppTestPlatform } from "./types.js";
import { AppSDKSupportedPlatformEnum } from "../appium-sdk/types.js";
import { MobileDeviceSchema } from "../../sdk-utils/common/schema.js";

export const RUN_APP_AUTOMATE_DESCRIPTION = `Execute pre-built native mobile test suites (Espresso for Android, XCUITest for iOS) by direct upload to BrowserStack. ONLY for compiled .apk/.ipa test files. This is NOT for SDK integration or Appium tests. For Appium-based testing with SDK setup, use 'setupBrowserStackAppAutomateTests' instead.`;

Expand Down Expand Up @@ -30,34 +30,11 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
"If in other directory, provide existing test file path",
),
devices: z
.array(
z.union([
// Android: [android, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.android)
.describe("Platform identifier: 'android'"),
z
.string()
.describe(
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
),
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
]),
// iOS: [ios, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.ios)
.describe("Platform identifier: 'ios'"),
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
]),
]),
)
.array(MobileDeviceSchema)
.max(3)
.default([])
.describe(
"Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]",
"Mobile device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14' }] or [{ platform: 'ios', deviceName: 'iPhone 15', osVersion: '17' }].",
),
project: z
.string()
Expand Down
13 changes: 11 additions & 2 deletions src/tools/appautomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { maybeCompressBase64 } from "../lib/utils.js";
import { remote } from "webdriverio";
import { AppTestPlatform } from "./appautomate-utils/native-execution/types.js";
import { setupAppAutomateHandler } from "./appautomate-utils/appium-sdk/handler.js";
import { validateAppAutomateDevices } from "./sdk-utils/common/device-validator.js";
import {
validateAppAutomateDevices,
convertMobileDevicesToTuples,
DEFAULT_MOBILE_DEVICE,
} from "./sdk-utils/common/device-validator.js";

import {
SETUP_APP_AUTOMATE_DESCRIPTION,
Expand Down Expand Up @@ -378,7 +382,12 @@ export default function addAppAutomationTools(
undefined,
config,
);
return await runAppTestsOnBrowserStack(args, config);
// Convert device objects to tuples for the handler
const devices: Array<Array<string>> =
args.devices.length === 0
? DEFAULT_MOBILE_DEVICE
: convertMobileDevicesToTuples(args.devices);
return await runAppTestsOnBrowserStack({ ...args, devices }, config);
} catch (error) {
trackMCP(
"runAppTestsOnBrowserStack",
Expand Down
42 changes: 37 additions & 5 deletions src/tools/sdk-utils/bstack/sdkHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,45 @@ export async function runBstackSDKOnly(
const authString = getBrowserStackAuth(config);
const [username, accessKey] = authString.split(":");

// Validate devices against real BrowserStack device data
const tupleTargets = (input as any).devices as
| Array<Array<string>>
| undefined;
// Convert device objects to tuples for validator
const devices = input.devices || [];
const tupleTargets: Array<Array<string>> = devices.map((device) => {
const platform = device.platform;
if (platform === "windows") {
return [
"windows",
device.osVersion,
device.browser,
device.browserVersion || "latest",
];
} else if (platform === "macos") {
return [
"macos",
device.osVersion,
device.browser,
device.browserVersion || "latest",
];
} else if (platform === "android") {
return [
"android",
device.deviceName,
device.osVersion,
device.browser || "chrome",
];
} else if (platform === "ios") {
return [
"ios",
device.deviceName,
device.osVersion,
device.browser || "safari",
];
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
});

const validatedEnvironments = await validateDevices(
tupleTargets || [],
tupleTargets,
input.detectedBrowserAutomationFramework,
);

Expand Down
22 changes: 22 additions & 0 deletions src/tools/sdk-utils/common/device-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ const DEFAULTS = {
ios: { device: "iPhone 15", browser: "safari" },
} as const;

// Default mobile device tuple for App Automate when no devices are provided
export const DEFAULT_MOBILE_DEVICE: Array<Array<string>> = [
["android", "Samsung Galaxy S24", "latest"],
];

// Performance optimization: Indexed maps for faster lookups
interface DesktopIndex {
byOS: Map<string, DesktopBrowserEntry[]>;
Expand Down Expand Up @@ -621,6 +626,23 @@ export async function validateAppAutomateDevices(
// SHARED UTILITY FUNCTIONS
// ============================================================================

/**
* Convert mobile device objects to tuples for validators
* @param devices Array of device objects with platform, deviceName, osVersion
* @returns Array of tuples [platform, deviceName, osVersion]
*/
export function convertMobileDevicesToTuples(
devices: Array<{ platform: string; deviceName: string; osVersion: string }>,
): Array<Array<string>> {
return devices.map((device) => {
if (device.platform === "android" || device.platform === "ios") {
return [device.platform, device.deviceName, device.osVersion];
} else {
throw new Error(`Unsupported platform: ${device.platform}`);
}
});
}

// Exact browser validation (preferred for structured fields)
function validateBrowserExact(
requestedBrowser: string,
Expand Down
113 changes: 68 additions & 45 deletions src/tools/sdk-utils/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,72 @@ export const SetUpPercyParamsShape = {
),
};

// Shared mobile device schema for App Automate (no browser field)
export const MobileDeviceSchema = z.discriminatedUnion("platform", [
z.object({
platform: z.literal("android"),
deviceName: z
.string()
.describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
osVersion: z
.string()
.describe("Android version, e.g. '14', '16', 'latest'"),
}),
z.object({
platform: z.literal("ios"),
deviceName: z
.string()
.describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
osVersion: z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
}),
]);

const DeviceSchema = z.discriminatedUnion("platform", [
z.object({
platform: z.literal("windows"),
osVersion: z.string().describe("Windows version, e.g. '10', '11'"),
browser: z
.string()
.describe("Browser name, e.g. 'chrome', 'firefox', 'edge'"),
browserVersion: z
.string()
.optional()
.describe("Browser version, e.g. '132', 'latest', 'oldest'"),
}),
z.object({
platform: z.literal("android"),
deviceName: z
.string()
.describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
osVersion: z
.string()
.describe("Android version, e.g. '14', '16', 'latest'"),
browser: z
.string()
.optional()
.describe("Browser name, e.g. 'chrome', 'samsung browser'"),
}),
z.object({
platform: z.literal("ios"),
deviceName: z
.string()
.describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
osVersion: z.string().describe("iOS version, e.g. '17', 'latest'"),
browser: z.string().optional().describe("Browser name, typically 'safari'"),
}),
z.object({
platform: z.literal("macos"),
osVersion: z
.string()
.describe("macOS version name, e.g. 'Sequoia', 'Ventura'"),
browser: z.string().describe("Browser name, e.g. 'safari', 'chrome'"),
browserVersion: z
.string()
.optional()
.describe("Browser version, e.g. 'latest'"),
}),
]);

export const RunTestsOnBrowserStackParamsShape = {
projectName: z
.string()
Expand All @@ -58,54 +124,11 @@ export const RunTestsOnBrowserStackParamsShape = {
),
detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum),
devices: z
.array(
z.union([
// Windows: [windows, osVersion, browser, browserVersion]
z.tuple([
z
.nativeEnum(WindowsPlatformEnum)
.describe("Platform identifier: 'windows'"),
z.string().describe("Windows version, e.g. '10', '11'"),
z.string().describe("Browser name, e.g. 'chrome', 'firefox', 'edge'"),
z
.string()
.describe("Browser version, e.g. '132', 'latest', 'oldest'"),
]),
// Android: [android, name, model, osVersion, browser]
z.tuple([
z
.literal(PlatformEnum.ANDROID)
.describe("Platform identifier: 'android'"),
z
.string()
.describe(
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
),
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'"),
]),
// iOS: [ios, name, model, osVersion, browser]
z.tuple([
z.literal(PlatformEnum.IOS).describe("Platform identifier: 'ios'"),
z.string().describe("Device name, e.g. 'iPhone 12 Pro'"),
z.string().describe("iOS version, e.g. '17', 'latest'"),
z.string().describe("Browser name, typically 'safari'"),
]),
// macOS: [mac|macos, name, model, browser, browserVersion]
z.tuple([
z
.nativeEnum(MacOSPlatformEnum)
.describe("Platform identifier: 'mac' or 'macos'"),
z.string().describe("macOS version name, e.g. 'Sequoia', 'Ventura'"),
z.string().describe("Browser name, e.g. 'safari', 'chrome'"),
z.string().describe("Browser version, e.g. 'latest'"),
]),
]),
)
.array(DeviceSchema)
.max(3)
.default([])
.describe(
"Preferred tuples of target devices.Add device only when user asks explicitly for it. Defaults to [] . Example: [['windows', '11', 'chrome', 'latest']]",
"Device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'windows', osVersion: '11', browser: 'chrome', browserVersion: 'latest' }] or [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14', browser: 'chrome' }].",
),
};

Expand Down