diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index 5b9a249..c132800 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -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 = { @@ -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 diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index 0a24358..80492c6 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -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, @@ -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>) ?? []; 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> = + input.devices.length === 0 + ? DEFAULT_MOBILE_DEVICE + : convertMobileDevicesToTuples(input.devices); // Validate devices against real BrowserStack device data const validatedEnvironments = await validateAppAutomateDevices(devices); diff --git a/src/tools/appautomate-utils/native-execution/constants.ts b/src/tools/appautomate-utils/native-execution/constants.ts index 9239a43..595cd98 100644 --- a/src/tools/appautomate-utils/native-execution/constants.ts +++ b/src/tools/appautomate-utils/native-execution/constants.ts @@ -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.`; @@ -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() diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 29a7d57..502acd0 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -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, @@ -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> = + args.devices.length === 0 + ? DEFAULT_MOBILE_DEVICE + : convertMobileDevicesToTuples(args.devices); + return await runAppTestsOnBrowserStack({ ...args, devices }, config); } catch (error) { trackMCP( "runAppTestsOnBrowserStack", diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 54171d4..312acd4 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -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> - | undefined; + // Convert device objects to tuples for validator + const devices = input.devices || []; + const tupleTargets: Array> = 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, ); diff --git a/src/tools/sdk-utils/common/device-validator.ts b/src/tools/sdk-utils/common/device-validator.ts index d2047b2..1bdae28 100644 --- a/src/tools/sdk-utils/common/device-validator.ts +++ b/src/tools/sdk-utils/common/device-validator.ts @@ -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> = [ + ["android", "Samsung Galaxy S24", "latest"], +]; + // Performance optimization: Indexed maps for faster lookups interface DesktopIndex { byOS: Map; @@ -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> { + 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, diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 39c5cb4..351ed27 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -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() @@ -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' }].", ), };