Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/lib/device-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export enum BrowserStackProducts {
LIVE = "live",
APP_LIVE = "app_live",
APP_AUTOMATE = "app_automate",
SELENIUM_AUTOMATE = "selenium_automate",
PLAYWRIGHT_AUTOMATE = "playwright_automate",
}

const URLS: Record<BrowserStackProducts, string> = {
Expand All @@ -22,6 +24,10 @@ const URLS: Record<BrowserStackProducts, string> = {
"https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
[BrowserStackProducts.APP_AUTOMATE]:
"https://www.browserstack.com/list-of-browsers-and-platforms/app_automate.json",
[BrowserStackProducts.SELENIUM_AUTOMATE]:
"https://www.browserstack.com/list-of-browsers-and-platforms/automate.json",
[BrowserStackProducts.PLAYWRIGHT_AUTOMATE]:
"https://www.browserstack.com/list-of-browsers-and-platforms/playwright.json",
};

/**
Expand Down
50 changes: 35 additions & 15 deletions src/lib/version-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export function resolveVersion(requested: string, available: string[]): string {
// strip duplicates & sort
const uniq = Array.from(new Set(available));

// pick min/max
if (requested === "latest" || requested === "oldest") {
// try numeric
Expand All @@ -21,29 +22,48 @@ export function resolveVersion(requested: string, available: string[]): string {
return requested === "latest" ? lex[lex.length - 1] : lex[0];
}

// exact?
// exact match?
if (uniq.includes(requested)) {
return requested;
}

// try closest numeric
// Try major version matching (e.g., "14" matches "14.0", "14.1", etc.)
const reqNum = parseFloat(requested);
const nums = uniq
.map((v) => ({ v, n: parseFloat(v) }))
.filter((x) => !isNaN(x.n));
if (!isNaN(reqNum) && nums.length) {
let best = nums[0],
bestDiff = Math.abs(nums[0].n - reqNum);
for (const x of nums) {
const d = Math.abs(x.n - reqNum);
if (d < bestDiff) {
best = x;
bestDiff = d;
if (!isNaN(reqNum)) {
const majorVersionMatches = uniq.filter((v) => {
const vNum = parseFloat(v);
return !isNaN(vNum) && Math.floor(vNum) === Math.floor(reqNum);
});

if (majorVersionMatches.length > 0) {
// If multiple matches, prefer the most common format or latest
const exactMatch = majorVersionMatches.find(
(v) => v === `${Math.floor(reqNum)}.0`,
);
if (exactMatch) {
return exactMatch;
}
// Return the first match (usually the most common format)
return majorVersionMatches[0];
}
}

// Fuzzy matching: find the closest version
const reqNumForFuzzy = parseFloat(requested);
if (!isNaN(reqNumForFuzzy)) {
const numericVersions = uniq
.map((v) => ({ v, n: parseFloat(v) }))
.filter((x) => !isNaN(x.n))
.sort(
(a, b) =>
Math.abs(a.n - reqNumForFuzzy) - Math.abs(b.n - reqNumForFuzzy),
);

if (numericVersions.length > 0) {
return numericVersions[0].v;
}
return best.v;
}

// final fallback
// Fallback: return the first available version
return uniq[0];
}
92 changes: 61 additions & 31 deletions src/tools/appautomate-utils/appium-sdk/config-generator.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
// Configuration utilities for BrowserStack App SDK
import {
APP_DEVICE_CONFIGS,
AppSDKSupportedTestingFrameworkEnum,
DEFAULT_APP_PATH,
createStep,
} from "./index.js";
import { ValidatedEnvironment } from "../../sdk-utils/common/device-validator.js";

export function generateAppBrowserStackYMLInstructions(
platforms: string[],
config: {
validatedEnvironments?: ValidatedEnvironment[];
platforms?: string[];
testingFramework?: string;
projectName?: string;
},
username: string,
accessKey: string,
appPath: string = DEFAULT_APP_PATH,
testingFramework: string,
): string {
if (
testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch ||
testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio ||
testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby
config.testingFramework ===
AppSDKSupportedTestingFrameworkEnum.nightwatch ||
config.testingFramework ===
AppSDKSupportedTestingFrameworkEnum.webdriverio ||
config.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 "";
const platformConfigs = generatePlatformConfigs(config);

return devices
.map(
(device) => ` - platformName: ${platform}
deviceName: ${device.deviceName}
platformVersion: "${device.platformVersion}"`,
)
.join("\n");
})
.filter(Boolean)
.join("\n");
const projectName = config.projectName || "BrowserStack Sample";
const buildName = config.projectName
? `${config.projectName}-AppAutomate-Build`
: "bstack-demo";

// Construct YAML content
const configContent = `\`\`\`yaml
userName: ${username}
accessKey: ${accessKey}
Expand All @@ -48,8 +42,9 @@ platforms:
${platformConfigs}
parallelsPerPlatform: 1
browserstackLocal: true
buildName: bstack-demo
projectName: BrowserStack Sample
// TODO: replace projectName and buildName according to actual project
projectName: ${projectName}
buildName: ${buildName}
debug: true
networkLogs: true
percy: false
Expand All @@ -63,11 +58,46 @@ accessibility: false
- 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:
const stepTitle =
"Update browserstack.yml file with App Automate configuration:";

const stepDescription = `Create or update the browserstack.yml file in your project root with the following content:
${configContent}`;

return createStep(stepTitle, stepDescription);
}

function generatePlatformConfigs(config: {
validatedEnvironments?: ValidatedEnvironment[];
platforms?: string[];
}): string {
if (config.validatedEnvironments && config.validatedEnvironments.length > 0) {
return config.validatedEnvironments
.filter((env) => env.platform === "android" || env.platform === "ios")
.map((env) => {
return ` - platformName: ${env.platform}
deviceName: "${env.deviceName}"
platformVersion: "${env.osVersion}"`;
})
.join("\n");
} else if (config.platforms && config.platforms.length > 0) {
return config.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");
}

${configContent}`,
);
return "";
}
31 changes: 28 additions & 3 deletions src/tools/appautomate-utils/appium-sdk/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,35 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
"The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'",
),

desiredPlatforms: z
.array(z.nativeEnum(AppSDKSupportedPlatformEnum))
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'"),
]),
]),
)
.max(3)
.default([])
.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']",
"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']]",
),

appPath: z
Expand Down
24 changes: 21 additions & 3 deletions src/tools/appautomate-utils/appium-sdk/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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 {
getAppUploadInstruction,
validateSupportforAppAutomate,
Expand Down Expand Up @@ -36,13 +38,25 @@ export async function setupAppAutomateHandler(
const testingFramework =
input.detectedTestingFramework as AppSDKSupportedTestingFramework;
const language = input.detectedLanguage as AppSDKSupportedLanguage;
const platforms = (input.desiredPlatforms as string[]) ?? ["android"];
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;

// Validate devices against real BrowserStack device data
const validatedEnvironments = await validateAppAutomateDevices(devices);

// Extract platforms for backward compatibility (if needed)
const platforms = validatedEnvironments.map((env) => env.platform);

// Step 1: Generate SDK setup command
const sdkCommand = getAppSDKPrefixCommand(
language,
Expand All @@ -58,11 +72,15 @@ export async function setupAppAutomateHandler(

// Step 2: Generate browserstack.yml configuration
const configInstructions = generateAppBrowserStackYMLInstructions(
platforms,
{
validatedEnvironments,
platforms,
testingFramework,
projectName: input.project as string,
},
username,
accessKey,
appPath,
testingFramework,
);

if (configInstructions) {
Expand Down
30 changes: 28 additions & 2 deletions src/tools/appautomate-utils/native-execution/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod";
import { AppTestPlatform } from "./types.js";
import { AppSDKSupportedPlatformEnum } from "../appium-sdk/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.`;

Expand Down Expand Up @@ -29,9 +30,34 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
"If in other directory, provide existing test file path",
),
devices: z
.array(z.string())
.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'"),
]),
]),
)
.max(3)
.default([])
.describe(
"List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'iPhone 12 Pro-16.0'].",
"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']]",
),
project: z
.string()
Expand Down
Loading