diff --git a/src/tools/appautomate-utils/appium-sdk/config-generator.ts b/src/tools/appautomate-utils/appium-sdk/config-generator.ts new file mode 100644 index 00000000..bec4f667 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -0,0 +1,73 @@ +// Configuration utilities for BrowserStack App SDK +import { + APP_DEVICE_CONFIGS, + AppSDKSupportedTestingFrameworkEnum, + DEFAULT_APP_PATH, + createStep, +} from "./index.js"; + +export function generateAppBrowserStackYMLInstructions( + platforms: string[], + username: string, + accessKey: string, + appPath: string = DEFAULT_APP_PATH, + testingFramework: string, +): string { + if ( + testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || + testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio || + testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby + ) { + return ""; + } + + // Generate platform and device configurations + const platformConfigs = platforms + .map((platform) => { + const devices = + APP_DEVICE_CONFIGS[platform as keyof typeof APP_DEVICE_CONFIGS]; + if (!devices) return ""; + + return devices + .map( + (device) => ` - platformName: ${platform} + deviceName: ${device.deviceName} + platformVersion: "${device.platformVersion}"`, + ) + .join("\n"); + }) + .filter(Boolean) + .join("\n"); + + // Construct YAML content + const configContent = `\`\`\`yaml +userName: ${username} +accessKey: ${accessKey} +app: ${appPath} +platforms: +${platformConfigs} +parallelsPerPlatform: 1 +browserstackLocal: true +buildName: bstack-demo +projectName: BrowserStack Sample +debug: true +networkLogs: true +percy: false +percyCaptureMode: auto +accessibility: false +\`\`\` + +**Important notes:** +- Replace \`app: ${appPath}\` with the path to your actual app file (e.g., \`./SampleApp.apk\` for Android or \`./SampleApp.ipa\` for iOS) +- You can upload your app using BrowserStack's App Upload API or manually through the dashboard +- Set \`browserstackLocal: true\` if you need to test with local/staging servers +- Adjust \`parallelsPerPlatform\` based on your subscription limits`; + + // Return formatted step for instructions + return createStep( + "Update browserstack.yml file with App Automate configuration:", + `Create or update the browserstack.yml file in your project root with the following content: + +${configContent}`, + ); +} diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts new file mode 100644 index 00000000..c75872dd --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; +import { + AppSDKSupportedFrameworkEnum, + AppSDKSupportedTestingFrameworkEnum, + AppSDKSupportedLanguageEnum, + AppSDKSupportedPlatformEnum, +} from "./index.js"; + +// App Automate specific device configurations +export const APP_DEVICE_CONFIGS = { + android: [ + { deviceName: "Samsung Galaxy S22 Ultra", platformVersion: "12.0" }, + { deviceName: "Google Pixel 7 Pro", platformVersion: "13.0" }, + { deviceName: "OnePlus 9", platformVersion: "11.0" }, + ], + ios: [ + { deviceName: "iPhone 14", platformVersion: "16" }, + { deviceName: "iPhone 13", platformVersion: "15" }, + { deviceName: "iPad Air 4", platformVersion: "14" }, + ], +}; + +// Step delimiter for parsing instructions +export const STEP_DELIMITER = "---STEP---"; + +// Default app path for examples +export const DEFAULT_APP_PATH = "bs://sample.app"; + +// Tool description and schema for setupBrowserStackAppAutomateTests +export const SETUP_APP_AUTOMATE_DESCRIPTION = + "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead."; + +export const SETUP_APP_AUTOMATE_SCHEMA = { + detectedFramework: z + .nativeEnum(AppSDKSupportedFrameworkEnum) + .describe( + "The mobile automation framework configured in the project. Example: 'appium'", + ), + + detectedTestingFramework: z + .nativeEnum(AppSDKSupportedTestingFrameworkEnum) + .describe( + "The testing framework used in the project. Be precise with framework selection Example: 'testng', 'behave', 'pytest', 'robot'", + ), + + detectedLanguage: z + .nativeEnum(AppSDKSupportedLanguageEnum) + .describe( + "The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'", + ), + + desiredPlatforms: z + .array(z.nativeEnum(AppSDKSupportedPlatformEnum)) + .describe( + "The mobile platforms the user wants to test on. Always ask this to the user, do not try to infer this. Example: ['android', 'ios']", + ), + + appPath: z + .string() + .describe( + "Path to the mobile app file (.apk for Android, .ipa for iOS). Can be a local file path or a BrowserStack app URL (bs://)", + ), + project: z + .string() + .optional() + .default("BStack-AppAutomate-Suite") + .describe("Project name for organizing test runs on BrowserStack."), +}; diff --git a/src/tools/appautomate-utils/appium-sdk/formatter.ts b/src/tools/appautomate-utils/appium-sdk/formatter.ts new file mode 100644 index 00000000..0a9d1a6c --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/formatter.ts @@ -0,0 +1,87 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { STEP_DELIMITER } from "./constants.js"; + +export function formatFinalAppInstructions( + formattedInstructions: string, +): CallToolResult { + const fullInstructions = ` +⚠️ IMPORTANT: DO NOT SKIP ANY STEP +All the setup steps described in this file MUST be executed regardless of any existing configuration or setup. +This ensures proper BrowserStack App Automate SDK setup. +Each step is compulsory and sequence needs to be maintained. + +${formattedInstructions}`; + + return { + content: [ + { + type: "text", + text: fullInstructions, + isError: false, + }, + ], + }; +} + +export function createStep(title: string, content: string): string { + return `${STEP_DELIMITER} +${title} + +${content}`; +} + +export function combineInstructions(...instructionParts: string[]): string { + return instructionParts.filter(Boolean).join("\n\n"); +} + +export function formatEnvCommands( + username: string, + accessKey: string, + isWindows: boolean, +): string { + if (isWindows) { + return `\`\`\`cmd +setx BROWSERSTACK_USERNAME "${username}" +setx BROWSERSTACK_ACCESS_KEY "${accessKey}" +\`\`\``; + } + return `\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\``; +} + +export function createEnvStep( + username: string, + accessKey: string, + isWindows: boolean, + platformLabel: string, + title: string = "Set BrowserStack credentials as environment variables:", +): string { + return createStep( + title, + `**${platformLabel}:** +${formatEnvCommands(username, accessKey, isWindows)}`, + ); +} + +export function formatMultiLineCommand( + command: string, + isWindows: boolean = process.platform === "win32", +): string { + if (isWindows) { + // For Windows, keep commands on single line + return command.replace(/\s*\\\s*\n\s*/g, " "); + } + return command; +} + +export function formatAppInstructionsWithNumbers(instructions: string): string { + const steps = instructions + .split(STEP_DELIMITER) + .filter((step) => step.trim()); + + return steps + .map((step, index) => `**Step ${index + 1}:**\n${step.trim()}`) + .join("\n\n"); +} diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts new file mode 100644 index 00000000..405b203d --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -0,0 +1,109 @@ +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 { + getAppUploadInstruction, + validateSupportforAppAutomate, + SupportedFramework, +} from "./utils.js"; + +import { + getAppSDKPrefixCommand, + generateAppBrowserStackYMLInstructions, +} from "./index.js"; + +import { + AppSDKSupportedLanguage, + AppSDKSupportedTestingFramework, + AppSDKInstruction, + formatAppInstructionsWithNumbers, + getAppInstructionsForProjectConfiguration, + SETUP_APP_AUTOMATE_SCHEMA, +} from "./index.js"; + +export async function setupAppAutomateHandler( + rawInput: unknown, + config: BrowserStackConfig, +): Promise { + const input = z.object(SETUP_APP_AUTOMATE_SCHEMA).parse(rawInput); + const auth = getBrowserStackAuth(config); + const [username, accessKey] = auth.split(":"); + + const instructions: AppSDKInstruction[] = []; + + // Use variables for all major input properties + const testingFramework = + input.detectedTestingFramework as AppSDKSupportedTestingFramework; + const language = input.detectedLanguage as AppSDKSupportedLanguage; + const platforms = (input.desiredPlatforms as string[]) ?? ["android"]; + const appPath = input.appPath as string; + const framework = input.detectedFramework as SupportedFramework; + + //Validating if supported framework or not + validateSupportforAppAutomate(framework, language, testingFramework); + + // Step 1: Generate SDK setup command + const sdkCommand = getAppSDKPrefixCommand( + language, + testingFramework, + username, + accessKey, + appPath, + ); + + if (sdkCommand) { + instructions.push({ content: sdkCommand, type: "setup" }); + } + + // Step 2: Generate browserstack.yml configuration + const configInstructions = generateAppBrowserStackYMLInstructions( + platforms, + username, + accessKey, + appPath, + testingFramework, + ); + + if (configInstructions) { + instructions.push({ content: configInstructions, type: "config" }); + } + + // Step 3: Generate app upload instruction + const appUploadInstruction = await getAppUploadInstruction( + appPath, + username, + accessKey, + testingFramework, + ); + + if (appUploadInstruction) { + instructions.push({ content: appUploadInstruction, type: "setup" }); + } + + // Step 4: Generate project configuration and run instructions + const projectInstructions = getAppInstructionsForProjectConfiguration( + framework, + testingFramework, + language, + ); + + if (projectInstructions) { + instructions.push({ content: projectInstructions, type: "run" }); + } + + const combinedInstructions = instructions + .map((instruction) => instruction.content) + .join("\n\n"); + + return { + content: [ + { + type: "text", + text: formatAppInstructionsWithNumbers(combinedInstructions), + isError: false, + }, + ], + isError: false, + }; +} diff --git a/src/tools/appautomate-utils/appium-sdk/index.ts b/src/tools/appautomate-utils/appium-sdk/index.ts new file mode 100644 index 00000000..f9c4a29b --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/index.ts @@ -0,0 +1,12 @@ +// Barrel exports for App BrowserStack module +export { + getAppSDKPrefixCommand, + getAppInstructionsForProjectConfiguration, +} from "./instructions.js"; +export { generateAppBrowserStackYMLInstructions } from "./config-generator.js"; + +export * from "./types.js"; +export * from "./constants.js"; +export * from "./utils.js"; +export * from "./instructions.js"; +export * from "./formatter.js"; diff --git a/src/tools/appautomate-utils/appium-sdk/instructions.ts b/src/tools/appautomate-utils/appium-sdk/instructions.ts new file mode 100644 index 00000000..b1d73d4c --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/instructions.ts @@ -0,0 +1,66 @@ +import { + AppSDKSupportedLanguage, + AppSDKSupportedTestingFramework, +} from "./index.js"; + +// Language-specific instruction imports +import { getJavaAppInstructions } from "./languages/java.js"; +import { getCSharpAppInstructions } from "./languages/csharp.js"; +import { getNodejsAppInstructions } from "./languages/nodejs.js"; +import { getPythonAppInstructions } from "./languages/python.js"; +import { getRubyAppInstructions } from "./languages/ruby.js"; + +// Language-specific command imports +import { getCSharpSDKCommand } from "./languages/csharp.js"; +import { getJavaSDKCommand } from "./languages/java.js"; +import { getNodejsSDKCommand } from "./languages/nodejs.js"; +import { getPythonSDKCommand } from "./languages/python.js"; +import { getRubySDKCommand } from "./languages/ruby.js"; + +export function getAppInstructionsForProjectConfiguration( + framework: string, + testingFramework: AppSDKSupportedTestingFramework, + language: AppSDKSupportedLanguage, +): string { + if (!framework || !testingFramework || !language) { + return ""; + } + + switch (language) { + case "java": + return getJavaAppInstructions(); + case "nodejs": + return getNodejsAppInstructions(testingFramework); + case "python": + return getPythonAppInstructions(testingFramework); + case "ruby": + return getRubyAppInstructions(); + case "csharp": + return getCSharpAppInstructions(); + default: + return ""; + } +} + +export function getAppSDKPrefixCommand( + language: AppSDKSupportedLanguage, + testingFramework: string, + username: string, + accessKey: string, + appPath?: string, +): string { + switch (language) { + case "csharp": + return getCSharpSDKCommand(username, accessKey); + case "java": + return getJavaSDKCommand(testingFramework, username, accessKey, appPath); + case "nodejs": + return getNodejsSDKCommand(testingFramework, username, accessKey); + case "python": + return getPythonSDKCommand(testingFramework, username, accessKey); + case "ruby": + return getRubySDKCommand(testingFramework, username, accessKey); + default: + return ""; + } +} diff --git a/src/tools/appautomate-utils/appium-sdk/languages/csharp.ts b/src/tools/appautomate-utils/appium-sdk/languages/csharp.ts new file mode 100644 index 00000000..11caa233 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/csharp.ts @@ -0,0 +1,112 @@ +// C# instructions and commands for App SDK utilities +import { + PLATFORM_UTILS, + createStep, + createEnvStep, + combineInstructions, +} from "../index.js"; + +export function getCSharpAppInstructions(): string { + const { isWindows, isAppleSilicon, getPlatformLabel } = PLATFORM_UTILS; + + let runCommand = ""; + if (isWindows) { + runCommand = `\`\`\`cmd +dotnet build +dotnet test --filter [other_args] +\`\`\``; + } else if (isAppleSilicon) { + runCommand = `\`\`\`bash +dotnet build +dotnet test --filter [other_args] +\`\`\` + +**Did not set the alias?** +Use the absolute path to the dotnet installation to run your tests on Mac computers with Apple silicon chips: +\`\`\`bash +/dotnet test +\`\`\``; + } else { + runCommand = `\`\`\`bash +dotnet build +dotnet test --filter [other_args] +\`\`\``; + } + + const runStep = createStep( + "Run your C# test suite:", + `**${getPlatformLabel()}:** +${runCommand} + +**Debug Guidelines:** +If you encounter the error: java.lang.IllegalArgumentException: Multiple entries with the same key, +__Resolution:__ +- The app capability should only be set in one place: browserstack.yml. +- Remove or comment out any code or configuration in your test setup (e.g., step definitions, runners, or capabilities setup) that sets the app path directly.`, + ); + + return runStep; +} + +export function getCSharpSDKCommand( + username: string, + accessKey: string, +): string { + const { + isWindows = false, + isAppleSilicon = false, + getPlatformLabel = () => "Unknown", + } = PLATFORM_UTILS || {}; + if (!PLATFORM_UTILS) { + console.warn("PLATFORM_UTILS is undefined. Defaulting platform values."); + } + + const envStep = createEnvStep( + username, + accessKey, + isWindows, + getPlatformLabel(), + ); + + const installCommands = isWindows + ? `\`\`\`cmd +dotnet add package BrowserStack.TestAdapter +dotnet build +dotnet browserstack-sdk setup --userName "${username}" --accessKey "${accessKey}" +\`\`\`` + : `\`\`\`bash +dotnet add package BrowserStack.TestAdapter +dotnet build +dotnet browserstack-sdk setup --userName "${username}" --accessKey "${accessKey}" +\`\`\``; + + const installStep = createStep( + "Install BrowserStack SDK", + `Run the following command to install the BrowserStack SDK and create a browserstack.yml file in the root directory of your project: + +**${getPlatformLabel()}:** +${installCommands}`, + ); + + const appleSiliconNote = isAppleSilicon + ? createStep( + "[Only for Macs with Apple silicon] Install dotnet x64 on MacOS", + `If you are using a Mac computer with Apple silicon chip (M1 or M2) architecture, use the given command: + +\`\`\`bash +cd #(project folder Android or iOS) +dotnet browserstack-sdk setup-dotnet --dotnet-path "" --dotnet-version "" +\`\`\` + +- \`\` - Mention the absolute path to the directory where you want to save dotnet x64 +- \`\` - Mention the dotnet version which you want to use to run tests + +This command performs the following functions: +- Installs dotnet x64 +- Installs the required version of dotnet x64 at an appropriate path +- Sets alias for the dotnet installation location on confirmation (enter y option)`, + ) + : ""; + + return combineInstructions(envStep, installStep, appleSiliconNote); +} diff --git a/src/tools/appautomate-utils/appium-sdk/languages/java.ts b/src/tools/appautomate-utils/appium-sdk/languages/java.ts new file mode 100644 index 00000000..ea5ad958 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/java.ts @@ -0,0 +1,145 @@ +// Java instructions and commands for App SDK utilities +import { + createStep, + combineInstructions, + createEnvStep, + PLATFORM_UTILS, +} from "../index.js"; + +// Java-specific constants and mappings +export const MAVEN_ARCHETYPE_GROUP_ID = "com.browserstack"; +export const MAVEN_ARCHETYPE_ARTIFACT_ID = "junit-archetype-integrate"; +export const MAVEN_ARCHETYPE_VERSION = "1.0"; + +// Framework mapping for Java Maven archetype generation for App Automate +export const JAVA_APP_FRAMEWORK_MAP: Record = { + testng: "browserstack-sdk-archetype-integrate", + junit5: "browserstack-sdk-archetype-integrate", + selenide: "selenide-archetype-integrate", + jbehave: "browserstack-sdk-archetype-integrate", + cucumberTestng: "browserstack-sdk-archetype-integrate", + cucumberJunit4: "browserstack-sdk-archetype-integrate", + cucumberJunit5: "browserstack-sdk-archetype-integrate", +}; + +// Common Gradle setup instructions for App Automate (platform-independent) +export const GRADLE_APP_SETUP_INSTRUCTIONS = ` +**For Gradle setup:** +1. Add browserstack-java-sdk to dependencies: + compileOnly 'com.browserstack:browserstack-java-sdk:latest.release' + +2. Add browserstackSDK path variable: + def browserstackSDKArtifact = configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find { it.name == 'browserstack-java-sdk' } + +3. Add javaagent to gradle tasks: + jvmArgs "-javaagent:\${browserstackSDKArtifact.file}" +`; + +export function getJavaAppInstructions(): string { + const baseRunStep = createStep( + "Run your App Automate test suite:", + `\`\`\`bash +mvn test +\`\`\``, + ); + return baseRunStep; +} + +export function getJavaAppFrameworkForMaven(framework: string): string { + return JAVA_APP_FRAMEWORK_MAP[framework] || framework; +} + +function getMavenCommandForWindows( + framework: string, + mavenFramework: string, + username: string, + accessKey: string, +): string { + return ( + `mvn archetype:generate -B ` + + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + + `-DarchetypeArtifactId="${mavenFramework}" ` + + `-DarchetypeVersion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + + `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + + `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DBROWSERSTACK_USERNAME="${username}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}" ` + + `-DBROWSERSTACK_FRAMEWORK="${framework}"` + ); +} + +function getMavenCommandForUnix( + framework: string, + mavenFramework: string, + username: string, + accessKey: string, +): string { + return ( + `mvn archetype:generate -B ` + + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + + `-DarchetypeArtifactId="${mavenFramework}" ` + + `-DarchetypeVersion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + + `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + + `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DBROWSERSTACK_USERNAME="${username}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}" ` + + `-DBROWSERSTACK_FRAMEWORK="${framework}"` + ); +} + +export function getJavaSDKCommand( + framework: string, + username: string, + accessKey: string, + appPath?: string, +): string { + const { isWindows = false, getPlatformLabel } = PLATFORM_UTILS || {}; + + const mavenFramework = getJavaAppFrameworkForMaven(framework); + + let mavenCommand: string; + + if (isWindows) { + mavenCommand = getMavenCommandForWindows( + framework, + mavenFramework, + username, + accessKey, + ); + if (appPath) { + mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; + } + } else { + mavenCommand = getMavenCommandForUnix( + framework, + mavenFramework, + username, + accessKey, + ); + if (appPath) { + mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; + } + } + + const envStep = createEnvStep( + username, + accessKey, + isWindows, + getPlatformLabel(), + ); + + const mavenStep = createStep( + "Install BrowserStack SDK using Maven Archetype for App Automate", + `Maven command for ${framework} (${getPlatformLabel()}): +\`\`\`bash +${mavenCommand} +\`\`\` + +Alternative setup for Gradle users: +${GRADLE_APP_SETUP_INSTRUCTIONS}`, + ); + + return combineInstructions(envStep, mavenStep); +} diff --git a/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts b/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts new file mode 100644 index 00000000..fe4cff3d --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts @@ -0,0 +1,289 @@ +// Node.js instructions and commands for App SDK utilities +import { + AppSDKSupportedTestingFramework, + AppSDKSupportedTestingFrameworkEnum, + createStep, + combineInstructions, +} from "../index.js"; + +export function getNodejsSDKCommand( + testingFramework: string, + username: string, + accessKey: string, +): string { + switch (testingFramework) { + case "webdriverio": + return getWebDriverIOCommand(username, accessKey); + case "nightwatch": + return getNightwatchCommand(username, accessKey); + case "jest": + return getJestCommand(username, accessKey); + case "mocha": + return getMochaCommand(username, accessKey); + case "cucumberJs": + return getCucumberJSCommand(username, accessKey); + default: + return ""; + } +} + +export function getNodejsAppInstructions( + testingFramework: AppSDKSupportedTestingFramework, +): string { + switch (testingFramework) { + case AppSDKSupportedTestingFrameworkEnum.webdriverio: + return createStep( + "Run your WebdriverIO test suite:", + "Your test suite is now ready to run on BrowserStack. Use the commands defined in your package.json file to run the tests", + ); + case AppSDKSupportedTestingFrameworkEnum.nightwatch: + return createStep( + "Run your App Automate test suite:", + `For Android: + \`\`\`bash + npx nightwatch --env browserstack.android + \`\`\` + For iOS: + \`\`\`bash + npx nightwatch --env browserstack.ios + \`\`\``, + ); + case AppSDKSupportedTestingFrameworkEnum.jest: + return createStep( + "Run your Jest test suite with BrowserStack SDK:", + `Use the npm script defined in your package.json. For example:\n\n\`\`\`bash\nnpx run browserstack-node-sdk jest specs/single_test.js\n\`\`\``, + ); + case AppSDKSupportedTestingFrameworkEnum.mocha: + return createStep( + "Run your Mocha test suite with BrowserStack SDK:", + `Use the npm script defined in your package.json. For example:\n\n\`\`\`bash\nnpx run browserstack-node-sdk mocha specs/single_test.js\n\`\`\``, + ); + + case AppSDKSupportedTestingFrameworkEnum.cucumberJs: + return createStep( + "Run your Cucumber JS test suite with BrowserStack SDK:", + `Use the npm script defined in your package.json. For example:\n\n\`\`\`bash\nnpx run browserstack-node-sdk cucumber-js specs/single_test.js\n\`\`\``, + ); + default: + return ""; + } +} + +function getWebDriverIOCommand(username: string, accessKey: string): string { + const prerequisiteStep = createStep( + "Prerequisite Setup:", + `a. Ensure you do not modify or replace any existing local driver code, + as it will be automatically managed and overwritten by the BrowserStack SDK/Driver. + b. Do not create any YML file in this integration as it is not required. + c. Ensure you create the WDIO config file as per the instructions below.`, + ); + + const envStep = createStep( + "Set your BrowserStack credentials as environment variables:", + `\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\``, + ); + + const installStep = createStep( + "Install BrowserStack WDIO service:", + `\`\`\`bash +npm install @wdio/browserstack-service@^7 --save-dev +\`\`\``, + ); + + const configStep = createStep( + "Update your WebdriverIO config file (e.g., \\`wdio.conf.js\\`) to add the BrowserStack service and capabilities:", + `\`\`\`js +exports.config = { + user: process.env.BROWSERSTACK_USERNAME || '${username}', + key: process.env.BROWSERSTACK_ACCESS_KEY || '${accessKey}', + hostname: 'hub.browserstack.com', + services: [ + [ + 'browserstack', + { + app: 'bs://sample.app', + browserstackLocal: true, + accessibility: false, + testObservabilityOptions: { + buildName: "bstack-demo", + projectName: "BrowserStack Sample", + buildTag: 'Any build tag goes here. For e.g. ["Tag1","Tag2"]' + }, + }, + ] + ], + capabilities: [{ + 'bstack:options': { + deviceName: 'Samsung Galaxy S22 Ultra', + platformVersion: '12.0', + platformName: 'android', + } + }], + commonCapabilities: { + 'bstack:options': { + debug: true, + networkLogs: true, + percy: false, + percyCaptureMode: 'auto' + } + }, + maxInstances: 10, + // ...other config +}; +\`\`\``, + ); + + return combineInstructions( + prerequisiteStep, + envStep, + installStep, + configStep, + ); +} + +function getNightwatchCommand(username: string, accessKey: string): string { + const prerequisiteStep = createStep( + "Prerequisite Setup:", + ` a. Ensure you do not modify or replace any existing local driver code, + as it will be automatically managed and overwritten by the BrowserStack SDK/Driver. + b. Do not create any YML file in this integration as it is not required. + c. Ensure you create the WDIO config file as per the instructions below.`, + ); + + const envStep = createStep( + "Set your BrowserStack credentials as environment variables:", + `\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\``, + ); + + const installStep = createStep( + "Install Nightwatch and BrowserStack integration:", + `\`\`\`bash +npm install --save-dev @nightwatch/browserstack +\`\`\``, + ); + + const configStep = createStep( + "Update your Nightwatch config file (e.g., \\`nightwatch.conf.js\\`) to add the BrowserStack settings and capabilities:", + `\`\`\`js + + test_settings:{ + ... + browserstack: { + selenium: { + host: 'hub.browserstack.com', + port: 443 + }, + desiredCapabilities: { + 'bstack:options': { + userName: '', + accessKey: '', + appiumVersion: '2.0.0' + } + }, + disable_error_log: false, + webdriver: { + timeout_options: { + timeout: 60000, + retry_attempts: 3 + }, + keep_alive: true, + start_process: false + } + }, + 'browserstack.android': { + extends: 'browserstack', + 'desiredCapabilities': { + browserName: null, + 'appium:options': { + automationName: 'UiAutomator2', + app: 'wikipedia-sample-app',// custom-id of the uploaded app + appPackage: 'org.wikipedia', + appActivity: 'org.wikipedia.main.MainActivity', + appWaitActivity: 'org.wikipedia.onboarding.InitialOnboardingActivity', + platformVersion: '11.0', + deviceName: 'Google Pixel 5' + }, + appUploadUrl: 'https://raw.githubusercontent.com/priyansh3133/wikipedia/main/wikipedia.apk',// URL of the app to be uploaded to BrowserStack before starting the test + // appUploadPath: '/path/to/app_name.apk' // if the app needs to be uploaded to BrowserStack from a local system + } + }, + 'browserstack.ios': { + extends: 'browserstack', + 'desiredCapabilities': { + browserName: null, + platformName: 'ios', + 'appium:options': { + automationName: 'XCUITest', + app: 'BStackSampleApp', + platformVersion: '16', + deviceName: 'iPhone 14' + }, + appUploadUrl: 'https://www.browserstack.com/app-automate/sample-apps/ios/BStackSampleApp.ipa', + // appUploadPath: '/path/to/app_name.ipa' + } + ... + } +\`\`\``, + ); + + return combineInstructions( + prerequisiteStep, + envStep, + installStep, + configStep, + ); +} + +function getJestCommand(username: string, accessKey: string): string { + const envStep = createStep( + "Set your BrowserStack credentials as environment variables:", + `\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\``, + ); + + const installStep = createStep( + "Install Jest and BrowserStack SDK:", + `\`\`\`bash +npm install --save-dev browserstack-node-sdk +\`\`\``, + ); + + return combineInstructions(envStep, installStep); +} + +function getMochaCommand(username: string, accessKey: string): string { + const envStep = createStep( + "Set your BrowserStack credentials as environment variables:", + `\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\``, + ); + + const installStep = createStep( + "Install Mocha and BrowserStack SDK:", + `\`\`\`bash +npm install --save-dev browserstack-node-sdk +\`\`\``, + ); + + return combineInstructions(envStep, installStep); +} + +function getCucumberJSCommand(username: string, accessKey: string): string { + return createStep( + "Set your BrowserStack credentials as environment variables:", + `\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\``, + ); +} diff --git a/src/tools/appautomate-utils/appium-sdk/languages/python.ts b/src/tools/appautomate-utils/appium-sdk/languages/python.ts new file mode 100644 index 00000000..a54a3ae2 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/python.ts @@ -0,0 +1,156 @@ +// Python instructions and commands for App SDK utilities +import { + AppSDKSupportedTestingFramework, + AppSDKSupportedTestingFrameworkEnum, + createStep, + createEnvStep, + combineInstructions, + PLATFORM_UTILS, +} from "../index.js"; + +export function getPythonAppInstructions( + testingFramework: AppSDKSupportedTestingFramework, +): string { + switch (testingFramework) { + case AppSDKSupportedTestingFrameworkEnum.robot: + return createStep( + "Run your App Automate test suite with Robot Framework:", + `\`\`\`bash +browserstack-sdk robot +\`\`\``, + ); + case AppSDKSupportedTestingFrameworkEnum.pytest: + return createStep( + "Run your App Automate test suite with Pytest:", + `\`\`\`bash +browserstack-sdk pytest -s +\`\`\``, + ); + case AppSDKSupportedTestingFrameworkEnum.behave: + return createStep( + "Run your App Automate test suite with Behave:", + `\`\`\`bash +browserstack-sdk behave +\`\`\``, + ); + case AppSDKSupportedTestingFrameworkEnum.lettuce: + return createStep( + "Run your test with Lettuce:", + `\`\`\`bash +# Run using paver +paver run first_test +\`\`\``, + ); + default: + return ""; + } +} + +export function getPythonSDKCommand( + framework: string, + username: string, + accessKey: string, +): string { + const { isWindows, getPlatformLabel } = PLATFORM_UTILS; + + switch (framework) { + case "robot": + case "pytest": + case "behave": + return getPythonCommonSDKCommand( + username, + accessKey, + isWindows, + getPlatformLabel(), + ); + case "lettuce": + return getLettuceCommand( + username, + accessKey, + isWindows, + getPlatformLabel(), + ); + default: + return ""; + } +} + +function getPythonCommonSDKCommand( + username: string, + accessKey: string, + isWindows: boolean, + platformLabel: string, +): string { + const envStep = createEnvStep( + username, + accessKey, + isWindows, + platformLabel, + "Set your BrowserStack credentials as environment variables:", + ); + + const installStep = createStep( + "Install BrowserStack Python SDK:", + `\`\`\`bash +python3 -m pip install browserstack-sdk +\`\`\``, + ); + + const setupStep = createStep( + "Set up BrowserStack SDK:", + `\`\`\`bash +browserstack-sdk setup --username "${username}" --key "${accessKey}" +\`\`\``, + ); + + return combineInstructions(envStep, installStep, setupStep); +} + +function getLettuceCommand( + username: string, + accessKey: string, + isWindows: boolean, + platformLabel: string, +): string { + const envStep = createEnvStep( + username, + accessKey, + isWindows, + platformLabel, + "Set your BrowserStack credentials as environment variables:", + ); + + const configStep = createStep( + "Configure Appium's desired capabilities in config.json:", + `**Android example:** +\`\`\`json +{ + "capabilities": { + "browserstack.user" : "${username}", + "browserstack.key" : "${accessKey}", + "project": "First Lettuce Android Project", + "build": "Lettuce Android", + "name": "first_test", + "browserstack.debug": true, + "app": "bs://", + "device": "Google Pixel 3", + "os_version": "9.0" + } +} +\`\`\``, + ); + + const initStep = createStep( + "Initialize remote WebDriver in terrain.py:", + `\`\`\`python +# Initialize the remote Webdriver using BrowserStack remote URL +# and desired capabilities defined above +context.browser = webdriver.Remote( + desired_capabilities=desired_capabilities, + command_executor="https://hub-cloud.browserstack.com/wd/hub" +) +\`\`\``, + ); + + return combineInstructions(envStep, configStep, initStep); +} diff --git a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts new file mode 100644 index 00000000..10ff6a6f --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts @@ -0,0 +1,125 @@ +// Ruby instructions and commands for App SDK utilities +import { + createStep, + combineInstructions, + createEnvStep, + PLATFORM_UTILS, +} from "../index.js"; + +const username = "${process.env.BROWSERSTACK_USERNAME}"; +const accessKey = "${process.env.BROWSERSTACK_ACCESS_KEY}"; + +export function getRubyAppInstructions(): string { + const configStep = createStep( + "Create/Update the config file (config.yml) as follows:", + `\`\`\`yaml +server: "hub-cloud.browserstack.com" + +common_caps: + "browserstack.user": "${username}" + "browserstack.key": "${accessKey}" + "project": "First Cucumber Android Project" + "build": "Cucumber Android" + "browserstack.debug": true + +browser_caps: + - + "deviceName": "Google Pixel 3" + "os_version": "9.0" + "app": "" + "name": "first_test" +\`\`\``, + ); + + const envStep = createStep( + "Create/Update your support/env.rb file:", + `\`\`\`ruby +require 'rubygems' +require 'appium_lib' + +# Load configuration from config.yml +caps = Appium.load_appium_txt file: File.expand_path('./../config.yml', __FILE__) +username = "${username}" +password = "${accessKey}" + +# Create desired capabilities +desired_caps = { + caps: caps, + appium_lib: { + server_url: "https://#{username}:#{password}@#{caps['server']}/wd/hub" + } +} + +# Initialize Appium driver +begin + $appium_driver = Appium::Driver.new(desired_caps, true) + $driver = $appium_driver.start_driver +rescue Exception => e + puts e.message + Process.exit(0) +end + +# Add cleanup hook +at_exit do + $driver.quit if $driver +end +\`\`\``, + ); + + const runStep = createStep( + "Run the test:", + `\`\`\`bash +bundle exec cucumber +\`\`\``, + ); + + return combineInstructions(configStep, envStep, runStep); +} + +export function getRubySDKCommand( + framework: string, + username: string, + accessKey: string, +): string { + const { isWindows, getPlatformLabel } = PLATFORM_UTILS; + + const envStep = createEnvStep( + username, + accessKey, + isWindows, + getPlatformLabel(), + "Set your BrowserStack credentials as environment variables:", + ); + + const installStep = createStep( + "Install required Ruby gems:", + `\`\`\`bash +# Install Bundler if not already installed +gem install bundler + +# Install Appium Ruby client library +gem install appium_lib + +# Install Cucumber +gem install cucumber +\`\`\``, + ); + + const gemfileStep = createStep( + "Create a Gemfile for dependency management:", + `\`\`\`ruby +# Gemfile +source 'https://rubygems.org' + +gem 'appium_lib' +gem 'cucumber' +\`\`\` + +Then run: +\`\`\`bash +bundle install +\`\`\``, + ); + + return combineInstructions(envStep, installStep, gemfileStep); +} diff --git a/src/tools/appautomate-utils/appium-sdk/types.ts b/src/tools/appautomate-utils/appium-sdk/types.ts new file mode 100644 index 00000000..e17f0da3 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/types.ts @@ -0,0 +1,75 @@ +// Shared types for App SDK utilities +export enum AppSDKSupportedLanguageEnum { + java = "java", + nodejs = "nodejs", + python = "python", + ruby = "ruby", + csharp = "csharp", +} +export type AppSDKSupportedLanguage = keyof typeof AppSDKSupportedLanguageEnum; + +export enum AppSDKSupportedFrameworkEnum { + appium = "appium", +} + +export type AppSDKSupportedFramework = + keyof typeof AppSDKSupportedFrameworkEnum; + +export enum AppSDKSupportedTestingFrameworkEnum { + testng = "testng", + junit5 = "junit5", + junit4 = "junit4", + selenide = "selenide", + jbehave = "jbehave", + cucumberTestng = "cucumberTestng", + cucumberJunit4 = "cucumberJunit4", + cucumberJunit5 = "cucumberJunit5", + webdriverio = "webdriverio", + nightwatch = "nightwatch", + jest = "jest", + mocha = "mocha", + cucumberJs = "cucumberJs", + robot = "robot", + pytest = "pytest", + behave = "behave", + lettuce = "lettuce", + rspec = "rspec", + cucumberRuby = "cucumberRuby", + nunit = "nunit", + mstest = "mstest", + xunit = "xunit", + specflow = "specflow", + reqnroll = "reqnroll", +} + +export type AppSDKSupportedTestingFramework = + keyof typeof AppSDKSupportedTestingFrameworkEnum; + +export enum AppSDKSupportedPlatformEnum { + android = "android", + ios = "ios", +} +export type AppSDKSupportedPlatform = keyof typeof AppSDKSupportedPlatformEnum; + +// App SDK instruction type +export interface AppSDKInstruction { + content: string; + type: "config" | "run" | "setup"; +} + +export const SUPPORTED_CONFIGURATIONS = { + appium: { + ruby: ["cucumberRuby"], + java: [ + "junit5", + "junit4", + "testng", + "cucumberTestng", + "selenide", + "jbehave", + ], + csharp: ["nunit", "xunit", "mstest", "specflow", "reqnroll"], + python: ["pytest", "robot", "behave", "lettuce"], + nodejs: ["jest", "mocha", "cucumberJs", "webdriverio", "nightwatch"], + }, +}; diff --git a/src/tools/appautomate-utils/appium-sdk/utils.ts b/src/tools/appautomate-utils/appium-sdk/utils.ts new file mode 100644 index 00000000..9c042ba5 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -0,0 +1,105 @@ +import { uploadApp } from "../native-execution/appautomate.js"; +import { + AppSDKSupportedTestingFramework, + AppSDKSupportedTestingFrameworkEnum, + createStep, +} from "./index.js"; +import { SUPPORTED_CONFIGURATIONS } from "./types.js"; + +export function isBrowserStackAppUrl(appPath: string): boolean { + return appPath.startsWith("bs://"); +} + +export function generateBuildName(baseName: string = "app-automate"): string { + const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, ""); + return `${baseName}-${timestamp}`; +} + +export function createError( + message: string, + context?: Record, +): Error { + const error = new Error(message); + if (context) { + (error as any).context = context; + } + return error; +} + +// Platform utilities for cross-platform support +export const PLATFORM_UTILS = { + isWindows: process.platform === "win32", + isMac: process.platform === "darwin", + isAppleSilicon: process.platform === "darwin" && process.arch === "arm64", + getPlatformLabel: () => { + switch (process.platform) { + case "win32": + return "Windows"; + case "darwin": + return "macOS"; + default: + return "macOS"; + } + }, +}; + +export async function getAppUploadInstruction( + appPath: string, + username: string, + accessKey: string, + detectedTestingFramework: AppSDKSupportedTestingFramework, +): Promise { + if ( + detectedTestingFramework === + AppSDKSupportedTestingFrameworkEnum.nightwatch || + detectedTestingFramework === + AppSDKSupportedTestingFrameworkEnum.webdriverio || + detectedTestingFramework === + AppSDKSupportedTestingFrameworkEnum.cucumberRuby + ) { + const app_url = await uploadApp(appPath, username, accessKey); + if (app_url) { + return createStep( + "Updating app_path with app_url", + `Replace the value of app_path in your configuration with: ${app_url}`, + ); + } + } + return ""; +} + +export type SupportedFramework = keyof typeof SUPPORTED_CONFIGURATIONS; +type SupportedLanguage = + keyof (typeof SUPPORTED_CONFIGURATIONS)[SupportedFramework]; +type SupportedTestingFramework = string; + +export function validateSupportforAppAutomate( + framework: SupportedFramework, + language: SupportedLanguage, + testingFramework: SupportedTestingFramework, +) { + const frameworks = Object.keys( + SUPPORTED_CONFIGURATIONS, + ) as SupportedFramework[]; + if (!SUPPORTED_CONFIGURATIONS[framework]) { + throw new Error( + `Unsupported framework '${framework}'. Supported frameworks: ${frameworks.join(", ")}`, + ); + } + + const languages = Object.keys( + SUPPORTED_CONFIGURATIONS[framework], + ) as SupportedLanguage[]; + if (!SUPPORTED_CONFIGURATIONS[framework][language]) { + throw new Error( + `Unsupported language '${language}' for framework '${framework}'. Supported languages: ${languages.join(", ")}`, + ); + } + + const testingFrameworks = SUPPORTED_CONFIGURATIONS[framework][language]; + if (!testingFrameworks.includes(testingFramework)) { + throw new Error( + `Unsupported testing framework '${testingFramework}' for language '${language}' and framework '${framework}'. Supported testing frameworks: ${testingFrameworks.join(", ")}`, + ); + } +} diff --git a/src/tools/appautomate-utils/appautomate.ts b/src/tools/appautomate-utils/native-execution/appautomate.ts similarity index 97% rename from src/tools/appautomate-utils/appautomate.ts rename to src/tools/appautomate-utils/native-execution/appautomate.ts index 9e16dc1f..00a2f8b6 100644 --- a/src/tools/appautomate-utils/appautomate.ts +++ b/src/tools/appautomate-utils/native-execution/appautomate.ts @@ -1,8 +1,8 @@ import fs from "fs"; import FormData from "form-data"; -import { apiClient } from "../../lib/apiClient.js"; -import { customFuzzySearch } from "../../lib/fuzzy.js"; -import { BrowserStackConfig } from "../../lib/types.js"; +import { apiClient } from "../../../lib/apiClient.js"; +import { customFuzzySearch } from "../../../lib/fuzzy.js"; +import { BrowserStackConfig } from "../../../lib/types.js"; interface Device { device: string; diff --git a/src/tools/appautomate-utils/native-execution/constants.ts b/src/tools/appautomate-utils/native-execution/constants.ts new file mode 100644 index 00000000..722db385 --- /dev/null +++ b/src/tools/appautomate-utils/native-execution/constants.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; +import { AppTestPlatform } from "./types.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.`; + +export const RUN_APP_AUTOMATE_SCHEMA = { + appPath: z + .string() + .describe( + "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 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()) + .describe( + "List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'iPhone 12 Pro-16.0'].", + ), + project: z + .string() + .optional() + .default("BStack-AppAutomate-Suite") + .describe("Project name for organizing test runs on BrowserStack."), + detectedAutomationFramework: z + .nativeEnum(AppTestPlatform) + .describe( + "The automation framework used in the project, such as 'espresso' (Android) or 'xcuitest' (iOS).", + ), +}; diff --git a/src/tools/appautomate-utils/native-execution/types.ts b/src/tools/appautomate-utils/native-execution/types.ts new file mode 100644 index 00000000..6f0901d3 --- /dev/null +++ b/src/tools/appautomate-utils/native-execution/types.ts @@ -0,0 +1,22 @@ +export enum AppTestPlatform { + ESPRESSO = "espresso", + XCUITEST = "xcuitest", +} + +export interface Device { + device: string; + display_name: string; + os_version: string; + real_mobile: boolean; +} + +export interface PlatformDevices { + os: string; + os_display_name: string; + devices: Device[]; +} + +export enum Platform { + ANDROID = "android", + IOS = "ios", +} diff --git a/src/tools/appautomate-utils/types.ts b/src/tools/appautomate-utils/types.ts deleted file mode 100644 index 16af7262..00000000 --- a/src/tools/appautomate-utils/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum AppTestPlatform { - ESPRESSO = "espresso", - APPIUM = "appium", - XCUITEST = "xcuitest", -} diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index c7b1af1f..d13f8a46 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -7,7 +7,18 @@ import { BrowserStackConfig } from "../lib/types.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 { AppTestPlatform } from "./appautomate-utils/native-execution/types.js"; +import { setupAppAutomateHandler } from "./appautomate-utils/appium-sdk/handler.js"; + +import { + SETUP_APP_AUTOMATE_DESCRIPTION, + SETUP_APP_AUTOMATE_SCHEMA, +} from "./appautomate-utils/appium-sdk/constants.js"; + +import { + PlatformDevices, + Platform, +} from "./appautomate-utils/native-execution/types.js"; import { getDevicesAndBrowsers, @@ -26,26 +37,11 @@ import { uploadXcuiApp, uploadXcuiTestSuite, triggerXcuiBuild, -} from "./appautomate-utils/appautomate.js"; - -// Types -interface Device { - device: string; - display_name: string; - os_version: string; - real_mobile: boolean; -} - -interface PlatformDevices { - os: string; - os_display_name: string; - devices: Device[]; -} - -enum Platform { - ANDROID = "android", - IOS = "ios", -} +} from "./appautomate-utils/native-execution/appautomate.js"; +import { + RUN_APP_AUTOMATE_DESCRIPTION, + RUN_APP_AUTOMATE_SCHEMA, +} from "./appautomate-utils/native-execution/constants.js"; /** * Launches an app on a selected BrowserStack device and takes a screenshot. @@ -356,48 +352,8 @@ export default function addAppAutomationTools( tools.runAppTestsOnBrowserStack = server.tool( "runAppTestsOnBrowserStack", - "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 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 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()) - .describe( - "List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'iPhone 12 Pro-16.0'].", - ), - project: z - .string() - .optional() - .default("BStack-AppAutomate-Suite") - .describe("Project name for organizing test runs on BrowserStack."), - detectedAutomationFramework: z - .string() - .describe( - "The automation framework used in the project, such as 'espresso' (Android) or 'xcuitest' (iOS).", - ), - }, + RUN_APP_AUTOMATE_DESCRIPTION, + RUN_APP_AUTOMATE_SCHEMA, async (args) => { try { trackMCP( @@ -429,5 +385,29 @@ export default function addAppAutomationTools( }, ); + tools.setupBrowserStackAppAutomateTests = server.tool( + "setupBrowserStackAppAutomateTests", + SETUP_APP_AUTOMATE_DESCRIPTION, + SETUP_APP_AUTOMATE_SCHEMA, + async (args) => { + try { + return await setupAppAutomateHandler(args, config); + } catch (error) { + const error_message = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Failed to bootstrap project with BrowserStack App Automate SDK. Error: ${error_message}. Please open an issue on GitHub if the problem persists`, + isError: true, + }, + ], + isError: true, + }; + } + }, + ); + return tools; } diff --git a/tests/tools/appautomate.test.ts b/tests/tools/appautomate.test.ts index 8598ecfa..cd7de09e 100644 --- a/tests/tools/appautomate.test.ts +++ b/tests/tools/appautomate.test.ts @@ -3,7 +3,7 @@ import { getDeviceVersions, resolveVersion, validateArgs, -} from '../../src/tools/appautomate-utils/appautomate'; +} from '../../src/tools/appautomate-utils/native-execution/appautomate'; import { beforeEach, it, expect, describe, vi } from 'vitest'