From 7b2c99a2fca383f22e37cd06656d770cda03b5d3 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 20 Aug 2025 19:52:08 +0530 Subject: [PATCH 01/84] chore: bump version to 1.2.3 in package.json and package-lock.json --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f62c471c..4120c7e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.2", + "version": "1.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@browserstack/mcp-server", - "version": "1.2.2", + "version": "1.2.3", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.4", diff --git a/package.json b/package.json index 5afe2d93..34c9a474 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.2", + "version": "1.2.3", "description": "BrowserStack's Official MCP Server", "main": "dist/index.js", "repository": { From 9a6df701e7bf50cdc10e0529106ce17608be6f07 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 22 Aug 2025 23:31:50 +0530 Subject: [PATCH 02/84] feat: Add support in App Automate for Appium --- .../appium-sdk/config-generator.ts | 58 ++++ .../appautomate-utils/appium-sdk/constants.ts | 63 ++++ .../appautomate-utils/appium-sdk/formatter.ts | 87 ++++++ .../appautomate-utils/appium-sdk/index.ts | 12 + .../appium-sdk/instructions.ts | 66 ++++ .../appium-sdk/languages/csharp.ts | 112 +++++++ .../appium-sdk/languages/java.ts | 142 +++++++++ .../appium-sdk/languages/nodejs.ts | 292 ++++++++++++++++++ .../appium-sdk/languages/python.ts | 156 ++++++++++ .../appium-sdk/languages/ruby.ts | 209 +++++++++++++ .../appautomate-utils/appium-sdk/types.ts | 80 +++++ .../appautomate-utils/appium-sdk/utils.ts | 47 +++ src/tools/appautomate-utils/handler.ts | 79 +++++ .../{ => native-execution}/appautomate.ts | 6 +- .../native-execution/constants.ts | 46 +++ .../native-execution/types.ts | 22 ++ src/tools/appautomate-utils/types.ts | 5 - src/tools/appautomate.ts | 104 +++---- 18 files changed, 1515 insertions(+), 71 deletions(-) create mode 100644 src/tools/appautomate-utils/appium-sdk/config-generator.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/constants.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/formatter.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/index.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/instructions.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/languages/csharp.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/languages/java.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/languages/python.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/languages/ruby.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/types.ts create mode 100644 src/tools/appautomate-utils/appium-sdk/utils.ts create mode 100644 src/tools/appautomate-utils/handler.ts rename src/tools/appautomate-utils/{ => native-execution}/appautomate.ts (97%) create mode 100644 src/tools/appautomate-utils/native-execution/constants.ts create mode 100644 src/tools/appautomate-utils/native-execution/types.ts delete mode 100644 src/tools/appautomate-utils/types.ts 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..716cc402 --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -0,0 +1,58 @@ +// Configuration utilities for BrowserStack App SDK +import { APP_DEVICE_CONFIGS, DEFAULT_APP_PATH, createStep } from "./index.js"; + +export function generateAppBrowserStackYMLInstructions( + platforms: string[], + username: string, + accessKey: string, + appPath: string = DEFAULT_APP_PATH, + testingFramework?: string, +): string { + 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"); + + const configContent = `\`\`\`yaml +userName: ${username} +accessKey: ${accessKey} +framework: ${testingFramework} +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 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..ba7f592e --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -0,0 +1,63 @@ +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://)", + ), +}; 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/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..896a5acd --- /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(testingFramework); + case "csharp": + return getCSharpAppInstructions(); + default: + return ""; + } +} + +export function getAppSDKPrefixCommand( + language: AppSDKSupportedLanguage, + framework: string, + username: string, + accessKey: string, + appPath?: string, +): string { + switch (language) { + case "csharp": + return getCSharpSDKCommand(username, accessKey); + case "java": + return getJavaSDKCommand(framework, username, accessKey, appPath); + case "nodejs": + return getNodejsSDKCommand(framework, username, accessKey); + case "python": + return getPythonSDKCommand(framework, username, accessKey); + case "ruby": + return getRubySDKCommand(framework, 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..2d8cc90e --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/java.ts @@ -0,0 +1,142 @@ +// Java instructions and commands for App SDK utilities +import logger from "../../../../logger.js"; +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, +): 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="${process.env.BROWSERSTACK_USERNAME}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" ` + + `-DBROWSERSTACK_FRAMEWORK="${framework}"` + ); +} + +// Generates Maven archetype command for Unix-like platforms (macOS/Linux) +function getMavenCommandForUnix( + username: string, + accessKey: string, + mavenFramework: string, + framework: 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 { + logger.info("Generating Java SDK command"); + const { isWindows = false, getPlatformLabel = () => "Unknown" } = + PLATFORM_UTILS || {}; + if (!PLATFORM_UTILS) { + console.warn("PLATFORM_UTILS is undefined. Defaulting platform values."); + } + + const mavenFramework = getJavaAppFrameworkForMaven(framework); + + let mavenCommand: string; + logger.info( + `Maven command for ${framework} (${getPlatformLabel()}): ${isWindows}`, + ); + if (isWindows) { + mavenCommand = getMavenCommandForWindows(framework, mavenFramework); + if (appPath) { + mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; + } + } else { + mavenCommand = getMavenCommandForUnix( + username, + accessKey, + mavenFramework, + framework, + ); + if (appPath) { + mavenCommand += ` \\\n-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}`, + ); + + logger.info("Java SDK command generated successfully"); + 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..b0057d0c --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts @@ -0,0 +1,292 @@ +// Node.js instructions and commands for App SDK utilities +import { + AppSDKSupportedTestingFramework, + AppSDKSupportedTestingFrameworkEnum, + createStep, + combineInstructions, +} from "../index.js"; + +export function getNodejsSDKCommand( + framework: string, + username: string, + accessKey: string, +): string { + switch (framework) { + 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 App Automate test suite with Jest:", + "npm run [your-test-script-name]", + ); + case AppSDKSupportedTestingFrameworkEnum.mocha: + return createStep( + "Run your App Automate test suite with Mocha:", + "npm run [your-test-script-name]", + ); + case AppSDKSupportedTestingFrameworkEnum.cucumberJs: + return createStep( + "Run your App Automate test suite with CucumberJS:", + `\`\`\`bash + npm run [your-test-script-name] + \`\`\``, + ); + default: + return ""; + } +} + +function getWebDriverIOCommand(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 BrowserStack WDIO service:", + `\`\`\`bash +npm install @wdio/browserstack-service --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(envStep, installStep, configStep); +} + +function getNightwatchCommand(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 Nightwatch and BrowserStack integration:", + `\`\`\`bash +npm install nightwatch nightwatch-browserstack --save-dev +\`\`\``, + ); + + 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(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 jest @browserstack/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 mocha @browserstack/sdk +\`\`\``, + ); + + const configStep = createStep( + "Create a \\`browserstack.yml\\` file at the root of your project with the following content:", + `\`\`\`yaml +userName: ${username} +accessKey: ${accessKey} +app: bs://sample.app +platforms: + - platformName: android + deviceName: Samsung Galaxy S22 Ultra + platformVersion: '12.0' + - platformName: android + deviceName: Google Pixel 7 Pro + platformVersion: '13.0' + - platformName: android + deviceName: OnePlus 9 + platformVersion: '11.0' +parallelsPerPlatform: 1 +browserstackLocal: true +buildName: bstack-demo +projectName: BrowserStack Sample +CUSTOM_TAG_1: "You can set a custom Build Tag here" +debug: true +networkLogs: true +percy: false +percyCaptureMode: auto +\`\`\``, + ); + + return combineInstructions(envStep, installStep, configStep); +} + +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..33749dac --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts @@ -0,0 +1,209 @@ +// Ruby instructions and commands for App SDK utilities +import { + AppSDKSupportedTestingFramework, + AppSDKSupportedTestingFrameworkEnum, + createStep, + combineInstructions, + createEnvStep, + PLATFORM_UTILS, +} from "../index.js"; + +export function getRubyAppInstructions( + testingFramework: AppSDKSupportedTestingFramework, +): string { + if (testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby) { + return getCucumberRubyInstructions(); + } + + if (testingFramework === AppSDKSupportedTestingFrameworkEnum.rspec) { + return getRSpecInstructions(); + } + + return ""; +} + +function getCucumberRubyInstructions(): string { + const configStep = createStep( + "Create/Update the config file (config.yml) as follows:", + `\`\`\`yaml +server: "hub-cloud.browserstack.com" + +common_caps: + "project": "First Cucumber Android Project" + "build": "Cucumber Android" + "browserstack.debug": true + +browser_caps: + - + "device": "Google Pixel 3" + "os_version": "9.0" + "app": "bs://" + "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__) + +# Create desired capabilities +desired_caps = { + caps: caps, + appium_lib: { + server_url: "https://hub-cloud.browserstack.com/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); +} + +function getRSpecInstructions(): string { + const specHelperStep = createStep( + "Create/Update your spec_helper.rb file:", + `\`\`\`ruby +require 'rubygems' +require 'appium_lib' + +RSpec.configure do |config| + config.before(:all) do + # Define desired capabilities + desired_caps = { + 'platformName' => 'Android', + 'platformVersion' => '9.0', + 'deviceName' => 'Google Pixel 3', + 'app' => 'bs://', + 'project' => 'First RSpec Android Project', + 'build' => 'RSpec Android', + 'name' => 'first_test', + 'browserstack.debug' => true + } + + # Initialize Appium driver + begin + $appium_driver = Appium::Driver.new({ + caps: desired_caps, + appium_lib: { + server_url: "https://hub-cloud.browserstack.com/wd/hub" + } + }, true) + $driver = $appium_driver.start_driver + rescue Exception => e + puts e.message + Process.exit(0) + end + end + + config.after(:all) do + $driver.quit if $driver + end +end +\`\`\``, + ); + + const testFileStep = createStep( + "Create your test file (e.g., spec/app_spec.rb):", + `\`\`\`ruby +require 'spec_helper' + +describe 'App Test' do + it 'should launch the app successfully' do + # Your test code here + expect($driver).not_to be_nil + end +end +\`\`\``, + ); + + const runStep = createStep( + "Run the test:", + `\`\`\`bash +bundle exec rspec +\`\`\``, + ); + + return combineInstructions(specHelperStep, testFileStep, runStep); +} + +export function getRubySDKCommand( + framework: string, + username: string, + accessKey: string, +): string { + const { isWindows, getPlatformLabel } = PLATFORM_UTILS; + + if (framework === "rspec" || framework === "cucumberRuby") { + 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 + +# For Cucumber projects, also install cucumber +${framework === "cucumberRuby" ? "gem install cucumber" : ""} + +# For RSpec projects, also install rspec +${framework === "rspec" ? "gem install rspec" : ""} +\`\`\``, + ); + + const gemfileStep = createStep( + "Create a Gemfile for dependency management:", + `\`\`\`ruby +# Gemfile +source 'https://rubygems.org' + +gem 'appium_lib' +${framework === "cucumberRuby" ? "gem 'cucumber'" : ""} +${framework === "rspec" ? "gem 'rspec'" : ""} +\`\`\` + +Then run: +\`\`\`bash +bundle install +\`\`\``, + ); + + return combineInstructions(envStep, installStep, gemfileStep); + } + + return ""; +} 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..c52d9c1a --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/types.ts @@ -0,0 +1,80 @@ +// 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", + webdriverio = "webdriverio", + nightwatch = "nightwatch", + jest = "jest", + mocha = "mocha", + cucumberJs = "cucumberJs", + pytest = "pytest", + rspec = "rspec", + cucumberRuby = "cucumberRuby", +} + +export type AppSDKSupportedFramework = + keyof typeof AppSDKSupportedFrameworkEnum; + +export enum AppSDKSupportedTestingFrameworkEnum { + testng = "testng", + junit5 = "junit5", + 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; + +export type AppConfigMapping = Record< + AppSDKSupportedLanguageEnum, + Partial< + Record< + AppSDKSupportedFrameworkEnum, + Partial< + Record< + AppSDKSupportedTestingFrameworkEnum, + { instructions: (username: string, accessKey: string) => string } + > + > + > + > +>; + +// App SDK instruction type +export interface AppSDKInstruction { + content: string; + type: "config" | "run" | "setup"; +} 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..d9ef9b8f --- /dev/null +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -0,0 +1,47 @@ +export function getShellPrefix(): string { + return process.platform === "win32" ? "cmd" : "bash"; +} + +export function sanitizeInput(input: string): string { + // Basic sanitization - remove potentially dangerous characters + return input.replace(/[;&|`$(){}[\]]/g, ""); +} + +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"; + case "linux": + return "Linux"; + default: + return process.platform; + } + }, +}; diff --git a/src/tools/appautomate-utils/handler.ts b/src/tools/appautomate-utils/handler.ts new file mode 100644 index 00000000..8d515d51 --- /dev/null +++ b/src/tools/appautomate-utils/handler.ts @@ -0,0 +1,79 @@ +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 { + AppSDKSupportedLanguage, + AppSDKSupportedTestingFramework, + AppSDKInstruction, + formatAppInstructionsWithNumbers, + getAppInstructionsForProjectConfiguration, + SETUP_APP_AUTOMATE_SCHEMA, +} from "./appium-sdk/index.js"; +import { + getAppSDKPrefixCommand, + generateAppBrowserStackYMLInstructions, +} from "./appium-sdk/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[] = []; + + // Step 1: Generate SDK setup command + const sdkCommand = getAppSDKPrefixCommand( + input.detectedLanguage as AppSDKSupportedLanguage, + input.detectedFramework as string, + username, + accessKey, + input.appPath as string | undefined, + ); + + if (sdkCommand) { + instructions.push({ content: sdkCommand, type: "setup" }); + } + + // Step 2: Generate browserstack.yml configuration + const configInstructions = generateAppBrowserStackYMLInstructions( + (input.desiredPlatforms as string[]) ?? ["android"], + username, + accessKey, + input.appPath as string | undefined, + input.detectedTestingFramework as AppSDKSupportedTestingFramework, + ); + + if (configInstructions) { + instructions.push({ content: configInstructions, type: "config" }); + } + + // Step 3: Generate project configuration and run instructions + const projectInstructions = getAppInstructionsForProjectConfiguration( + input.detectedFramework as string, + input.detectedTestingFramework as AppSDKSupportedTestingFramework, + input.detectedLanguage as AppSDKSupportedLanguage, + ); + + 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/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..71f23889 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/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,27 @@ 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) { + return { + content: [ + { + type: "text", + text: `Failed to bootstrap project with BrowserStack App Automate SDK. Error: ${error}. Please open an issue on GitHub if the problem persists`, + isError: true, + }, + ], + isError: true, + }; + } + }, + ); + return tools; } From feb301d88f0a39be523279c96e5a7d25e232cbf0 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 25 Aug 2025 15:09:41 +0530 Subject: [PATCH 03/84] Refactoring ++ --- .../appium-sdk/config-generator.ts | 20 ++++- .../appautomate-utils/appium-sdk/constants.ts | 5 ++ .../{ => appium-sdk}/handler.ts | 53 +++++++++--- .../appium-sdk/instructions.ts | 10 +-- .../appium-sdk/languages/java.ts | 47 ++++------ .../appium-sdk/languages/nodejs.ts | 86 +++++++++---------- .../appautomate-utils/appium-sdk/utils.ts | 38 +++++--- src/tools/appautomate.ts | 2 +- tests/tools/appautomate.test.ts | 2 +- 9 files changed, 152 insertions(+), 111 deletions(-) rename src/tools/appautomate-utils/{ => appium-sdk}/handler.ts (59%) diff --git a/src/tools/appautomate-utils/appium-sdk/config-generator.ts b/src/tools/appautomate-utils/appium-sdk/config-generator.ts index 716cc402..2fef0df5 100644 --- a/src/tools/appautomate-utils/appium-sdk/config-generator.ts +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -1,13 +1,26 @@ // Configuration utilities for BrowserStack App SDK -import { APP_DEVICE_CONFIGS, DEFAULT_APP_PATH, createStep } from "./index.js"; +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, + testingFramework: string, ): string { + if ( + testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || + testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio + ) { + return ""; + } + + // Generate platform and device configurations const platformConfigs = platforms .map((platform) => { const devices = @@ -25,10 +38,10 @@ export function generateAppBrowserStackYMLInstructions( .filter(Boolean) .join("\n"); + // Construct YAML content const configContent = `\`\`\`yaml userName: ${username} accessKey: ${accessKey} -framework: ${testingFramework} app: ${appPath} platforms: ${platformConfigs} @@ -49,6 +62,7 @@ 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: diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index ba7f592e..c75872dd 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -60,4 +60,9 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { .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/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts similarity index 59% rename from src/tools/appautomate-utils/handler.ts rename to src/tools/appautomate-utils/appium-sdk/handler.ts index 8d515d51..3afb9c29 100644 --- a/src/tools/appautomate-utils/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -1,7 +1,7 @@ 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 { BrowserStackConfig } from "../../../lib/types.js"; +import { getBrowserStackAuth } from "../../../lib/get-auth.js"; import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework, @@ -9,11 +9,13 @@ import { formatAppInstructionsWithNumbers, getAppInstructionsForProjectConfiguration, SETUP_APP_AUTOMATE_SCHEMA, -} from "./appium-sdk/index.js"; +} from "./index.js"; import { getAppSDKPrefixCommand, generateAppBrowserStackYMLInstructions, -} from "./appium-sdk/index.js"; +} from "./index.js"; +import { getAppUploadInstruction } from "./utils.js"; +import logger from "../../../logger.js"; export async function setupAppAutomateHandler( rawInput: unknown, @@ -25,13 +27,24 @@ export async function setupAppAutomateHandler( 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 string; + + logger.info("Generating SDK setup command..."); + logger.debug(`Input: ${JSON.stringify(input)}`); + // Step 1: Generate SDK setup command const sdkCommand = getAppSDKPrefixCommand( - input.detectedLanguage as AppSDKSupportedLanguage, - input.detectedFramework as string, + language, + testingFramework, username, accessKey, - input.appPath as string | undefined, + appPath, ); if (sdkCommand) { @@ -40,22 +53,34 @@ export async function setupAppAutomateHandler( // Step 2: Generate browserstack.yml configuration const configInstructions = generateAppBrowserStackYMLInstructions( - (input.desiredPlatforms as string[]) ?? ["android"], + platforms, username, accessKey, - input.appPath as string | undefined, - input.detectedTestingFramework as AppSDKSupportedTestingFramework, + appPath, + testingFramework, ); if (configInstructions) { instructions.push({ content: configInstructions, type: "config" }); } - // Step 3: Generate project configuration and run instructions + // 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( - input.detectedFramework as string, - input.detectedTestingFramework as AppSDKSupportedTestingFramework, - input.detectedLanguage as AppSDKSupportedLanguage, + framework, + testingFramework, + language, ); if (projectInstructions) { diff --git a/src/tools/appautomate-utils/appium-sdk/instructions.ts b/src/tools/appautomate-utils/appium-sdk/instructions.ts index 896a5acd..25bc676b 100644 --- a/src/tools/appautomate-utils/appium-sdk/instructions.ts +++ b/src/tools/appautomate-utils/appium-sdk/instructions.ts @@ -44,7 +44,7 @@ export function getAppInstructionsForProjectConfiguration( export function getAppSDKPrefixCommand( language: AppSDKSupportedLanguage, - framework: string, + testingFramework: string, username: string, accessKey: string, appPath?: string, @@ -53,13 +53,13 @@ export function getAppSDKPrefixCommand( case "csharp": return getCSharpSDKCommand(username, accessKey); case "java": - return getJavaSDKCommand(framework, username, accessKey, appPath); + return getJavaSDKCommand(testingFramework, username, accessKey, appPath); case "nodejs": - return getNodejsSDKCommand(framework, username, accessKey); + return getNodejsSDKCommand(testingFramework, username, accessKey); case "python": - return getPythonSDKCommand(framework, username, accessKey); + return getPythonSDKCommand(testingFramework, username, accessKey); case "ruby": - return getRubySDKCommand(framework, username, accessKey); + return getRubySDKCommand(testingFramework, username, accessKey); default: return ""; } diff --git a/src/tools/appautomate-utils/appium-sdk/languages/java.ts b/src/tools/appautomate-utils/appium-sdk/languages/java.ts index 2d8cc90e..794b249a 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/java.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/java.ts @@ -1,5 +1,4 @@ // Java instructions and commands for App SDK utilities -import logger from "../../../../logger.js"; import { createStep, combineInstructions, @@ -68,19 +67,22 @@ function getMavenCommandForWindows( ); } -// Generates Maven archetype command for Unix-like platforms (macOS/Linux) function getMavenCommandForUnix( - username: string, - accessKey: string, - mavenFramework: string, framework: string, + mavenFramework: 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}"`; + 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="${process.env.BROWSERSTACK_USERNAME}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" ` + + `-DBROWSERSTACK_FRAMEWORK="${framework}"` + ); } export function getJavaSDKCommand( @@ -89,33 +91,21 @@ export function getJavaSDKCommand( accessKey: string, appPath?: string, ): string { - logger.info("Generating Java SDK command"); - const { isWindows = false, getPlatformLabel = () => "Unknown" } = - PLATFORM_UTILS || {}; - if (!PLATFORM_UTILS) { - console.warn("PLATFORM_UTILS is undefined. Defaulting platform values."); - } + const { isWindows = false, getPlatformLabel } = PLATFORM_UTILS || {}; const mavenFramework = getJavaAppFrameworkForMaven(framework); let mavenCommand: string; - logger.info( - `Maven command for ${framework} (${getPlatformLabel()}): ${isWindows}`, - ); + if (isWindows) { mavenCommand = getMavenCommandForWindows(framework, mavenFramework); if (appPath) { mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; } } else { - mavenCommand = getMavenCommandForUnix( - username, - accessKey, - mavenFramework, - framework, - ); + mavenCommand = getMavenCommandForUnix(framework, mavenFramework); if (appPath) { - mavenCommand += ` \\\n-DBROWSERSTACK_APP="${appPath}"`; + mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; } } @@ -128,7 +118,7 @@ export function getJavaSDKCommand( const mavenStep = createStep( "Install BrowserStack SDK using Maven Archetype for App Automate", - `**Maven command for ${framework} (${getPlatformLabel()}):** + `Maven command for ${framework} (${getPlatformLabel()}): \`\`\`bash ${mavenCommand} \`\`\` @@ -137,6 +127,5 @@ Alternative setup for Gradle users: ${GRADLE_APP_SETUP_INSTRUCTIONS}`, ); - logger.info("Java SDK command generated successfully"); 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 index b0057d0c..a39f5931 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts @@ -7,11 +7,11 @@ import { } from "../index.js"; export function getNodejsSDKCommand( - framework: string, + testingFramework: string, username: string, accessKey: string, ): string { - switch (framework) { + switch (testingFramework) { case "webdriverio": return getWebDriverIOCommand(username, accessKey); case "nightwatch": @@ -50,20 +50,19 @@ export function getNodejsAppInstructions( ); case AppSDKSupportedTestingFrameworkEnum.jest: return createStep( - "Run your App Automate test suite with Jest:", - "npm run [your-test-script-name]", + "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 App Automate test suite with Mocha:", - "npm run [your-test-script-name]", + "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 App Automate test suite with CucumberJS:", - `\`\`\`bash - npm run [your-test-script-name] - \`\`\``, + "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 ""; @@ -71,6 +70,14 @@ export function getNodejsAppInstructions( } 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 @@ -82,7 +89,7 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} const installStep = createStep( "Install BrowserStack WDIO service:", `\`\`\`bash -npm install @wdio/browserstack-service --save-dev +npm install @wdio/browserstack-service@^7 --save-dev \`\`\``, ); @@ -129,13 +136,26 @@ exports.config = { \`\`\``, ); - return combineInstructions(envStep, installStep, configStep); + 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 +`\`\`\`bash export BROWSERSTACK_USERNAME=${username} export BROWSERSTACK_ACCESS_KEY=${accessKey} \`\`\``, @@ -143,8 +163,8 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} const installStep = createStep( "Install Nightwatch and BrowserStack integration:", - `\`\`\`bash -npm install nightwatch nightwatch-browserstack --save-dev +`\`\`\`bash +npm install --save-dev @nightwatch/browserstack \`\`\``, ); @@ -212,7 +232,7 @@ npm install nightwatch nightwatch-browserstack --save-dev \`\`\``, ); - return combineInstructions(envStep, installStep, configStep); + return combineInstructions(prerequisiteStep, envStep, installStep, configStep); } function getJestCommand(username: string, accessKey: string): string { @@ -227,7 +247,7 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} const installStep = createStep( "Install Jest and BrowserStack SDK:", `\`\`\`bash -npm install --save-dev jest @browserstack/sdk +npm install --save-dev browserstack-node-sdk \`\`\``, ); @@ -246,39 +266,11 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} const installStep = createStep( "Install Mocha and BrowserStack SDK:", `\`\`\`bash -npm install --save-dev mocha @browserstack/sdk +npm install --save-dev browserstack-node-sdk \`\`\``, ); - const configStep = createStep( - "Create a \\`browserstack.yml\\` file at the root of your project with the following content:", - `\`\`\`yaml -userName: ${username} -accessKey: ${accessKey} -app: bs://sample.app -platforms: - - platformName: android - deviceName: Samsung Galaxy S22 Ultra - platformVersion: '12.0' - - platformName: android - deviceName: Google Pixel 7 Pro - platformVersion: '13.0' - - platformName: android - deviceName: OnePlus 9 - platformVersion: '11.0' -parallelsPerPlatform: 1 -browserstackLocal: true -buildName: bstack-demo -projectName: BrowserStack Sample -CUSTOM_TAG_1: "You can set a custom Build Tag here" -debug: true -networkLogs: true -percy: false -percyCaptureMode: auto -\`\`\``, - ); - - return combineInstructions(envStep, installStep, configStep); + return combineInstructions(envStep, installStep); } function getCucumberJSCommand(username: string, accessKey: string): string { diff --git a/src/tools/appautomate-utils/appium-sdk/utils.ts b/src/tools/appautomate-utils/appium-sdk/utils.ts index d9ef9b8f..8cfe6138 100644 --- a/src/tools/appautomate-utils/appium-sdk/utils.ts +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -1,11 +1,8 @@ -export function getShellPrefix(): string { - return process.platform === "win32" ? "cmd" : "bash"; -} - -export function sanitizeInput(input: string): string { - // Basic sanitization - remove potentially dangerous characters - return input.replace(/[;&|`$(){}[\]]/g, ""); -} +import { + AppSDKSupportedTestingFramework, + AppSDKSupportedTestingFrameworkEnum, + createStep, +} from "./index.js"; export function isBrowserStackAppUrl(appPath: string): boolean { return appPath.startsWith("bs://"); @@ -38,10 +35,29 @@ export const PLATFORM_UTILS = { return "Windows"; case "darwin": return "macOS"; - case "linux": - return "Linux"; default: - return process.platform; + return "macOS"; } }, }; + +export async function getAppUploadInstruction( + appPath: string, + username: string, + accessKey: string, + detectedTestingFramework: AppSDKSupportedTestingFramework, +): Promise { + if ( + detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || + detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio + ) { + const app_url = "bs://ff4e358328a3e914fe4f0e46ec7af73f9c08cd55"; + if (app_url) { + return createStep( + "Updating app_path with app_url", + `Replace the value of app_path in your configuration with: ${app_url}`, + ); + } + } + return ""; +} diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 71f23889..0bdefb11 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -8,7 +8,7 @@ import { trackMCP } from "../lib/instrumentation.js"; import { maybeCompressBase64 } from "../lib/utils.js"; import { remote } from "webdriverio"; import { AppTestPlatform } from "./appautomate-utils/native-execution/types.js"; -import { setupAppAutomateHandler } from "./appautomate-utils/handler.js"; +import { setupAppAutomateHandler } from "./appautomate-utils/appium-sdk/handler.js"; import { SETUP_APP_AUTOMATE_DESCRIPTION, 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' From 326c64a1b594cf165b17aa770c4c3e218deb8d5b Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 26 Aug 2025 13:13:16 +0530 Subject: [PATCH 04/84] Enhance App SDK support for additional testing frameworks --- .../appium-sdk/config-generator.ts | 3 +- .../appium-sdk/instructions.ts | 2 +- .../appium-sdk/languages/nodejs.ts | 13 ++- .../appium-sdk/languages/ruby.ts | 106 +++--------------- .../appautomate-utils/appium-sdk/utils.ts | 3 +- 5 files changed, 27 insertions(+), 100 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/config-generator.ts b/src/tools/appautomate-utils/appium-sdk/config-generator.ts index 2fef0df5..bec4f667 100644 --- a/src/tools/appautomate-utils/appium-sdk/config-generator.ts +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -15,7 +15,8 @@ export function generateAppBrowserStackYMLInstructions( ): string { if ( testingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || - testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio + testingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio || + testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby ) { return ""; } diff --git a/src/tools/appautomate-utils/appium-sdk/instructions.ts b/src/tools/appautomate-utils/appium-sdk/instructions.ts index 25bc676b..b1d73d4c 100644 --- a/src/tools/appautomate-utils/appium-sdk/instructions.ts +++ b/src/tools/appautomate-utils/appium-sdk/instructions.ts @@ -34,7 +34,7 @@ export function getAppInstructionsForProjectConfiguration( case "python": return getPythonAppInstructions(testingFramework); case "ruby": - return getRubyAppInstructions(testingFramework); + return getRubyAppInstructions(); case "csharp": return getCSharpAppInstructions(); default: diff --git a/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts b/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts index a39f5931..fe4cff3d 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/nodejs.ts @@ -145,7 +145,7 @@ exports.config = { } function getNightwatchCommand(username: string, accessKey: string): string { - const prerequisiteStep = createStep( + 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. @@ -155,7 +155,7 @@ function getNightwatchCommand(username: string, accessKey: string): string { const envStep = createStep( "Set your BrowserStack credentials as environment variables:", -`\`\`\`bash + `\`\`\`bash export BROWSERSTACK_USERNAME=${username} export BROWSERSTACK_ACCESS_KEY=${accessKey} \`\`\``, @@ -163,7 +163,7 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} const installStep = createStep( "Install Nightwatch and BrowserStack integration:", -`\`\`\`bash + `\`\`\`bash npm install --save-dev @nightwatch/browserstack \`\`\``, ); @@ -232,7 +232,12 @@ npm install --save-dev @nightwatch/browserstack \`\`\``, ); - return combineInstructions(prerequisiteStep, envStep, installStep, configStep); + return combineInstructions( + prerequisiteStep, + envStep, + installStep, + configStep, + ); } function getJestCommand(username: string, accessKey: string): string { diff --git a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts index 33749dac..15f69af1 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts @@ -1,41 +1,30 @@ // Ruby instructions and commands for App SDK utilities import { - AppSDKSupportedTestingFramework, - AppSDKSupportedTestingFrameworkEnum, createStep, combineInstructions, createEnvStep, PLATFORM_UTILS, } from "../index.js"; -export function getRubyAppInstructions( - testingFramework: AppSDKSupportedTestingFramework, -): string { - if (testingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby) { - return getCucumberRubyInstructions(); - } - - if (testingFramework === AppSDKSupportedTestingFrameworkEnum.rspec) { - return getRSpecInstructions(); - } +const username = "${process.env.BROWSERSTACK_USERNAME}"; +const accessKey = "${process.env.BROWSERSTACK_ACCESS_KEY}"; - return ""; -} - -function getCucumberRubyInstructions(): string { +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: - - "device": "Google Pixel 3" + "deviceName": "Google Pixel 3" "os_version": "9.0" "app": "bs://" "name": "first_test" @@ -50,12 +39,14 @@ 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://hub-cloud.browserstack.com/wd/hub" + server_url = "https://#{username}:#{password}@#{caps['server']}/wd/hub" } } @@ -85,73 +76,6 @@ bundle exec cucumber return combineInstructions(configStep, envStep, runStep); } -function getRSpecInstructions(): string { - const specHelperStep = createStep( - "Create/Update your spec_helper.rb file:", - `\`\`\`ruby -require 'rubygems' -require 'appium_lib' - -RSpec.configure do |config| - config.before(:all) do - # Define desired capabilities - desired_caps = { - 'platformName' => 'Android', - 'platformVersion' => '9.0', - 'deviceName' => 'Google Pixel 3', - 'app' => 'bs://', - 'project' => 'First RSpec Android Project', - 'build' => 'RSpec Android', - 'name' => 'first_test', - 'browserstack.debug' => true - } - - # Initialize Appium driver - begin - $appium_driver = Appium::Driver.new({ - caps: desired_caps, - appium_lib: { - server_url: "https://hub-cloud.browserstack.com/wd/hub" - } - }, true) - $driver = $appium_driver.start_driver - rescue Exception => e - puts e.message - Process.exit(0) - end - end - - config.after(:all) do - $driver.quit if $driver - end -end -\`\`\``, - ); - - const testFileStep = createStep( - "Create your test file (e.g., spec/app_spec.rb):", - `\`\`\`ruby -require 'spec_helper' - -describe 'App Test' do - it 'should launch the app successfully' do - # Your test code here - expect($driver).not_to be_nil - end -end -\`\`\``, - ); - - const runStep = createStep( - "Run the test:", - `\`\`\`bash -bundle exec rspec -\`\`\``, - ); - - return combineInstructions(specHelperStep, testFileStep, runStep); -} - export function getRubySDKCommand( framework: string, username: string, @@ -159,7 +83,7 @@ export function getRubySDKCommand( ): string { const { isWindows, getPlatformLabel } = PLATFORM_UTILS; - if (framework === "rspec" || framework === "cucumberRuby") { + if (framework === "cucumberRuby") { const envStep = createEnvStep( username, accessKey, @@ -177,11 +101,8 @@ gem install bundler # Install Appium Ruby client library gem install appium_lib -# For Cucumber projects, also install cucumber -${framework === "cucumberRuby" ? "gem install cucumber" : ""} - -# For RSpec projects, also install rspec -${framework === "rspec" ? "gem install rspec" : ""} +# Install Cucumber +gem install cucumber \`\`\``, ); @@ -192,8 +113,7 @@ ${framework === "rspec" ? "gem install rspec" : ""} source 'https://rubygems.org' gem 'appium_lib' -${framework === "cucumberRuby" ? "gem 'cucumber'" : ""} -${framework === "rspec" ? "gem 'rspec'" : ""} +gem 'cucumber' \`\`\` Then run: diff --git a/src/tools/appautomate-utils/appium-sdk/utils.ts b/src/tools/appautomate-utils/appium-sdk/utils.ts index 8cfe6138..e5ba2227 100644 --- a/src/tools/appautomate-utils/appium-sdk/utils.ts +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -48,7 +48,8 @@ export async function getAppUploadInstruction( detectedTestingFramework: AppSDKSupportedTestingFramework, ): Promise { if ( - detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || + detectedTestingFramework === + AppSDKSupportedTestingFrameworkEnum.nightwatch || detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio ) { const app_url = "bs://ff4e358328a3e914fe4f0e46ec7af73f9c08cd55"; From e096ad399466b25f297a1b4bcfa1317e0b8728c6 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 26 Aug 2025 19:18:13 +0530 Subject: [PATCH 05/84] Additional improvements --- .../appautomate-utils/appium-sdk/handler.ts | 21 ++++---- .../appium-sdk/languages/ruby.ts | 54 +++++++++---------- .../appautomate-utils/appium-sdk/types.ts | 36 +++++-------- .../appautomate-utils/appium-sdk/utils.ts | 35 +++++++++++- src/tools/appautomate.ts | 3 +- 5 files changed, 85 insertions(+), 64 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index 3afb9c29..c325665d 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -2,6 +2,14 @@ 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 logger from "../../../logger.js"; + +import { + getAppSDKPrefixCommand, + generateAppBrowserStackYMLInstructions, +} from "./index.js"; + import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework, @@ -10,17 +18,12 @@ import { getAppInstructionsForProjectConfiguration, SETUP_APP_AUTOMATE_SCHEMA, } from "./index.js"; -import { - getAppSDKPrefixCommand, - generateAppBrowserStackYMLInstructions, -} from "./index.js"; -import { getAppUploadInstruction } from "./utils.js"; -import logger from "../../../logger.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(":"); @@ -33,10 +36,10 @@ export async function setupAppAutomateHandler( const language = input.detectedLanguage as AppSDKSupportedLanguage; const platforms = (input.desiredPlatforms as string[]) ?? ["android"]; const appPath = input.appPath as string; - const framework = input.detectedFramework as string; + const framework = input.detectedFramework as SupportedFramework; - logger.info("Generating SDK setup command..."); - logger.debug(`Input: ${JSON.stringify(input)}`); + //Validating if supported framework or not + validateSupportforAppAutomate(framework, language, testingFramework); // Step 1: Generate SDK setup command const sdkCommand = getAppSDKPrefixCommand( diff --git a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts index 15f69af1..dc5e8ac7 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts @@ -26,9 +26,9 @@ browser_caps: - "deviceName": "Google Pixel 3" "os_version": "9.0" - "app": "bs://" + "app": "" "name": "first_test" -\`\`\``, +\`\`\`` ); const envStep = createStep( @@ -46,7 +46,7 @@ password = "${accessKey}" desired_caps = { caps: caps, appium_lib: { - server_url = "https://#{username}:#{password}@#{caps['server']}/wd/hub" + server_url: "https://#{username}:#{password}@#{caps['server']}/wd/hub" } } @@ -63,14 +63,14 @@ end at_exit do $driver.quit if $driver end -\`\`\``, +\`\`\`` ); const runStep = createStep( "Run the test:", `\`\`\`bash bundle exec cucumber -\`\`\``, +\`\`\`` ); return combineInstructions(configStep, envStep, runStep); @@ -79,22 +79,21 @@ bundle exec cucumber export function getRubySDKCommand( framework: string, username: string, - accessKey: string, + accessKey: string ): string { const { isWindows, getPlatformLabel } = PLATFORM_UTILS; - if (framework === "cucumberRuby") { - const envStep = createEnvStep( - username, - accessKey, - isWindows, - getPlatformLabel(), - "Set your BrowserStack credentials as environment variables:", - ); - - const installStep = createStep( - "Install required Ruby gems:", - `\`\`\`bash + 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 @@ -103,12 +102,12 @@ gem install appium_lib # Install Cucumber gem install cucumber -\`\`\``, - ); +\`\`\`` + ); - const gemfileStep = createStep( - "Create a Gemfile for dependency management:", - `\`\`\`ruby + const gemfileStep = createStep( + "Create a Gemfile for dependency management:", + `\`\`\`ruby # Gemfile source 'https://rubygems.org' @@ -119,11 +118,8 @@ gem 'cucumber' Then run: \`\`\`bash bundle install -\`\`\``, - ); - - return combineInstructions(envStep, installStep, gemfileStep); - } +\`\`\`` + ); - return ""; + 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 index c52d9c1a..07fc7e5f 100644 --- a/src/tools/appautomate-utils/appium-sdk/types.ts +++ b/src/tools/appautomate-utils/appium-sdk/types.ts @@ -9,15 +9,7 @@ export enum AppSDKSupportedLanguageEnum { export type AppSDKSupportedLanguage = keyof typeof AppSDKSupportedLanguageEnum; export enum AppSDKSupportedFrameworkEnum { - appium = "appium", - webdriverio = "webdriverio", - nightwatch = "nightwatch", - jest = "jest", - mocha = "mocha", - cucumberJs = "cucumberJs", - pytest = "pytest", - rspec = "rspec", - cucumberRuby = "cucumberRuby", + appium = "appium" } export type AppSDKSupportedFramework = @@ -26,6 +18,7 @@ export type AppSDKSupportedFramework = export enum AppSDKSupportedTestingFrameworkEnum { testng = "testng", junit5 = "junit5", + junit4 = "junit4", selenide = "selenide", jbehave = "jbehave", cucumberTestng = "cucumberTestng", @@ -58,23 +51,18 @@ export enum AppSDKSupportedPlatformEnum { } export type AppSDKSupportedPlatform = keyof typeof AppSDKSupportedPlatformEnum; -export type AppConfigMapping = Record< - AppSDKSupportedLanguageEnum, - Partial< - Record< - AppSDKSupportedFrameworkEnum, - Partial< - Record< - AppSDKSupportedTestingFrameworkEnum, - { instructions: (username: string, accessKey: string) => string } - > - > - > - > ->; - // 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 index e5ba2227..312286ce 100644 --- a/src/tools/appautomate-utils/appium-sdk/utils.ts +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -3,6 +3,7 @@ import { AppSDKSupportedTestingFrameworkEnum, createStep, } from "./index.js"; +import {SUPPORTED_CONFIGURATIONS} from "./types.js" export function isBrowserStackAppUrl(appPath: string): boolean { return appPath.startsWith("bs://"); @@ -50,7 +51,8 @@ export async function getAppUploadInstruction( if ( detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || - detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio + detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio || + detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby ) { const app_url = "bs://ff4e358328a3e914fe4f0e46ec7af73f9c08cd55"; if (app_url) { @@ -62,3 +64,34 @@ export async function getAppUploadInstruction( } 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.ts b/src/tools/appautomate.ts index 0bdefb11..4ff8eba9 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -393,11 +393,12 @@ export default function addAppAutomationTools( 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}. Please open an issue on GitHub if the problem persists`, + 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, }, ], From d7eeeaeba0064e8fd0a8275569fdae7db5848858 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 28 Aug 2025 11:48:56 +0530 Subject: [PATCH 06/84] Percy setup agent integration --- src/lib/inmemory-store.ts | 1 + src/server-factory.ts | 2 + src/tools/add-percy-snapshots.ts | 32 + src/tools/bstack-sdk.ts | 247 +---- src/tools/list-test-files.ts | 39 + src/tools/percy-sdk.ts | 159 +++ src/tools/percy-snapshot-utils/constants.ts | 514 +++++++++ .../percy-snapshot-utils/detect-test-files.ts | 265 +++++ src/tools/percy-snapshot-utils/types.ts | 20 + src/tools/percy-snapshot-utils/utils.ts | 42 + src/tools/sdk-utils/bstack/commands.ts | 123 +++ src/tools/sdk-utils/bstack/configUtils.ts | 72 ++ src/tools/sdk-utils/{ => bstack}/constants.ts | 211 ++-- src/tools/sdk-utils/bstack/frameworks.ts | 59 ++ src/tools/sdk-utils/bstack/index.ts | 5 + src/tools/sdk-utils/bstack/sdkHandler.ts | 123 +++ src/tools/sdk-utils/commands.ts | 81 -- src/tools/sdk-utils/common/constants.ts | 102 ++ src/tools/sdk-utils/common/formatUtils.ts | 34 + src/tools/sdk-utils/common/index.ts | 4 + .../sdk-utils/common/instructionUtils.ts | 49 + src/tools/sdk-utils/common/schema.ts | 52 + src/tools/sdk-utils/common/types.ts | 94 ++ src/tools/sdk-utils/common/utils.ts | 128 +++ src/tools/sdk-utils/handler.ts | 204 ++++ src/tools/sdk-utils/instructions.ts | 138 --- .../sdk-utils/percy-automate/constants.ts | 375 +++++++ .../sdk-utils/percy-automate/frameworks.ts | 56 + src/tools/sdk-utils/percy-automate/handler.ts | 43 + src/tools/sdk-utils/percy-automate/index.ts | 2 + src/tools/sdk-utils/percy-automate/types.ts | 13 + .../{percy => percy-bstack}/constants.ts | 54 +- .../sdk-utils/percy-bstack/frameworks.ts | 29 + src/tools/sdk-utils/percy-bstack/handler.ts | 158 +++ src/tools/sdk-utils/percy-bstack/index.ts | 8 + .../{percy => percy-bstack}/instructions.ts | 26 +- src/tools/sdk-utils/percy-bstack/types.ts | 14 + src/tools/sdk-utils/percy-web/constants.ts | 981 ++++++++++++++++++ .../sdk-utils/percy-web/fetchPercyToken.ts | 45 + src/tools/sdk-utils/percy-web/frameworks.ts | 109 ++ src/tools/sdk-utils/percy-web/handler.ts | 49 + src/tools/sdk-utils/percy-web/index.ts | 5 + src/tools/sdk-utils/percy-web/types.ts | 12 + src/tools/sdk-utils/percy/types.ts | 21 - 44 files changed, 4192 insertions(+), 608 deletions(-) create mode 100644 src/tools/add-percy-snapshots.ts create mode 100644 src/tools/list-test-files.ts create mode 100644 src/tools/percy-sdk.ts create mode 100644 src/tools/percy-snapshot-utils/constants.ts create mode 100644 src/tools/percy-snapshot-utils/detect-test-files.ts create mode 100644 src/tools/percy-snapshot-utils/types.ts create mode 100644 src/tools/percy-snapshot-utils/utils.ts create mode 100644 src/tools/sdk-utils/bstack/commands.ts create mode 100644 src/tools/sdk-utils/bstack/configUtils.ts rename src/tools/sdk-utils/{ => bstack}/constants.ts (89%) create mode 100644 src/tools/sdk-utils/bstack/frameworks.ts create mode 100644 src/tools/sdk-utils/bstack/index.ts create mode 100644 src/tools/sdk-utils/bstack/sdkHandler.ts delete mode 100644 src/tools/sdk-utils/commands.ts create mode 100644 src/tools/sdk-utils/common/constants.ts create mode 100644 src/tools/sdk-utils/common/formatUtils.ts create mode 100644 src/tools/sdk-utils/common/index.ts create mode 100644 src/tools/sdk-utils/common/instructionUtils.ts create mode 100644 src/tools/sdk-utils/common/schema.ts create mode 100644 src/tools/sdk-utils/common/types.ts create mode 100644 src/tools/sdk-utils/common/utils.ts create mode 100644 src/tools/sdk-utils/handler.ts delete mode 100644 src/tools/sdk-utils/instructions.ts create mode 100644 src/tools/sdk-utils/percy-automate/constants.ts create mode 100644 src/tools/sdk-utils/percy-automate/frameworks.ts create mode 100644 src/tools/sdk-utils/percy-automate/handler.ts create mode 100644 src/tools/sdk-utils/percy-automate/index.ts create mode 100644 src/tools/sdk-utils/percy-automate/types.ts rename src/tools/sdk-utils/{percy => percy-bstack}/constants.ts (72%) create mode 100644 src/tools/sdk-utils/percy-bstack/frameworks.ts create mode 100644 src/tools/sdk-utils/percy-bstack/handler.ts create mode 100644 src/tools/sdk-utils/percy-bstack/index.ts rename src/tools/sdk-utils/{percy => percy-bstack}/instructions.ts (60%) create mode 100644 src/tools/sdk-utils/percy-bstack/types.ts create mode 100644 src/tools/sdk-utils/percy-web/constants.ts create mode 100644 src/tools/sdk-utils/percy-web/fetchPercyToken.ts create mode 100644 src/tools/sdk-utils/percy-web/frameworks.ts create mode 100644 src/tools/sdk-utils/percy-web/handler.ts create mode 100644 src/tools/sdk-utils/percy-web/index.ts create mode 100644 src/tools/sdk-utils/percy-web/types.ts delete mode 100644 src/tools/sdk-utils/percy/types.ts diff --git a/src/lib/inmemory-store.ts b/src/lib/inmemory-store.ts index 76515ebc..0c09d201 100644 --- a/src/lib/inmemory-store.ts +++ b/src/lib/inmemory-store.ts @@ -1 +1,2 @@ export const signedUrlMap = new Map(); +export const testFilePathsMap = new Map(); diff --git a/src/server-factory.ts b/src/server-factory.ts index 82f93730..830dc46b 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -7,6 +7,7 @@ const require = createRequire(import.meta.url); const packageJson = require("../package.json"); import logger from "./logger.js"; import addSDKTools from "./tools/bstack-sdk.js"; +import addPercyTools from "./tools/percy-sdk.js"; import addBrowserLiveTools from "./tools/live.js"; import addAccessibilityTools from "./tools/accessibility.js"; import addTestManagementTools from "./tools/testmanagement.js"; @@ -48,6 +49,7 @@ export class BrowserStackMcpServer { const toolAdders = [ addAccessibilityTools, addSDKTools, + addPercyTools, addAppLiveTools, addBrowserLiveTools, addTestManagementTools, diff --git a/src/tools/add-percy-snapshots.ts b/src/tools/add-percy-snapshots.ts new file mode 100644 index 00000000..237ae55c --- /dev/null +++ b/src/tools/add-percy-snapshots.ts @@ -0,0 +1,32 @@ +import { testFilePathsMap } from "../lib/inmemory-store.js"; +import { updateFileAndStep } from "./percy-snapshot-utils/utils.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js"; + +export async function updateTestsWithPercyCommands(args: { + uuid: string; + index: number; +}): Promise { + const { uuid, index } = args; + const filePaths = testFilePathsMap.get(uuid); + + if (!filePaths) { + throw new Error(`No test files found in memory for UUID: ${uuid}`); + } + + if (index < 0 || index >= filePaths.length) { + throw new Error( + `Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`, + ); + } + const result = await updateFileAndStep( + filePaths[index], + index, + filePaths.length, + percyWebSetupInstructions, + ); + + return { + content: result, + }; +} diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index f2e597e5..84a84329 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -1,169 +1,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; -import { trackMCP } from "../lib/instrumentation.js"; -import { getSDKPrefixCommand } from "./sdk-utils/commands.js"; - -import { - SDKSupportedBrowserAutomationFramework, - SDKSupportedLanguage, - SDKSupportedTestingFramework, - SDKSupportedLanguageEnum, - SDKSupportedBrowserAutomationFrameworkEnum, - SDKSupportedTestingFrameworkEnum, -} from "./sdk-utils/types.js"; - -import { - generateBrowserStackYMLInstructions, - getInstructionsForProjectConfiguration, - formatInstructionsWithNumbers, -} from "./sdk-utils/instructions.js"; - -import { - formatPercyInstructions, - getPercyInstructions, -} from "./sdk-utils/percy/instructions.js"; -import { getBrowserStackAuth } from "../lib/get-auth.js"; import { BrowserStackConfig } from "../lib/types.js"; +import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js"; +import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js"; +import { RUN_ON_BROWSERSTACK_DESCRIPTION } from "./sdk-utils/common/constants.js"; -/** - * BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack. - * This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies. - */ -export async function bootstrapProjectWithSDK({ - detectedBrowserAutomationFramework, - detectedTestingFramework, - detectedLanguage, - desiredPlatforms, - enablePercy, - config, -}: { - detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFramework; - detectedTestingFramework: SDKSupportedTestingFramework; - detectedLanguage: SDKSupportedLanguage; - desiredPlatforms: string[]; - enablePercy: boolean; - config: BrowserStackConfig; -}): Promise { - // Get credentials from config - const authString = getBrowserStackAuth(config); - const [username, accessKey] = authString.split(":"); - - // Handle frameworks with unique setup instructions that don't use browserstack.yml - if ( - detectedBrowserAutomationFramework === "cypress" || - detectedTestingFramework === "webdriverio" - ) { - let combinedInstructions = getInstructionsForProjectConfiguration( - detectedBrowserAutomationFramework, - detectedTestingFramework, - detectedLanguage, - username, - accessKey, - ); - - if (enablePercy) { - const percyInstructions = getPercyInstructions( - detectedLanguage, - detectedBrowserAutomationFramework, - detectedTestingFramework, - ); - - if (percyInstructions) { - combinedInstructions += - "\n\n" + formatPercyInstructions(percyInstructions); - } else { - throw new Error( - `Percy is currently not supported through MCP for ${detectedLanguage} with ${detectedTestingFramework}. If you want to run the test cases without Percy, disable Percy and run it again.`, - ); - } - } - - // Apply consistent formatting for all configurations - return formatFinalInstructions(combinedInstructions); - } - - // Handle default flow using browserstack.yml - const sdkSetupCommand = getSDKPrefixCommand( - detectedLanguage, - detectedTestingFramework, - username, - accessKey, - ); - - const ymlInstructions = generateBrowserStackYMLInstructions( - desiredPlatforms, - enablePercy, - ); - - const instructionsForProjectConfiguration = - getInstructionsForProjectConfiguration( - detectedBrowserAutomationFramework, - detectedTestingFramework, - detectedLanguage, - username, - accessKey, - ); - - let combinedInstructions = ""; - - // Step 1: Add SDK setup command - if (sdkSetupCommand) { - combinedInstructions += sdkSetupCommand; - } - - // Step 2: Add browserstack.yml setup - if (ymlInstructions) { - combinedInstructions += "\n\n---STEP---\n" + ymlInstructions; - } - - // Step 3: Add language/framework-specific setup - if (instructionsForProjectConfiguration) { - combinedInstructions += "\n\n" + instructionsForProjectConfiguration; - } - - // Step 4: Add Percy setup if applicable - if (enablePercy) { - const percyInstructions = getPercyInstructions( - detectedLanguage, - detectedBrowserAutomationFramework, - detectedTestingFramework, - ); - - if (percyInstructions) { - combinedInstructions += - "\n\n" + formatPercyInstructions(percyInstructions); - } else { - throw new Error( - `Percy is currently not supported through MCP for ${detectedLanguage} with ${detectedTestingFramework}. If you want to run the test cases without Percy, disable Percy and run it again.`, - ); - } - } - - // Apply consistent formatting for all configurations - return formatFinalInstructions(combinedInstructions); -} - -// Helper function to apply consistent formatting to all instruction types -function formatFinalInstructions(combinedInstructions: 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 SDK setup. - - ${formatInstructionsWithNumbers(combinedInstructions)}`; - - return { - content: [ - { - type: "text", - text: fullInstructions, - isError: false, - }, - ], - }; -} - -export default function addSDKTools( +export function registerRunBrowserStackTestsTool( server: McpServer, config: BrowserStackConfig, ) { @@ -171,84 +12,14 @@ export default function addSDKTools( tools.setupBrowserStackAutomateTests = server.tool( "setupBrowserStackAutomateTests", - "Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use for functional or integration tests on BrowserStack, with optional Percy visual testing for supported frameworks. Example prompts: run this test on browserstack; run this test on browserstack with Percy; set up this project for browserstack with Percy. Integrate BrowserStack SDK into your project", - { - detectedBrowserAutomationFramework: z - .nativeEnum(SDKSupportedBrowserAutomationFrameworkEnum) - .describe( - "The automation framework configured in the project. Example: 'playwright', 'selenium'", - ), - - detectedTestingFramework: z - .nativeEnum(SDKSupportedTestingFrameworkEnum) - .describe( - "The testing framework used in the project. Be precise with framework selection Example: 'webdriverio', 'jest', 'pytest', 'junit4', 'junit5', 'mocha'", - ), - - detectedLanguage: z - .nativeEnum(SDKSupportedLanguageEnum) - .describe( - "The programming language used in the project. Example: 'nodejs', 'python', 'java', 'csharp'", - ), - - desiredPlatforms: z - .array(z.enum(["windows", "macos", "android", "ios"])) - .describe( - "The platforms the user wants to test on. Always ask this to the user, do not try to infer this.", - ), - - enablePercy: z - .boolean() - .optional() - .default(false) - .describe( - "Set to true if the user wants to enable Percy for visual testing. Defaults to false.", - ), - }, - + RUN_ON_BROWSERSTACK_DESCRIPTION, + RunTestsOnBrowserStackParamsShape, async (args) => { - try { - trackMCP( - "runTestsOnBrowserStack", - server.server.getClientVersion()!, - undefined, - config, - ); - - return await bootstrapProjectWithSDK({ - detectedBrowserAutomationFramework: - args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, - - detectedTestingFramework: - args.detectedTestingFramework as SDKSupportedTestingFramework, - - detectedLanguage: args.detectedLanguage as SDKSupportedLanguage, - - desiredPlatforms: args.desiredPlatforms, - enablePercy: args.enablePercy, - config, - }); - } catch (error) { - trackMCP( - "runTestsOnBrowserStack", - server.server.getClientVersion()!, - error, - config, - ); - - return { - content: [ - { - type: "text", - text: `Failed to bootstrap project with BrowserStack SDK. Error: ${error}. Please open an issue on GitHub if the problem persists`, - isError: true, - }, - ], - isError: true, - }; - } + return runTestsOnBrowserStackHandler(args, config); }, ); return tools; } + +export default registerRunBrowserStackTestsTool; diff --git a/src/tools/list-test-files.ts b/src/tools/list-test-files.ts new file mode 100644 index 00000000..7fb1d897 --- /dev/null +++ b/src/tools/list-test-files.ts @@ -0,0 +1,39 @@ +import { listTestFiles } from "./percy-snapshot-utils/detect-test-files.js"; +import { testFilePathsMap } from "../lib/inmemory-store.js"; +import crypto from "crypto"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +export async function addListTestFiles(args: any): Promise { + const { dirs, language, framework } = args; + let testFiles: string[] = []; + + for (const dir of dirs) { + const files = await listTestFiles({ + language, + framework, + baseDir: dir, + }); + testFiles = testFiles.concat(files); + } + + if (testFiles.length === 0) { + throw new Error("No test files found"); + } + + // Generate a UUID and store the test files in memory + const uuid = crypto.randomUUID(); + testFilePathsMap.set(uuid, testFiles); + + return { + content: [ + { + type: "text", + text: `The Test files are stored in memory with id ${uuid} and the total number of tests files found is ${testFiles.length}. You can use this UUID to retrieve the tests file paths later.`, + }, + { + type: "text", + text: `You can now use the tool addPercySnapshotCommands to update the test file with Percy commands for visual testing with the UUID ${uuid}`, + }, + ], + }; +} diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts new file mode 100644 index 00000000..7e2e4631 --- /dev/null +++ b/src/tools/percy-sdk.ts @@ -0,0 +1,159 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { BrowserStackConfig } from "../lib/types.js"; +import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js"; +import { updateTestsWithPercyCommands } from "./add-percy-snapshots.js"; +import { addListTestFiles } from "./list-test-files.js"; +import { trackMCP } from "../index.js"; +import { + setUpPercyHandler, + setUpSimulatePercyChangeHandler, +} from "./sdk-utils/handler.js"; +import { + SETUP_PERCY_DESCRIPTION, + SIMULATE_PERCY_CHANGE_DESCRIPTION, + LIST_TEST_FILES_DESCRIPTION, + PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, +} from "./sdk-utils/common/constants.js"; +import { + ListTestFilesParamsShape, + UpdateTestFileWithInstructionsParams, +} from "./percy-snapshot-utils/constants.js"; + +export function registerPercyTools( + server: McpServer, + config: BrowserStackConfig, +) { + const tools: Record = {}; + + // Register setupPercyVisualTesting + tools.setupPercyVisualTesting = server.tool( + "setupPercyVisualTesting", + SETUP_PERCY_DESCRIPTION, + SetUpPercyParamsShape, + async (args) => { + try { + trackMCP( + "setupPercyVisualTesting", + server.server.getClientVersion()!, + config, + ); + return setUpPercyHandler(args, config); + } catch (error) { + trackMCP( + "setupPercyVisualTesting", + server.server.getClientVersion()!, + error, + config, + ); + return { + content: [ + { + type: "text", + text: error instanceof Error ? error.message : String(error), + }, + ], + isError: true, + }; + } + }, + ); + + // Register simulatePercyChange + tools.simulatePercyChange = server.tool( + "simulatePercyChange", + SIMULATE_PERCY_CHANGE_DESCRIPTION, + SetUpPercyParamsShape, + async (args) => { + try { + trackMCP( + "simulatePercyChange", + server.server.getClientVersion()!, + config, + ); + return setUpSimulatePercyChangeHandler(args, config); + } catch (error) { + trackMCP( + "simulatePercyChange", + server.server.getClientVersion()!, + error, + config, + ); + return { + content: [ + { + type: "text", + text: error instanceof Error ? error.message : String(error), + }, + ], + isError: true, + }; + } + }, + ); + + // Register addPercySnapshotCommands + tools.addPercySnapshotCommands = server.tool( + "addPercySnapshotCommands", + PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, + UpdateTestFileWithInstructionsParams, + async (args) => { + try { + trackMCP( + "addPercySnapshotCommands", + server.server.getClientVersion()!, + config, + ); + return await updateTestsWithPercyCommands(args); + } catch (error) { + trackMCP( + "addPercySnapshotCommands", + server.server.getClientVersion()!, + error, + config, + ); + return { + content: [ + { + type: "text", + text: error instanceof Error ? error.message : String(error), + }, + ], + isError: true, + }; + } + }, + ); + + // Register listTestFiles + tools.listTestFiles = server.tool( + "listTestFiles", + LIST_TEST_FILES_DESCRIPTION, + ListTestFilesParamsShape, + async (args) => { + try { + trackMCP("listTestFiles", server.server.getClientVersion()!, config); + return addListTestFiles(args); + } catch (error) { + trackMCP( + "listTestFiles", + server.server.getClientVersion()!, + error, + config, + ); + return { + content: [ + { + type: "text", + text: error instanceof Error ? error.message : String(error), + }, + ], + isError: true, + }; + } + }, + ); + + return tools; +} + +export default registerPercyTools; diff --git a/src/tools/percy-snapshot-utils/constants.ts b/src/tools/percy-snapshot-utils/constants.ts new file mode 100644 index 00000000..ef77a419 --- /dev/null +++ b/src/tools/percy-snapshot-utils/constants.ts @@ -0,0 +1,514 @@ +import { z } from "zod"; +import { + SDKSupportedLanguages, + SDKSupportedTestingFrameworks, +} from "../sdk-utils/common/types.js"; +import { SDKSupportedLanguage } from "../sdk-utils/common/types.js"; +import { DetectionConfig } from "./types.js"; + +export const UpdateTestFileWithInstructionsParams = { + uuid: z + .string() + .describe("UUID referencing the in-memory array of test file paths"), + index: z.number().describe("Index of the test file to update"), +}; + +export const ListTestFilesParamsShape = { + dirs: z + .array(z.string()) + .describe("Array of directory paths to search for test files"), + language: z + .enum(SDKSupportedLanguages as [string, ...string[]]) + .describe("Programming language"), + framework: z + .enum(SDKSupportedTestingFrameworks as [string, ...string[]]) + .describe("Testing framework (optional)"), +}; + +export const TEST_FILE_DETECTION: Record< + SDKSupportedLanguage, + DetectionConfig +> = { + java: { + extensions: [".java"], + namePatterns: [ + /Test\.java$/, + /Tests\.java$/, + /Steps\.java$/, + /.*UI.*Test\.java$/, + /.*Web.*Test\.java$/, + /.*E2E.*Test\.java$/, + /.*Integration.*Test\.java$/, + /.*Functional.*Test\.java$/, + ], + contentRegex: [ + /@Test\b/, + /@RunWith\b/, + /@CucumberOptions\b/, + /import\s+org\.junit/, + /import\s+org\.testng/, + /import\s+io\.cucumber/, + /import\s+org\.jbehave/, + ], + uiDriverRegex: [ + /import\s+org\.openqa\.selenium/, + /import\s+org\.seleniumhq\.selenium/, + /import\s+io\.appium\.java_client/, + /import\s+com\.microsoft\.playwright/, + /import\s+com\.codeborne\.selenide/, + /import\s+net\.serenitybdd/, + /import\s+cucumber\.api\.java\.en/, + /new\s+\w*Driver\s*\(/, + /\.findElement\s*\(/, + /\.get\s*\(['"]https?:/, + /\.click\s*\(/, + /\.navigate\(\)/, + /WebDriver/, + /RemoteWebDriver/, + /ChromeDriver/, + /FirefoxDriver/, + ], + uiIndicatorRegex: [ + // UI interactions without explicit driver imports + /\.sendKeys\s*\(/, + /\.getText\s*\(/, + /\.isDisplayed\s*\(/, + /By\.id\s*\(/, + /By\.className\s*\(/, + /By\.xpath\s*\(/, + /waitForElement/, + /waitForVisible/, + /assertTitle/, + /screenshot/, + /captureScreenshot/, + // Page Object patterns + /PageObject/, + /BasePage/, + /WebPage/, + // UI test annotations and patterns + /@UITest/, + /@WebTest/, + /@E2ETest/, + // Common UI assertions + /assertUrl/, + /verifyText/, + /checkElement/, + // Browser/window operations + /maximizeWindow/, + /setWindowSize/, + /switchTo/, + // Cucumber UI steps + /Given.*I\s+(open|visit|navigate)/, + /When.*I\s+(click|type|select)/, + /Then.*I\s+(see|verify|check)/, + /And.*I\s+(wait|scroll)/, + ], + backendRegex: [ + /import\s+org\.springframework\.test/, + /import\s+javax\.persistence/, + /@DataJpaTest/, + /@WebMvcTest/, + /@MockBean/, + /EntityManager/, + /JdbcTemplate/, + /TestRestTemplate/, + /@Repository/, + /@Service/, + /@Entity/, + ], + excludeRegex: [ + /UnitTest/, + /MockTest/, + /StubTest/, + /DatabaseTest/, + /import\s+org\.mockito/, + /@Mock\b/, + /@Spy\b/, + ], + }, + csharp: { + extensions: [".cs"], + namePatterns: [ + /Test\.cs$/, + /Tests\.cs$/, + /Steps\.cs$/, + /.*UI.*Test\.cs$/, + /.*Web.*Test\.cs$/, + /.*E2E.*Test\.cs$/, + ], + contentRegex: [ + /\[Test\]/, + /\[TestCase\]/, + /\[Fact\]/, + /\[Theory\]/, + /\[Binding\]/, + /using\s+NUnit\.Framework/, + /using\s+Xunit/, + /using\s+TechTalk\.SpecFlow/, + ], + uiDriverRegex: [ + /using\s+OpenQA\.Selenium/, + /using\s+Appium/, + /using\s+Microsoft\.Playwright/, + /using\s+Selenide/, + /using\s+Atata/, + /new\s+\w*Driver\s*\(/, + /\.FindElement\s*\(/, + /\.Navigate\(\)/, + /IWebDriver/, + /WebDriver/, + ], + uiIndicatorRegex: [ + /\.SendKeys\s*\(/, + /\.Click\s*\(/, + /\.Text/, + /\.Displayed/, + /By\.Id/, + /By\.ClassName/, + /By\.XPath/, + /WaitForElement/, + /TakeScreenshot/, + /PageObject/, + /\[UITest\]/, + /\[WebTest\]/, + /\[E2ETest\]/, + /NavigateTo/, + /VerifyText/, + /AssertUrl/, + ], + backendRegex: [ + /using\s+Microsoft\.EntityFrameworkCore/, + /using\s+System\.Data/, + /DbContext/, + /Repository/, + /Controller/, + /\[ApiTest\]/, + /\[DatabaseTest\]/, + ], + excludeRegex: [/\[UnitTest\]/, /Mock/, /Stub/, /using\s+Moq/], + }, + nodejs: { + extensions: [".js", ".ts"], + namePatterns: [ + /.test.js$/, + /.spec.js$/, + /.test.ts$/, + /.spec.ts$/, + /.*ui.*.test.(js|ts)$/, + /.*web.*.test.(js|ts)$/, + /.*e2e.*.(js|ts)$/, + /.*integration.*.test.(js|ts)$/, + ], + contentRegex: [ + /\bdescribe\s*\(/, + /\bit\s*\(/, + /\btest\s*\(/, + /require\(['"]mocha['"]\)/, + /require\(['"]jest['"]\)/, + /import.*from\s+['"]jest['"]/, + /from\s+['"]@jest/, + ], + uiDriverRegex: [ + /require\(['"]selenium-webdriver['"]\)/, + /require\(['"]webdriverio['"]\)/, + /require\(['"]puppeteer['"]\)/, + /require\(['"]playwright['"]\)/, + /require\(['"]cypress['"]\)/, + /require\(['"]@wdio\/sync['"]\)/, + /import.*from\s+['"]selenium-webdriver['"]/, + /import.*from\s+['"]webdriverio['"]/, + /import.*from\s+['"]puppeteer['"]/, + /import.*from\s+['"]playwright['"]/, + /import.*from\s+['"]cypress['"]/, + /import.*from\s+['"]@wdio/, + /\.launch\(/, + /\.goto\(/, + /driver\./, + /browser\./, + ], + uiIndicatorRegex: [ + // Browser automation - SPECIFIC CONTEXT + /driver\.click\(/, + /driver\.type\(/, + /driver\.fill\(/, + /browser\.click\(/, + /driver\.waitForSelector\(/, + /browser\.waitForElement\(/, + /driver\.screenshot\(/, + /browser\.screenshot\(/, + /driver\.evaluate\(/, + /driver\.focus\(/, + /driver\.hover\(/, + // Page object patterns - UI specific + /page\.goto/, + /page\.click/, + /page\.fill/, + /page\.screenshot/, + /page\.waitForSelector/, + /page\.locator/, + /page\.getByRole/, + // Cypress specific patterns + /cy\.visit/, + /cy\.get/, + /cy\.click/, + /cy\.type/, + /cy\.should/, + /cy\.wait/, + /cy\.screenshot/, + /cy\.viewport/, + // WebDriverIO specific patterns + /browser\.url/, + /browser\.click/, + /browser\.setValue/, + /\$\(['"][#.]/, + /\$\$\(['"][#.]/, // CSS/XPath selectors + // Playwright specific + /expect.*toBeVisible/, + /expect.*toHaveText/, + /expect.*toBeEnabled/, + /locator\(/, + /getByText\(/, + /getByRole\(/, + /getByTestId\(/, + // DOM queries in test context + /findElement/, + /querySelector.*\)\.click/, + /getElementById.*\)\.click/, + // Test descriptions clearly indicating UI + /describe.*['"`].*UI/, + /describe.*['"`].*Web/, + /describe.*['"`].*E2E/, + /describe.*['"`].*Browser/, + /describe.*['"`].*Selenium/, + /it.*['"`].*(click|type|navigate|visit|see).*element/, + /it.*['"`].*(open|load).*page/, + /it.*['"`].*browser/, + ], + backendRegex: [ + /require\(['"]express['"]\)/, + /require\(['"]fastify['"]\)/, + /require\(['"]supertest['"]\)/, + /request\(app\)/, + /mongoose/, + /sequelize/, + /prisma/, + /knex/, + /app\.get\(/, + /app\.post\(/, + /server\./, + /\.connect\(/, + /\.query\(/, + ], + excludeRegex: [ + /\.unit\./, + /\.mock\./, + /jest\.mock/, + /sinon/, + /describe.*['"`]Unit/, + /describe.*['"`]Mock/, + ], + }, + python: { + extensions: [".py"], + namePatterns: [ + /^test_.*\.py$/, + /_test\.py$/, + /test.*ui.*\.py$/, + /test.*web.*\.py$/, + /test.*e2e.*\.py$/, + /test.*integration.*\.py$/, + ], + contentRegex: [ + /import\s+pytest/, + /@pytest\.mark/, + /def\s+test_/, + /\bpytest\./, + /import\s+unittest/, + /class.*TestCase/, + ], + uiDriverRegex: [ + /import\s+selenium/, + /from\s+selenium/, + /import\s+playwright/, + /from\s+playwright/, + /import\s+appium/, + /from\s+appium/, + /import\s+splinter/, + /from\s+splinter/, + /driver\s*=\s*webdriver\./, + /webdriver\.Chrome/, + /webdriver\.Firefox/, + ], + uiIndicatorRegex: [ + // Selenium patterns without imports + /\.find_element/, + /\.click\(/, + /\.send_keys/, + /\.get\(/, + /\.screenshot/, + /\.execute_script/, + /\.switch_to/, + /By\.ID/, + /By\.CLASS_NAME/, + /By\.XPATH/, + // Playwright patterns + /page\.goto/, + /page\.click/, + /page\.fill/, + /page\.screenshot/, + /expect.*to_be_visible/, + /expect.*to_have_text/, + // Generic UI patterns + /WebDriverWait/, + /expected_conditions/, + /ActionChains/, + /@pytest\.mark\.ui/, + /@pytest\.mark\.web/, + /@pytest\.mark\.e2e/, + // Page object patterns + /BasePage/, + /PageObject/, + /WebPage/, + // BDD step patterns + /def\s+.*_(open|visit|navigate|click|type|see|verify)/, + ], + backendRegex: [ + /import\s+flask/, + /from\s+flask/, + /import\s+fastapi/, + /from\s+fastapi/, + /import\s+django/, + /from\s+django/, + /sqlalchemy/, + /requests\.get/, + /requests\.post/, + /TestClient/, + /@pytest\.mark\.django_db/, + /django\.test/, + ], + excludeRegex: [ + /unittest\.mock/, + /from\s+unittest\.mock/, + /mock\.patch/, + /@pytest\.mark\.unit/, + /@mock\./, + ], + }, + ruby: { + extensions: [".rb"], + namePatterns: [ + /_spec\.rb$/, + /_test\.rb$/, + /.*ui.*_spec\.rb$/, + /.*web.*_spec\.rb$/, + /.*e2e.*_spec\.rb$/, + ], + contentRegex: [ + /\bdescribe\s/, + /\bit\s/, + /require\s+['"]rspec/, + /require\s+['"]minitest/, + /RSpec\.describe/, + ], + uiDriverRegex: [ + /require\s+['"]selenium-webdriver['"]/, + /require\s+['"]capybara['"]/, + /require\s+['"]appium_lib['"]/, + /require\s+['"]watir['"]/, + /Selenium::WebDriver/, + /Capybara\./, + ], + uiIndicatorRegex: [ + // Capybara without explicit require + /visit\s/, + /click_button/, + /click_link/, + /fill_in/, + /find\(['"]/, + /has_content/, + /page\./, + /current_path/, + // Selenium patterns + /\.find_element/, + /\.click/, + /\.send_keys/, + // Generic UI patterns + /screenshot/, + /driver\./, + /browser\./, + /feature\s+['"]/, + /scenario\s+['"]/, + /expect.*to\s+have_content/, + /expect.*to\s+have_selector/, + ], + backendRegex: [ + /require\s+['"]sinatra['"]/, + /require\s+['"]rails['"]/, + /ActiveRecord/, + /DatabaseCleaner/, + /FactoryBot/, + ], + excludeRegex: [ + /double\(/, + /instance_double/, + /class_double/, + /allow\(.*\)\.to\s+receive/, + /mock/i, + ], + }, +}; + +export const EXCLUDED_DIRS = new Set([ + "node_modules", + ".venv", + "venv", + "__pycache__", + "site-packages", + "dist", + "build", + ".git", + ".mypy_cache", + ".pytest_cache", + ".tox", + ".idea", + ".vscode", + "coverage", + ".nyc_output", + "target", + "bin", + "obj", + "packages", + ".nuget", +]); + +export const backendIndicators = [ + /import\s+requests/, + /requests\.(get|post|put|delete|patch)/, + /@pytest\.mark\.(api|backend|integration)/, + /BASE_URL\s*=/, + /\.status_code/, + /\.json\(\)/, + /TestClient/, + /Bearer\s+/, + /Authorization.*Bearer/, +]; + +export const strongUIIndicators = [ + // Browser automation with specific context + /(driver|browser|page)\.(click|type|fill|screenshot|wait)/, + /webdriver\.(Chrome|Firefox|Safari|Edge)/, + /(selenium|playwright|puppeteer|cypress).*import/, + // CSS/XPath selectors + /By\.(ID|CLASS_NAME|XPATH|CSS_SELECTOR)/, + /\$\(['"#[.][^'"]*['"]\)/, // $(".class") or $("#id") + // Page Object Model + /class.*Page.*:/, + /class.*PageObject/, + // UI test markers + /@(ui|web|e2e|browser)_?test/, + /@pytest\.mark\.(ui|web|e2e|browser)/, + // Browser navigation + /\.goto\s*\(['"]https?:/, + /\.visit\s*\(['"]https?:/, + /\.navigate\(\)\.to\(/, +]; diff --git a/src/tools/percy-snapshot-utils/detect-test-files.ts b/src/tools/percy-snapshot-utils/detect-test-files.ts new file mode 100644 index 00000000..bccdf715 --- /dev/null +++ b/src/tools/percy-snapshot-utils/detect-test-files.ts @@ -0,0 +1,265 @@ +import fs from "fs"; +import path from "path"; +import logger from "../../logger.js"; + +import { + SDKSupportedLanguage, + SDKSupportedTestingFrameworkEnum, +} from "../sdk-utils/common/types.js"; + +import { + EXCLUDED_DIRS, + TEST_FILE_DETECTION, + backendIndicators, + strongUIIndicators, +} from "../percy-snapshot-utils/constants.js"; + +import { DetectionConfig } from "../percy-snapshot-utils/types.js"; + +async function walkDir(dir: string, extensions: string[]): Promise { + const result: string[] = []; + try { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith(".")) { + result.push(...(await walkDir(fullPath, extensions))); + } + } else if (extensions.some((ext) => entry.name.endsWith(ext))) { + result.push(fullPath); + } + } + } catch { + logger.error(`Failed to read directory: ${dir}`); + } + + return result; +} + +async function fileContainsRegex( + filePath: string, + regexes: RegExp[], +): Promise { + if (!regexes.length) return false; + + try { + const content = await fs.promises.readFile(filePath, "utf8"); + return regexes.some((re) => re.test(content)); + } catch { + logger.warn(`Failed to read file: ${filePath}`); + return false; + } +} + +async function batchRegexCheck( + filePath: string, + regexGroups: RegExp[][], +): Promise { + try { + const content = await fs.promises.readFile(filePath, "utf8"); + return regexGroups.map((regexes) => + regexes.length > 0 ? regexes.some((re) => re.test(content)) : false, + ); + } catch { + logger.warn(`Failed to read file: ${filePath}`); + return regexGroups.map(() => false); + } +} + +async function isLikelyUITest(filePath: string): Promise { + try { + const content = await fs.promises.readFile(filePath, "utf8"); + if (backendIndicators.some((pattern) => pattern.test(content))) { + return false; + } + return strongUIIndicators.some((pattern) => pattern.test(content)); + } catch { + return false; + } +} + +function getFileScore(fileName: string, config: DetectionConfig): number { + let score = 0; + + // Higher score for explicit UI test naming + if (/ui|web|e2e|integration|functional/i.test(fileName)) score += 3; + if (config.namePatterns.some((pattern) => pattern.test(fileName))) score += 2; + + return score; +} + +export interface ListTestFilesOptions { + language: SDKSupportedLanguage; + framework?: SDKSupportedTestingFrameworkEnum; + baseDir: string; + strictMode?: boolean; +} + +export async function listTestFiles( + options: ListTestFilesOptions, +): Promise { + const { language, framework, baseDir, strictMode = false } = options; + const config = TEST_FILE_DETECTION[language]; + + if (!config) { + logger.error(`Unsupported language: ${language}`); + return []; + } + + // Step 1: Collect all files with matching extensions + let files: string[] = []; + try { + files = await walkDir(baseDir, config.extensions); + } catch { + return []; + } + + if (files.length === 0) { + throw new Error("No files found with the specified extensions"); + } + + const candidateFiles: Map = new Map(); + + // Step 2: Fast name-based identification with scoring + for (const file of files) { + const fileName = path.basename(file); + const score = getFileScore(fileName, config); + + if (config.namePatterns.some((pattern) => pattern.test(fileName))) { + candidateFiles.set(file, score); + logger.debug(`File matched by name pattern: ${file} (score: ${score})`); + } + } + + // Step 3: Content-based test detection for remaining files + const remainingFiles = files.filter((file) => !candidateFiles.has(file)); + const contentCheckPromises = remainingFiles.map(async (file) => { + const hasTestContent = await fileContainsRegex(file, config.contentRegex); + if (hasTestContent) { + const fileName = path.basename(file); + const score = getFileScore(fileName, config); + candidateFiles.set(file, score); + logger.debug(`File matched by content regex: ${file} (score: ${score})`); + } + }); + + await Promise.all(contentCheckPromises); + + // Step 4: Handle SpecFlow .feature files for C# + SpecFlow + if (language === "csharp" && framework === "specflow") { + try { + const featureFiles = await walkDir(baseDir, [".feature"]); + featureFiles.forEach((file) => candidateFiles.set(file, 2)); + logger.info(`Added ${featureFiles.length} SpecFlow .feature files`); + } catch { + logger.warn( + `Failed to collect SpecFlow .feature files from baseDir: ${baseDir}`, + ); + } + } + + if (candidateFiles.size === 0) { + logger.info("No test files found matching patterns"); + return []; + } + + // Step 6: UI Detection with fallback patterns + const uiFiles: string[] = []; + const filesToCheck = Array.from(candidateFiles.keys()); + + // Batch process UI detection for better performance + const batchSize = 10; + for (let i = 0; i < filesToCheck.length; i += batchSize) { + const batch = filesToCheck.slice(i, i + batchSize); + + const batchPromises = batch.map(async (file) => { + // First, use the new precise UI detection + const isUITest = await isLikelyUITest(file); + + if (isUITest) { + logger.debug(`File included - strong UI indicators: ${file}`); + return file; + } + + // If not clearly UI, run the traditional checks + const [hasExplicitUI, hasUIIndicators, hasBackend, shouldExclude] = + await batchRegexCheck(file, [ + config.uiDriverRegex, + config.uiIndicatorRegex, + config.backendRegex, + config.excludeRegex || [], + ]); + + // Skip if explicitly excluded (mocks, unit tests, etc.) + if (shouldExclude) { + logger.debug(`File excluded by exclude regex: ${file}`); + return null; + } + + // Skip backend tests in any mode + if (hasBackend) { + logger.debug(`File excluded as backend test: ${file}`); + return null; + } + + // Include if has explicit UI drivers + if (hasExplicitUI) { + logger.debug(`File included - explicit UI drivers: ${file}`); + return file; + } + + // Include if has UI indicators (for cases where drivers aren't explicitly imported) + if (hasUIIndicators) { + logger.debug(`File included - UI indicators: ${file}`); + return file; + } + + // In non-strict mode, include high-scoring test files even without explicit UI patterns + if (!strictMode) { + const score = candidateFiles.get(file) || 0; + if (score >= 3) { + // High confidence UI test based on naming + logger.debug( + `File included - high confidence score: ${file} (score: ${score})`, + ); + return file; + } + } + + logger.debug(`File excluded - no UI patterns detected: ${file}`); + return null; + }); + + const batchResults = await Promise.all(batchPromises); + uiFiles.push( + ...batchResults.filter((file): file is string => file !== null), + ); + } + + // Step 7: Sort by score (higher confidence files first) + uiFiles.sort((a, b) => { + const scoreA = candidateFiles.get(a) || 0; + const scoreB = candidateFiles.get(b) || 0; + return scoreB - scoreA; + }); + + logger.info( + `Returning ${uiFiles.length} UI test files from ${candidateFiles.size} total test files`, + ); + return uiFiles; +} + +export async function listUITestFilesStrict( + options: Omit, +): Promise { + return listTestFiles({ ...options, strictMode: true }); +} + +export async function listUITestFilesRelaxed( + options: Omit, +): Promise { + return listTestFiles({ ...options, strictMode: false }); +} diff --git a/src/tools/percy-snapshot-utils/types.ts b/src/tools/percy-snapshot-utils/types.ts new file mode 100644 index 00000000..03b8b513 --- /dev/null +++ b/src/tools/percy-snapshot-utils/types.ts @@ -0,0 +1,20 @@ +import { + SDKSupportedTestingFrameworkEnum, + SDKSupportedLanguageEnum, +} from "../sdk-utils/common/types.js"; + +export type ListTestFilesParams = { + dirs: string[]; + language: SDKSupportedLanguageEnum; + framework?: SDKSupportedTestingFrameworkEnum; +}; + +export interface DetectionConfig { + extensions: string[]; + namePatterns: RegExp[]; + contentRegex: RegExp[]; + uiDriverRegex: RegExp[]; + uiIndicatorRegex: RegExp[]; + backendRegex: RegExp[]; + excludeRegex?: RegExp[]; +} diff --git a/src/tools/percy-snapshot-utils/utils.ts b/src/tools/percy-snapshot-utils/utils.ts new file mode 100644 index 00000000..396c64e6 --- /dev/null +++ b/src/tools/percy-snapshot-utils/utils.ts @@ -0,0 +1,42 @@ +const content: { type: "text"; text: string }[] = []; + +export async function updateFileAndStep( + file: string, + idx: number, + total: number, + instruction: string, +) { + content.length = 0; + const nextIndex = idx + 1; + + content.push({ + type: "text", + text: `Complete all steps in order. If a tool call is requested, update the file first, then call the tool. Follow instructions exactly— do not skip any steps to ensure all files are updated.`, + }); + + content.push({ + type: "text", + text: `Step 1 : You need to add percy snapshot commands in some key test cases in the file ${file} use the following instructions: \n${instruction}`, + }); + + content.push({ + type: "text", + text: `Step 2 : Confirm that Percy snapshot commands have been added at all key points of visual change in the file ${file}.`, + }); + + if (nextIndex < total) { + content.push({ + type: "text", + text: `Step 3 : Call the tool updateTestFileWithInstructions with index as ${nextIndex} out of ${total}`, + }); + } + + if (nextIndex === total) { + content.push({ + type: "text", + text: `Step 4: Percy snapshot commands have been added to all files. You can now run the Percy build using the above command.`, + }); + } + + return content; +} diff --git a/src/tools/sdk-utils/bstack/commands.ts b/src/tools/sdk-utils/bstack/commands.ts new file mode 100644 index 00000000..1f600d72 --- /dev/null +++ b/src/tools/sdk-utils/bstack/commands.ts @@ -0,0 +1,123 @@ +import { SDKSupportedLanguage } from "../common/types.js"; + +// Constants +const MAVEN_ARCHETYPE_GROUP_ID = "com.browserstack"; +const MAVEN_ARCHETYPE_ARTIFACT_ID = "browserstack-sdk-archetype-integrate"; +const MAVEN_ARCHETYPE_VERSION = "1.0"; + +// Mapping of test frameworks to their corresponding Maven archetype framework names +const JAVA_FRAMEWORK_MAP: Record = { + testng: "testng", + junit5: "junit5", + junit4: "junit4", + cucumber: "cucumber-testng", +} as const; + +// Template for Node.js SDK setup instructions +const NODEJS_SDK_INSTRUCTIONS = ( + username: string, + accessKey: string, +): string => `---STEP--- +Install BrowserStack Node SDK using command: +\`\`\`bash +npm i -D browserstack-node-sdk@latest +\`\`\` +---STEP--- +Run the following command to setup browserstack sdk: +\`\`\`bash +npx setup --username ${username} --key ${accessKey} +\`\`\``; + +// Template for Gradle setup instructions (platform-independent) +const GRADLE_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}" +`; + +// Generates Maven archetype command for Windows platform +function getMavenCommandForWindows( + framework: string, + mavenFramework: string, +): string { + return ( + `mvn archetype:generate -B ` + + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + + `-DarchetypeArtifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + + `-DarchetypeVersion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + + `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + + `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DBROWSERSTACK_USERNAME="${process.env.BROWSERSTACK_USERNAME}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" ` + + `-DBROWSERSTACK_FRAMEWORK="${mavenFramework}"` + ); +} + +// Generates Maven archetype command for Unix-like platforms (macOS/Linux) +function getMavenCommandForUnix( + username: string, + accessKey: string, + mavenFramework: string, +): string { + return `mvn archetype:generate -B -DarchetypeGroupId=${MAVEN_ARCHETYPE_GROUP_ID} \\ +-DarchetypeArtifactId=${MAVEN_ARCHETYPE_ARTIFACT_ID} -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="${mavenFramework}"`; +} + +// Generates Java SDK setup instructions with Maven/Gradle options +function getJavaSDKInstructions( + framework: string, + username: string, + accessKey: string, +): string { + const mavenFramework = getJavaFrameworkForMaven(framework); + const isWindows = process.platform === "win32"; + const platformLabel = isWindows ? "Windows" : "macOS/Linux"; + + const mavenCommand = isWindows + ? getMavenCommandForWindows(framework, mavenFramework) + : getMavenCommandForUnix(username, accessKey, mavenFramework); + + return `---STEP--- +Install BrowserStack Java SDK + +**Maven command for ${framework} (${platformLabel}):** +Run the command, it is required to generate the browserstack-sdk-archetype-integrate project: +${mavenCommand} + +Alternative setup for Gradle users: +${GRADLE_SETUP_INSTRUCTIONS}`; +} + +// Main function to get SDK setup commands based on language and framework +export function getSDKPrefixCommand( + language: SDKSupportedLanguage, + framework: string, + username: string, + accessKey: string, +): string { + switch (language) { + case "nodejs": + return NODEJS_SDK_INSTRUCTIONS(username, accessKey); + + case "java": + return getJavaSDKInstructions(framework, username, accessKey); + + default: + return ""; + } +} + +export function getJavaFrameworkForMaven(framework: string): string { + return JAVA_FRAMEWORK_MAP[framework] || framework; +} diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts new file mode 100644 index 00000000..587abd33 --- /dev/null +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -0,0 +1,72 @@ +/** + * Utilities for generating BrowserStack configuration files. + */ + +export function generateBrowserStackYMLInstructions( + desiredPlatforms: string[], + enablePercy: boolean = false, + projectName: string, +) { + let ymlContent = ` +# ====================== +# BrowserStack Reporting +# ====================== +# A single name for your project to organize all your tests. This is required for Percy. +projectName: ${projectName} +# TODO: Replace these sample values with your actual project details +buildName: Sample-Build + +# ======================================= +# Platforms (Browsers / Devices to test) +# ======================================= +# Platforms object contains all the browser / device combinations you want to test on. +# Generate this on the basis of the following platforms requested by the user: +# Requested platforms: ${desiredPlatforms} +platforms: + - os: Windows + osVersion: 11 + browserName: chrome + browserVersion: latest + +# ======================= +# Parallels per Platform +# ======================= +# The number of parallel threads to be used for each platform set. +# BrowserStack's SDK runner will select the best strategy based on the configured value +# +# Example 1 - If you have configured 3 platforms and set \`parallelsPerPlatform\` as 2, a total of 6 (2 * 3) parallel threads will be used on BrowserStack +# +# Example 2 - If you have configured 1 platform and set \`parallelsPerPlatform\` as 5, a total of 5 (1 * 5) parallel threads will be used on BrowserStack +parallelsPerPlatform: 1 + +# ================= +# Local Testing +# ================= +# Set to true to test local +browserstackLocal: true + +# =================== +# Debugging features +# =================== +debug: true # Visual logs, text logs, etc. +testObservability: true # For Test Observability`; + + if (enablePercy) { + ymlContent += ` + +# ===================== +# Percy Visual Testing +# ===================== +# Set percy to true to enable visual testing. +# Set percyCaptureMode to 'manual' to control when screenshots are taken. +percy: true +percyCaptureMode: manual`; + } + return ` +---STEP--- +Create a browserstack.yml file in the project root. The file should be in the following format: + +\`\`\`yaml${ymlContent} +\`\`\` +\n`; +} diff --git a/src/tools/sdk-utils/constants.ts b/src/tools/sdk-utils/bstack/constants.ts similarity index 89% rename from src/tools/sdk-utils/constants.ts rename to src/tools/sdk-utils/bstack/constants.ts index 6bd0c075..161be95e 100644 --- a/src/tools/sdk-utils/constants.ts +++ b/src/tools/sdk-utils/bstack/constants.ts @@ -1,10 +1,11 @@ -import { ConfigMapping } from "./types.js"; +import { ConfigMapping } from "../common/types.js"; /** * ---------- PYTHON INSTRUCTIONS ---------- */ -const pythonInstructions = (username: string, accessKey: string) => ` +export const pythonInstructions = (username: string, accessKey: string) => { + const setup = ` ---STEP--- Install the BrowserStack SDK: @@ -18,7 +19,9 @@ Setup the BrowserStack SDK with your credentials: \`\`\`bash browserstack-sdk setup --username "${username}" --key "${accessKey}" \`\`\` +`; + const run = ` ---STEP--- Run your tests on BrowserStack: @@ -27,8 +30,12 @@ browserstack-sdk python \`\`\` `; -const generatePythonFrameworkInstructions = - (framework: string) => (username: string, accessKey: string) => ` + return { setup, run }; +}; + +export const generatePythonFrameworkInstructions = + (framework: string) => (username: string, accessKey: string) => { + const setup = ` ---STEP--- Install the BrowserStack SDK: @@ -43,7 +50,9 @@ Setup the BrowserStack SDK with framework-specific configuration: \`\`\`bash browserstack-sdk setup --framework "${framework}" --username "${username}" --key "${accessKey}" \`\`\` +`; + const run = ` ---STEP--- Run your ${framework} tests on BrowserStack: @@ -52,9 +61,12 @@ browserstack-sdk ${framework} \`\`\` `; -const robotInstructions = generatePythonFrameworkInstructions("robot"); -const behaveInstructions = generatePythonFrameworkInstructions("behave"); -const pytestInstructions = generatePythonFrameworkInstructions("pytest"); + return { setup, run }; + }; + +export const robotInstructions = generatePythonFrameworkInstructions("robot"); +export const behaveInstructions = generatePythonFrameworkInstructions("behave"); +export const pytestInstructions = generatePythonFrameworkInstructions("pytest"); /** * ---------- JAVA INSTRUCTIONS ---------- @@ -63,7 +75,8 @@ const pytestInstructions = generatePythonFrameworkInstructions("pytest"); const argsInstruction = '-javaagent:"${com.browserstack:browserstack-java-sdk:jar}"'; -const javaInstructions = (username: string, accessKey: string) => ` +export const javaInstructions = (username: string, accessKey: string) => { + const setup = ` ---STEP--- Add the BrowserStack Java SDK dependency to your \`pom.xml\`: @@ -92,7 +105,9 @@ Export your BrowserStack credentials as environment variables: export BROWSERSTACK_USERNAME=${username} export BROWSERSTACK_ACCESS_KEY=${accessKey} \`\`\` +`; + const run = ` ---STEP--- Run your tests using Maven: @@ -106,68 +121,18 @@ gradle clean test \`\`\` `; -const serenityInstructions = (username: string, accessKey: string) => ` ----STEP--- - -Set BrowserStack credentials as environment variables: -For macOS/Linux: -\`\`\`bash -export BROWSERSTACK_USERNAME=${username} -export BROWSERSTACK_ACCESS_KEY=${accessKey} -\`\`\` - -For Windows Command Prompt: -\`\`\`cmd -set BROWSERSTACK_USERNAME=${username} -set BROWSERSTACK_ACCESS_KEY=${accessKey} -\`\`\` - ----STEP--- - -Add serenity-browserstack dependency in pom.xml: -Add the following dependency to your pom.xml file and save it: -\`\`\`xml - - net.serenity-bdd - serenity-browserstack - 3.3.4 - -\`\`\` - ----STEP--- - -Set up serenity.conf file: -Create or update your serenity.conf file in the project root with the following configuration: -\`\`\` -webdriver { - driver = remote - remote.url = "https://hub.browserstack.com/wd/hub" -} -browserstack.user="${username}" -browserstack.key="${accessKey}" -\`\`\` - ----STEP--- - -Run your Serenity tests: -You can continue running your tests as you normally would. For example: - -Using Maven: -\`\`\`bash -mvn clean verify -\`\`\` - -Using Gradle: -\`\`\`bash -gradle clean test -\`\`\` -`; + return { setup, run }; +}; /** * ---------- CSharp INSTRUCTIONS ---------- */ -const csharpCommonInstructions = (username: string, accessKey: string) => ` +export const csharpCommonInstructions = ( + username: string, + accessKey: string, +) => { + const setup = ` ---STEP--- Install BrowserStack TestAdapter NuGet package: @@ -216,7 +181,9 @@ Install the x64 version of .NET for BrowserStack compatibility. sudo dotnet browserstack-sdk setup-dotnet --dotnet-path "" --dotnet-version "" \`\`\` Common paths: /usr/local/share/dotnet, ~/dotnet-x64, or /opt/dotnet-x64 +`; + const run = ` ---STEP--- Run the tests: @@ -230,10 +197,14 @@ Run the tests: \`\`\` `; -const csharpPlaywrightCommonInstructions = ( + return { setup, run }; +}; + +export const csharpPlaywrightCommonInstructions = ( username: string, accessKey: string, -) => ` +) => { + const setup = ` ---STEP--- Install BrowserStack TestAdapter NuGet package: @@ -295,7 +266,9 @@ Fix for Playwright architecture (macOS only): If the folder exists: \`/bin/Debug/net8.0/.playwright/node/darwin-arm64\` Rename \`darwin-arm64\` to \`darwin-x64\` +`; + const run = ` ---STEP--- Run the tests: @@ -309,11 +282,15 @@ Run the tests: \`\`\` `; + return { setup, run }; +}; + /** * ---------- NODEJS INSTRUCTIONS ---------- */ -const nodejsInstructions = (username: string, accessKey: string) => ` +export const nodejsInstructions = (username: string, accessKey: string) => { + const setup = ` ---STEP--- Ensure \`browserstack-node-sdk\` is present in package.json with the latest version: @@ -350,11 +327,27 @@ Run your tests: You can now run your tests on BrowserStack using your standard command or Use the commands defined in your package.json file to run the tests. `; + const run = ` +---STEP--- + +Run your tests on BrowserStack: +\`\`\`bash +npm run test:browserstack +\`\`\` +`; + + return { setup, run }; +}; + /** * ---------- EXPORT CONFIG ---------- */ -const webdriverioInstructions = (username: string, accessKey: string) => ` +export const webdriverioInstructions = ( + username: string, + accessKey: string, +) => { + const setup = ` ---STEP--- Set BrowserStack Credentials: @@ -453,14 +446,20 @@ exports.config.capabilities.forEach(function (caps) { caps[i] = { ...caps[i], ...exports.config.commonCapabilities[i]}; }); \`\`\` +`; + const run = ` ---STEP--- Run your tests: You can now run your tests on BrowserStack using your standard WebdriverIO command or Use the commands defined in your package.json file to run the tests. `; -const cypressInstructions = (username: string, accessKey: string) => ` + return { setup, run }; +}; + +export const cypressInstructions = (username: string, accessKey: string) => { + const setup = ` ---STEP--- Install the BrowserStack Cypress CLI: @@ -521,7 +520,9 @@ Open the generated \`browserstack.json\` file and update it with your BrowserSta \`\`\` **Note:** For Cypress v9 or lower, use \`"cypress_config_file": "./cypress.json"\`. The \`testObservability: true\` flag enables the [Test Reporting & Analytics dashboard](https://www.browserstack.com/docs/test-management/test-reporting-and-analytics) for deeper insights into your test runs. +`; + const run = ` ---STEP--- Run Your Tests on BrowserStack: @@ -530,9 +531,74 @@ Execute your tests on BrowserStack using the following command: npx browserstack-cypress run --sync \`\`\` -After the tests complete, you can view the results on your [BrowserStack Automate Dashboard](https://automate.browserstack.com/dashboard/). +After the tests complete, you can view the results on your [BrowserStack Automate Dashboard](https://automate.browserstack.com/dashboard/).`; + + return { setup, run }; +}; + +const serenityInstructions = (username: string, accessKey: string) => { + const setup = ` +---STEP--- + +Set BrowserStack credentials as environment variables: +For macOS/Linux: +\`\`\`bash +export BROWSERSTACK_USERNAME=${username} +export BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\` + +For Windows Command Prompt: +\`\`\`cmd +set BROWSERSTACK_USERNAME=${username} +set BROWSERSTACK_ACCESS_KEY=${accessKey} +\`\`\` + +---STEP--- + +Add serenity-browserstack dependency in pom.xml: +Add the following dependency to your pom.xml file and save it: +\`\`\`xml + + net.serenity-bdd + serenity-browserstack + 3.3.4 + +\`\`\` + +---STEP--- + +Set up serenity.conf file: +Create or update your serenity.conf file in the project root with the following configuration: +\`\`\` +webdriver { + driver = remote + remote.url = "https://hub.browserstack.com/wd/hub" +} +browserstack.user="${username}" +browserstack.key="${accessKey}" +\`\`\` +`; + + const run = ` +---STEP--- + +Run your Serenity tests: +You can continue running your tests as you normally would. For example: + +Using Maven: +\`\`\`bash +mvn clean verify +\`\`\` + +Using Gradle: +\`\`\`bash +gradle clean test +\`\`\` `; + return { setup, run }; +}; + export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { python: { playwright: { @@ -588,8 +654,5 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { cypress: { cypress: { instructions: cypressInstructions }, }, - webdriverio: { - mocha: { instructions: webdriverioInstructions }, - }, }, }; diff --git a/src/tools/sdk-utils/bstack/frameworks.ts b/src/tools/sdk-utils/bstack/frameworks.ts new file mode 100644 index 00000000..cfacb315 --- /dev/null +++ b/src/tools/sdk-utils/bstack/frameworks.ts @@ -0,0 +1,59 @@ +import { ConfigMapping } from "../common/types.js"; +import * as constants from "./constants.js"; + +export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { + python: { + playwright: { + pytest: { instructions: constants.pythonInstructions }, + }, + selenium: { + pytest: { instructions: constants.pytestInstructions }, + robot: { instructions: constants.robotInstructions }, + behave: { instructions: constants.behaveInstructions }, + }, + }, + java: { + playwright: { + junit4: { instructions: constants.javaInstructions }, + junit5: { instructions: constants.javaInstructions }, + testng: { instructions: constants.javaInstructions }, + }, + selenium: { + testng: { instructions: constants.javaInstructions }, + cucumber: { instructions: constants.javaInstructions }, + junit4: { instructions: constants.javaInstructions }, + junit5: { instructions: constants.javaInstructions }, + }, + }, + csharp: { + playwright: { + nunit: { instructions: constants.csharpPlaywrightCommonInstructions }, + mstest: { instructions: constants.csharpPlaywrightCommonInstructions }, + }, + selenium: { + xunit: { instructions: constants.csharpCommonInstructions }, + nunit: { instructions: constants.csharpCommonInstructions }, + mstest: { instructions: constants.csharpCommonInstructions }, + specflow: { instructions: constants.csharpCommonInstructions }, + reqnroll: { instructions: constants.csharpCommonInstructions }, + }, + }, + nodejs: { + playwright: { + jest: { instructions: constants.nodejsInstructions }, + codeceptjs: { instructions: constants.nodejsInstructions }, + playwright: { instructions: constants.nodejsInstructions }, + }, + selenium: { + jest: { instructions: constants.nodejsInstructions }, + webdriverio: { instructions: constants.webdriverioInstructions }, + mocha: { instructions: constants.nodejsInstructions }, + cucumber: { instructions: constants.nodejsInstructions }, + nightwatch: { instructions: constants.nodejsInstructions }, + codeceptjs: { instructions: constants.nodejsInstructions }, + }, + cypress: { + cypress: { instructions: constants.cypressInstructions }, + }, + }, +}; diff --git a/src/tools/sdk-utils/bstack/index.ts b/src/tools/sdk-utils/bstack/index.ts new file mode 100644 index 00000000..d11f85f1 --- /dev/null +++ b/src/tools/sdk-utils/bstack/index.ts @@ -0,0 +1,5 @@ +// BrowserStack SDK utilities +export { runBstackSDKOnly } from "./sdkHandler.js"; +export { getSDKPrefixCommand, getJavaFrameworkForMaven } from "./commands.js"; +export { generateBrowserStackYMLInstructions } from "./configUtils.js"; +export { SUPPORTED_CONFIGURATIONS } from "./frameworks.js"; diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts new file mode 100644 index 00000000..23957d7b --- /dev/null +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -0,0 +1,123 @@ +// Handler for BrowserStack SDK only (no Percy) - Sets up BrowserStack SDK with YML configuration +import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; +import { RunTestsOnBrowserStackInput } from "../common/schema.js"; +import { getBrowserStackAuth } from "../../../lib/get-auth.js"; +import { getSDKPrefixCommand } from "./commands.js"; +import { generateBrowserStackYMLInstructions } from "./configUtils.js"; +import { getInstructionsForProjectConfiguration } from "../common/instructionUtils.js"; +import { BrowserStackConfig } from "../../../lib/types.js"; +import { + SDKSupportedBrowserAutomationFramework, + SDKSupportedTestingFramework, + SDKSupportedLanguage, +} from "../common/types.js"; + +export function runBstackSDKOnly( + input: RunTestsOnBrowserStackInput, + config: BrowserStackConfig, + isPercyAutomate = false, +): RunTestsInstructionResult { + const steps: RunTestsStep[] = []; + const authString = getBrowserStackAuth(config); + const [username, accessKey] = authString.split(":"); + + // Handle frameworks with unique setup instructions that don't use browserstack.yml + if ( + input.detectedBrowserAutomationFramework === "cypress" || + input.detectedTestingFramework === "webdriverio" + ) { + const frameworkInstructions = getInstructionsForProjectConfiguration( + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, + input.detectedTestingFramework as SDKSupportedTestingFramework, + input.detectedLanguage as SDKSupportedLanguage, + username, + accessKey, + ); + + if (frameworkInstructions) { + if (frameworkInstructions.setup) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions.setup, + }); + } + + if (frameworkInstructions.run && !isPercyAutomate) { + steps.push({ + type: "instruction", + title: "Run the tests", + content: frameworkInstructions.run, + }); + } + } + + return { + steps, + requiresPercy: false, + missingDependencies: [], + }; + } + + // Default flow using browserstack.yml + const sdkSetupCommand = getSDKPrefixCommand( + input.detectedLanguage as SDKSupportedLanguage, + input.detectedTestingFramework as SDKSupportedTestingFramework, + username, + accessKey, + ); + + if (sdkSetupCommand) { + steps.push({ + type: "instruction", + title: "Install BrowserStack SDK", + content: sdkSetupCommand, + }); + } + + const ymlInstructions = generateBrowserStackYMLInstructions( + input.desiredPlatforms as string[], + false, + input.projectName, + ); + + if (ymlInstructions) { + steps.push({ + type: "instruction", + title: "Configure browserstack.yml", + content: ymlInstructions, + }); + } + + const frameworkInstructions = getInstructionsForProjectConfiguration( + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, + input.detectedTestingFramework as SDKSupportedTestingFramework, + input.detectedLanguage as SDKSupportedLanguage, + username, + accessKey, + ); + + if (frameworkInstructions) { + if (frameworkInstructions.setup) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions.setup, + }); + } + + if (frameworkInstructions.run && !isPercyAutomate) { + steps.push({ + type: "instruction", + title: "Run the tests", + content: frameworkInstructions.run, + }); + } + } + + return { + steps, + requiresPercy: false, + missingDependencies: [], + }; +} diff --git a/src/tools/sdk-utils/commands.ts b/src/tools/sdk-utils/commands.ts deleted file mode 100644 index f2573475..00000000 --- a/src/tools/sdk-utils/commands.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Utility to get the language-dependent prefix command for BrowserStack SDK setup -import { SDKSupportedLanguage } from "./types.js"; - -// Framework mapping for Java Maven archetype generation -const JAVA_FRAMEWORK_MAP: Record = { - testng: "testng", - junit5: "junit5", - junit4: "junit4", - cucumber: "cucumber-testng", - serenity: "serenity", -}; - -// Common Gradle setup instructions (platform-independent) -const GRADLE_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 getSDKPrefixCommand( - language: SDKSupportedLanguage, - framework: string, - username: string, - accessKey: string, -): string { - switch (language) { - case "nodejs": - return `---STEP--- -Install BrowserStack Node SDK using command: -\`\`\`bash -npm i -D browserstack-node-sdk@latest -\`\`\` ----STEP--- -Run the following command to setup browserstack sdk: -\`\`\`bash -npx setup --username ${username} --key ${accessKey} -\`\`\` ----STEP--- -Edit the browserstack.yml file that was created in the project root to add your desired platforms and browsers.`; - - case "java": { - const mavenFramework = getJavaFrameworkForMaven(framework); - const isWindows = process.platform === "win32"; - - const mavenCommand = isWindows - ? `mvn archetype:generate -B -DarchetypeGroupId="com.browserstack" -DarchetypeArtifactId="browserstack-sdk-archetype-integrate" -DarchetypeVersion="1.0" -DgroupId="com.browserstack" -DartifactId="browserstack-sdk-archetype-integrate" -Dversion="1.0" -DBROWSERSTACK_USERNAME="${process.env.BROWSERSTACK_USERNAME}" -DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" -DBROWSERSTACK_FRAMEWORK="${mavenFramework}"` - : `mvn archetype:generate -B -DarchetypeGroupId=com.browserstack \\ --DarchetypeArtifactId=browserstack-sdk-archetype-integrate -DarchetypeVersion=1.0 \\ --DgroupId=com.browserstack -DartifactId=browserstack-sdk-archetype-integrate -Dversion=1.0 \\ --DBROWSERSTACK_USERNAME="${username}" \\ --DBROWSERSTACK_ACCESS_KEY="${accessKey}" \\ --DBROWSERSTACK_FRAMEWORK="${mavenFramework}"`; - - const platformLabel = isWindows ? "Windows" : "macOS/Linux"; - - return `---STEP--- -Install BrowserStack Java SDK - -**Maven command for ${framework} (${platformLabel}):** -Run the command, it is required to generate the browserstack-sdk-archetype-integrate project: -${mavenCommand} - -Alternative setup for Gradle users: -${GRADLE_SETUP_INSTRUCTIONS}`; - } - - // Add more languages as needed - default: - return ""; - } -} - -export function getJavaFrameworkForMaven(framework: string): string { - return JAVA_FRAMEWORK_MAP[framework] || framework; -} diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts new file mode 100644 index 00000000..36bfee70 --- /dev/null +++ b/src/tools/sdk-utils/common/constants.ts @@ -0,0 +1,102 @@ +export const IMPORTANT_SETUP_WARNING = + "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup."; + +export const SETUP_PERCY_DESCRIPTION = + "Set up Percy visual testing for your project. This supports both Percy Web Standalone and Percy Automate."; + +export const LIST_TEST_FILES_DESCRIPTION = + "Lists all test files for a given set of directories."; + +export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = + "Adds Percy snapshot commands to the specified test files."; + +export const RUN_ON_BROWSERSTACK_DESCRIPTION = + "Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use this tool for functional or integration test setup on BrowserStack only. For any visual testing or Percy integration, use the dedicated Percy setup tool. Example prompts: run this test on browserstack; set up this project for browserstack."; + +export const SIMULATE_PERCY_CHANGE_DESCRIPTION = + "This tool helps new users simulate a Percy visual change by guiding them to make a visible UI modification, rerun Percy, and verify that the change is detected. Only Percy web is supported"; + +export const PERCY_REPLACE_REGEX = + /Invoke listTestFiles\(\) with the provided directories[\s\S]*?- DO NOT STOP until you add commands in all the files or you reach end of the files\./; + +export const PERCY_SNAPSHOT_INSTRUCTION = ` +Invoke listTestFiles() with the provided directories from user to gather all test files in memory and obtain the generated UUID ---STEP--- +Process files in STRICT sequential order using tool addPercySnapshotCommands() with below instructions: +- Start with index 0 +- Then index 1 +- Then index 2 +- Continue sequentially until the last index +- DO NOT skip any index numbers +- DO NOT jump to random indices like 10, 20, 30 +- DO NOT worry about time consuming and any constraints. +- DO NOT STOP until you add commands in all the files or you reach end of the files. +`; + +export const PERCY_SIMULATE_INSTRUCTION = ` +MANDATORY: Aggressive, Accurate Percy Snapshots Integration + +Prerequisite: Before making any code changes in the user's repo, switch to a branch named temp-percy (run git init first if the repo is not yet initialized). + +You MUST follow these steps with NO EXCEPTIONS: +1. Identify 4-5 key test files in the project that are critical for visual testing. +2. For EACH identified test file, analyze the code to find EVERY location where a Percy snapshot should be called (e.g., after each navigation, UI change, or significant state in tests). +3. Insert snapshot calls at ALL such points, not just the first or most obvious one. + +Iteratively update every selected test file with percy snapshot calls at all identified points, ensuring comprehensive coverage of visual changes.`; + +export const PERCY_SIMULATION_DRIVER_INSTRUCTION = ` +MANDATORY: Step-by-Step Percy Visual Change Simulation +You MUST follow these steps with NO EXCEPTIONS: + +STEP 1: At this case we have added Percy snapshot commands to the test files at the appropriate locations (e.g., after navigation, UI change, or significant state). +- Use ONLY existing snapshot names. +- Do NOT add any popup injection or visual changes yet. +- Do NOT run any Percy builds at this stage. + +STEP 2: Run ONE comprehensive baseline Percy build that executes ALL tests containing Percy snapshots in a SINGLE build. This creates one baseline build with all snapshots for comparison. Use a command like: npx percy exec -- python -m pytest tests/ -k 'test_name1 or test_name2 or test_name3' -v to run multiple specific tests in one build." + +STEP 3: Modify your test to inject a visible UI change (such as a popup) IMMEDIATELY BEFORE an EXISTING snapshot command (e.g., before percy_snapshot(self.driver, "screenshot name")). +- Do NOT add a new snapshot name for the popup. +- The popup must appear in an existing snapshot, not a new one. +- Add this popup code in some test files before the percy_snapshot command you've added, to display the visual changes. + +\`\`\`Javascript +popup_script = \` +var popup = document.createElement('div'); +popup.id = 'percy-test-popup'; +popup.style.cssText = popup.style.cssText = \` + /* TODO: Add styles to make the popup large, centered, and visually noticeable. + Suggested properties: position: fixed; top/left; transform; background; color; font-size; padding; z-index; animation, etc. */ +\`; +popup.innerHTML = 'PERCY TEST
VISUAL CHANGE
DETECTED!'; +document.body.appendChild(popup); +\`; + +# Insert this just before the EXISTING snapshot command: +driver.execute_script(popup_script) +percy_snapshot(self.driver, "Before Adding to Cart") # (Do NOT change the snapshot name, keep existing one) +\`\`\` + +STEP 4: Run a second Percy build. +- The snapshot names must remain the same as in the baseline. +- The visual change should now appear in the same snapshot as before. +- Use the same build command you ran for the baseline. + +STEP 5: Compare the two Percy builds to see the detected visual difference. + +STEP 6: Now ask user if they want to setup percy for full project coverage? If yes, call the "setupPercyVisualTesting" tool to enable complete coverage for the entire project. + +CONSTRAINTS: +- Do NOT run any builds until explicitly instructed in the steps. +- Do NOT add new snapshot names—only use existing ones. +- Do NOT add popup injection until the baseline is established. +- Visual changes must appear in EXISTING snapshots, not new ones. + +VALIDATION CHECKPOINTS (before proceeding to the next step): +- Are you adding only snapshot commands (not running builds)? +- Are you reusing existing snapshot names (not creating new ones)? +- Have you established the baseline first (before adding visual changes) + +CRITICAL: +Do NOT run tests separately or create multiple builds during baseline establishment. The goal is to have exactly TWO builds total: (1) baseline build with all original snapshots, (2) modified build with the same tests but visual changes injected. +`; diff --git a/src/tools/sdk-utils/common/formatUtils.ts b/src/tools/sdk-utils/common/formatUtils.ts new file mode 100644 index 00000000..e50527b5 --- /dev/null +++ b/src/tools/sdk-utils/common/formatUtils.ts @@ -0,0 +1,34 @@ +export function formatInstructionsWithNumbers( + instructionText: string, + separator: string = "---STEP---", +): { formattedSteps: string; stepCount: number } { + // Split the instructions by the separator + const steps = instructionText + .split(separator) + .map((step) => step.trim()) + .filter((step) => step.length > 0); + + // If no separators found, treat the entire text as one step + if (steps.length === 1 && !instructionText.includes(separator)) { + return { + formattedSteps: `**Step 1:**\n${instructionText.trim()}`, + stepCount: 1, + }; + } + + // Format each step with numbering + const formattedSteps = steps + .map((step, index) => { + return `**Step ${index + 1}:**\n${step.trim()}`; + }) + .join("\n\n"); + + return { + formattedSteps, + stepCount: steps.length, + }; +} + +export function generateVerificationMessage(stepCount: number): string { + return `**✅ Verification:**\nPlease verify that you have completed all ${stepCount} steps above to ensure proper setup. If you encounter any issues, double-check each step and ensure all commands executed successfully.`; +} diff --git a/src/tools/sdk-utils/common/index.ts b/src/tools/sdk-utils/common/index.ts new file mode 100644 index 00000000..7a145f57 --- /dev/null +++ b/src/tools/sdk-utils/common/index.ts @@ -0,0 +1,4 @@ +// Common utilities and types for SDK tools +export * from "./types.js"; +export * from "./constants.js"; +export * from "./formatUtils.js"; diff --git a/src/tools/sdk-utils/common/instructionUtils.ts b/src/tools/sdk-utils/common/instructionUtils.ts new file mode 100644 index 00000000..484928ed --- /dev/null +++ b/src/tools/sdk-utils/common/instructionUtils.ts @@ -0,0 +1,49 @@ +/** + * Core instruction configuration utilities for runTestsOnBrowserStack tool. + */ + +import { SUPPORTED_CONFIGURATIONS } from "../bstack/frameworks.js"; +import { + SDKSupportedLanguage, + SDKSupportedBrowserAutomationFramework, + SDKSupportedTestingFramework, +} from "./types.js"; + +const errorMessageSuffix = + "Please open an issue at our Github repo: https://github.com/browserstack/browserstack-mcp-server/issues to request support for your project configuration"; + +export const getInstructionsForProjectConfiguration = ( + detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFramework, + detectedTestingFramework: SDKSupportedTestingFramework, + detectedLanguage: SDKSupportedLanguage, + username: string, + accessKey: string, +) => { + const configuration = SUPPORTED_CONFIGURATIONS[detectedLanguage]; + + if (!configuration) { + throw new Error( + `BrowserStack MCP Server currently does not support ${detectedLanguage}, ${errorMessageSuffix}`, + ); + } + + if (!configuration[detectedBrowserAutomationFramework]) { + throw new Error( + `BrowserStack MCP Server currently does not support ${detectedBrowserAutomationFramework} for ${detectedLanguage}, ${errorMessageSuffix}`, + ); + } + + if ( + !configuration[detectedBrowserAutomationFramework][detectedTestingFramework] + ) { + throw new Error( + `BrowserStack MCP Server currently does not support ${detectedTestingFramework} for ${detectedBrowserAutomationFramework} on ${detectedLanguage}, ${errorMessageSuffix}`, + ); + } + + const instructionFunction = + configuration[detectedBrowserAutomationFramework][detectedTestingFramework] + .instructions; + + return instructionFunction(username, accessKey); +}; diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts new file mode 100644 index 00000000..5b41c395 --- /dev/null +++ b/src/tools/sdk-utils/common/schema.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import { + SDKSupportedBrowserAutomationFrameworkEnum, + SDKSupportedTestingFrameworkEnum, + SDKSupportedLanguageEnum, +} from "./types.js"; +import { PercyIntegrationTypeEnum } from "./types.js"; + +export const SetUpPercyParamsShape = { + projectName: z.string().describe("A unique name for your Percy project."), + detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), + detectedBrowserAutomationFramework: z.nativeEnum( + SDKSupportedBrowserAutomationFrameworkEnum, + ), + detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), + integrationType: z + .nativeEnum(PercyIntegrationTypeEnum) + .describe( + "Specifies whether to integrate with Percy Web or Percy Automate. If not explicitly provided, prompt the user to select the desired integration type.", + ), + folderPaths: z + .array(z.string()) + .describe( + "An array of folder paths to include in which Percy will be integrated. If not provided, strictly inspect the code and return the folders which contain UI test cases.", + ), +}; + +export const RunTestsOnBrowserStackParamsShape = { + projectName: z + .string() + .describe( + "A single name for your project to organize all your tests. This is required for Percy.", + ), + detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), + detectedBrowserAutomationFramework: z.nativeEnum( + SDKSupportedBrowserAutomationFrameworkEnum, + ), + detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), + desiredPlatforms: z + .array(z.enum(["windows", "macos", "android", "ios"])) + .describe("An array of platforms to run tests on."), +}; + +export const SetUpPercySchema = z.object(SetUpPercyParamsShape); +export const RunTestsOnBrowserStackSchema = z.object( + RunTestsOnBrowserStackParamsShape, +); + +export type SetUpPercyInput = z.infer; +export type RunTestsOnBrowserStackInput = z.infer< + typeof RunTestsOnBrowserStackSchema +>; diff --git a/src/tools/sdk-utils/common/types.ts b/src/tools/sdk-utils/common/types.ts new file mode 100644 index 00000000..3994a899 --- /dev/null +++ b/src/tools/sdk-utils/common/types.ts @@ -0,0 +1,94 @@ +export enum PercyIntegrationTypeEnum { + WEB = "web", + AUTOMATE = "automate", +} + +export enum SDKSupportedLanguageEnum { + nodejs = "nodejs", + python = "python", + java = "java", + csharp = "csharp", + ruby = "ruby", +} +export type SDKSupportedLanguage = keyof typeof SDKSupportedLanguageEnum; + +export enum SDKSupportedBrowserAutomationFrameworkEnum { + playwright = "playwright", + selenium = "selenium", + cypress = "cypress", +} +export type SDKSupportedBrowserAutomationFramework = + keyof typeof SDKSupportedBrowserAutomationFrameworkEnum; + +export enum SDKSupportedTestingFrameworkEnum { + jest = "jest", + codeceptjs = "codeceptjs", + playwright = "playwright", + pytest = "pytest", + robot = "robot", + behave = "behave", + cucumber = "cucumber", + nightwatch = "nightwatch", + webdriverio = "webdriverio", + mocha = "mocha", + junit4 = "junit4", + junit5 = "junit5", + testng = "testng", + cypress = "cypress", + nunit = "nunit", + mstest = "mstest", + xunit = "xunit", + specflow = "specflow", + reqnroll = "reqnroll", + rspec = "rspec", + serenity = "serenity", +} + +export const SDKSupportedLanguages = Object.values(SDKSupportedLanguageEnum); +export type SDKSupportedTestingFramework = + keyof typeof SDKSupportedTestingFrameworkEnum; +export const SDKSupportedTestingFrameworks = Object.values( + SDKSupportedTestingFrameworkEnum, +); + +export type ConfigMapping = Partial< + Record< + SDKSupportedLanguageEnum, + Partial< + Record< + SDKSupportedBrowserAutomationFrameworkEnum, + Partial< + Record< + SDKSupportedTestingFrameworkEnum, + { + instructions: ( + username: string, + accessKey: string, + ) => { setup: string; run: string }; + } + > + > + > + > + > +>; + +// Common interfaces for instruction results +export interface RunTestsStep { + type: "instruction" | "error" | "warning"; + title: string; + content: string; + isError?: boolean; +} + +export interface RunTestsInstructionResult { + steps: RunTestsStep[]; + requiresPercy: boolean; + missingDependencies: string[]; + shouldSkipFormatting?: boolean; +} + +export enum PercyAutomateNotImplementedType { + LANGUAGE = "language", + FRAMEWORK = "framework", +} diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts new file mode 100644 index 00000000..9ddd6793 --- /dev/null +++ b/src/tools/sdk-utils/common/utils.ts @@ -0,0 +1,128 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { PercyIntegrationTypeEnum } from "../common/types.js"; +import { isPercyAutomateFrameworkSupported } from "../percy-automate/frameworks.js"; +import { isPercyWebFrameworkSupported } from "../percy-web/frameworks.js"; +import { + formatInstructionsWithNumbers, + generateVerificationMessage, +} from "./formatUtils.js"; +import { + RunTestsInstructionResult, + PercyAutomateNotImplementedType, +} from "./types.js"; +import { IMPORTANT_SETUP_WARNING } from "./index.js"; + +export function checkPercyIntegrationSupport(input: { + integrationType: string; + detectedLanguage: string; + detectedTestingFramework?: string; + detectedBrowserAutomationFramework?: string; +}): { supported: boolean; errorMessage?: string } { + if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { + const isSupported = isPercyAutomateFrameworkSupported( + input.detectedLanguage, + input.detectedBrowserAutomationFramework || "", + input.detectedTestingFramework || "", + ); + if (!isSupported) { + return { + supported: false, + errorMessage: `Percy Automate is not supported for this configuration. Language: ${input.detectedLanguage} Testing Framework: ${input.detectedTestingFramework}`, + }; + } + } else if (input.integrationType === PercyIntegrationTypeEnum.WEB) { + const isSupported = isPercyWebFrameworkSupported( + input.detectedLanguage, + input.detectedBrowserAutomationFramework || "", + ); + if (!isSupported) { + return { + supported: false, + errorMessage: `Percy Web is not supported for this configuration. Language: ${input.detectedLanguage} Browser Automation Framework: ${input.detectedBrowserAutomationFramework}`, + }; + } + } + return { supported: true }; +} + +export async function formatToolResult( + resultPromise: Promise | RunTestsInstructionResult, +): Promise { + const { steps, requiresPercy, missingDependencies, shouldSkipFormatting } = + await resultPromise; + + if (shouldSkipFormatting) { + return { + content: steps.map((step) => ({ + type: "text" as const, + text: step.content, + })), + isError: steps.some((s) => s.isError), + steps, + requiresPercy, + missingDependencies, + }; + } + + const combinedInstructions = steps.map((step) => step.content).join("\n"); + const { formattedSteps, stepCount } = + formatInstructionsWithNumbers(combinedInstructions); + const verificationMessage = generateVerificationMessage(stepCount); + + const finalContent = [ + { type: "text" as const, text: IMPORTANT_SETUP_WARNING }, + { type: "text" as const, text: formattedSteps }, + { type: "text" as const, text: verificationMessage }, + ]; + + return { + content: finalContent, + isError: steps.some((s) => s.isError), + requiresPercy, + missingDependencies, + }; +} + +export function getPercyAutomateNotImplementedMessage( + type: PercyAutomateNotImplementedType, + input: { + detectedLanguage: string; + detectedBrowserAutomationFramework: string; + }, + supported: string[], +): string { + if (type === PercyAutomateNotImplementedType.LANGUAGE) { + return `Percy Automate does not support the language: ${input.detectedLanguage}. Supported languages are: ${supported.join(", ")}.`; + } else { + return `Percy Automate does not support ${input.detectedBrowserAutomationFramework} for ${input.detectedLanguage}. Supported frameworks for ${input.detectedLanguage} are: ${supported.join(", ")}.`; + } +} + +export function getBootstrapFailedMessage( + error: unknown, + context: { config: unknown; percyMode?: string; sdkVersion?: string }, +): string { + return `Failed to bootstrap project with BrowserStack SDK. +Error: ${error} +Percy Mode: ${context.percyMode ?? "automate"} +SDK Version: ${context.sdkVersion ?? "N/A"} +Please open an issue on GitHub if the problem persists.`; +} + +export function percyUnsupportedResult( + integrationType: PercyIntegrationTypeEnum, + supportCheck?: { errorMessage?: string }, +): CallToolResult { + const defaultMessage = `Percy ${integrationType} integration is not supported for this configuration.`; + + return { + content: [ + { + type: "text", + text: supportCheck?.errorMessage || defaultMessage, + }, + ], + isError: true, + shouldSkipFormatting: true, + }; +} diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts new file mode 100644 index 00000000..aaf7592b --- /dev/null +++ b/src/tools/sdk-utils/handler.ts @@ -0,0 +1,204 @@ +import { + SetUpPercySchema, + RunTestsOnBrowserStackSchema, +} from "./common/schema.js"; +import { + getBootstrapFailedMessage, + percyUnsupportedResult, +} from "./common/utils.js"; +import { formatToolResult } from "./common/utils.js"; +import { BrowserStackConfig } from "../../lib/types.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { PercyIntegrationTypeEnum } from "./common/types.js"; +import { getBrowserStackAuth } from "../../lib/get-auth.js"; +import { fetchPercyToken } from "./percy-web/fetchPercyToken.js"; +import { runPercyWeb } from "./percy-web/handler.js"; +import { runPercyAutomateOnly } from "./percy-automate/handler.js"; +import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; +import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js"; +import { checkPercyIntegrationSupport } from "./common/utils.js"; +import { + PERCY_SIMULATE_INSTRUCTION, + PERCY_REPLACE_REGEX, + PERCY_SIMULATION_DRIVER_INSTRUCTION, +} from "./common/constants.js"; + +export async function runTestsOnBrowserStackHandler( + rawInput: unknown, + config: BrowserStackConfig, +): Promise { + try { + const input = RunTestsOnBrowserStackSchema.parse(rawInput); + + // Only handle BrowserStack SDK setup for functional/integration tests. + const result = runBstackSDKOnly(input, config); + return await formatToolResult(result); + } catch (error) { + throw new Error(getBootstrapFailedMessage(error, { config })); + } +} + +export async function setUpPercyHandler( + rawInput: unknown, + config: BrowserStackConfig, +): Promise { + try { + const input = SetUpPercySchema.parse(rawInput); + const authorization = getBrowserStackAuth(config); + + const percyInput = { + projectName: input.projectName, + detectedLanguage: input.detectedLanguage, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, + detectedTestingFramework: input.detectedTestingFramework, + integrationType: input.integrationType, + folderPaths: input.folderPaths || [], + }; + + // Check for Percy Web integration support + if (input.integrationType === PercyIntegrationTypeEnum.WEB) { + const supportCheck = checkPercyIntegrationSupport(percyInput); + if (!supportCheck.supported) { + return percyUnsupportedResult( + PercyIntegrationTypeEnum.WEB, + supportCheck, + ); + } + + // Fetch the Percy token + const percyToken = await fetchPercyToken( + input.projectName, + authorization, + { type: PercyIntegrationTypeEnum.WEB }, + ); + + const result = runPercyWeb(percyInput, percyToken); + return await formatToolResult(result); + } else if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { + // First try Percy with BrowserStack SDK + const percyWithBrowserstackSDKResult = runPercyWithBrowserstackSDK( + { + ...percyInput, + desiredPlatforms: [], + }, + config, + ); + const hasPercySDKError = + percyWithBrowserstackSDKResult.steps && + percyWithBrowserstackSDKResult.steps.some((step) => step.isError); + + if (!hasPercySDKError) { + // Percy with SDK is supported, prepend warning and return those steps + if (percyWithBrowserstackSDKResult.steps) { + percyWithBrowserstackSDKResult.steps.unshift({ + type: "instruction" as const, + title: "Important: Existing SDK Setup", + content: + "If you have already set up the BrowserStack SDK, do not override it unless you have explicitly decided to do so.", + }); + } + return await formatToolResult(percyWithBrowserstackSDKResult); + } else { + // Fallback to standalone Percy Automate if supported + const supportCheck = checkPercyIntegrationSupport({ + ...percyInput, + integrationType: PercyIntegrationTypeEnum.AUTOMATE, + }); + if (!supportCheck.supported) { + return percyUnsupportedResult( + PercyIntegrationTypeEnum.AUTOMATE, + supportCheck, + ); + } + // SDK setup instructions (for Automate, without Percy) + const sdkInput = { + projectName: input.projectName, + detectedLanguage: input.detectedLanguage, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, + detectedTestingFramework: input.detectedTestingFramework, + desiredPlatforms: [], + }; + const sdkResult = runBstackSDKOnly(sdkInput, config, true); + // Percy Automate instructions + const percyToken = await fetchPercyToken( + input.projectName, + authorization, + { type: PercyIntegrationTypeEnum.AUTOMATE }, + ); + const percyAutomateResult = runPercyAutomateOnly( + percyInput, + percyToken, + ); + + // Combine steps: warning, SDK steps, Percy Automate steps + const steps = [ + { + type: "instruction" as const, + title: "Important: Existing SDK Setup", + content: + "If you have already set up the BrowserStack SDK, do not override it unless you have explicitly decided to do so.", + }, + ...(sdkResult.steps || []), + ...(percyAutomateResult.steps || []), + ]; + + // Combine all steps into the final result + return await formatToolResult({ + ...percyAutomateResult, + steps, + }); + } + } else { + return { + content: [ + { + type: "text", + text: "Unknown or unsupported Percy integration type requested.", + }, + ], + isError: true, + shouldSkipFormatting: true, + }; + } + } catch (error) { + throw new Error(getBootstrapFailedMessage(error, { config })); + } +} + +export async function setUpSimulatePercyChangeHandler( + rawInput: unknown, + config: BrowserStackConfig, +): Promise { + try { + const percyInstruction = await setUpPercyHandler(rawInput, config); + + if (percyInstruction.isError) { + return percyInstruction; + } + + if (Array.isArray(percyInstruction.content)) { + percyInstruction.content.forEach((item) => { + if ( + typeof item.text === "string" && + PERCY_REPLACE_REGEX.test(item.text) + ) { + item.text = item.text.replace( + PERCY_REPLACE_REGEX, + PERCY_SIMULATE_INSTRUCTION, + ); + } + }); + } + + percyInstruction.content?.push({ + type: "text" as const, + text: PERCY_SIMULATION_DRIVER_INSTRUCTION, + }); + + return percyInstruction; + } catch (error) { + throw new Error(getBootstrapFailedMessage(error, { config })); + } +} diff --git a/src/tools/sdk-utils/instructions.ts b/src/tools/sdk-utils/instructions.ts deleted file mode 100644 index d98c872c..00000000 --- a/src/tools/sdk-utils/instructions.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { SUPPORTED_CONFIGURATIONS } from "./constants.js"; -import { SDKSupportedLanguage } from "./types.js"; -import { SDKSupportedBrowserAutomationFramework } from "./types.js"; -import { SDKSupportedTestingFramework } from "./types.js"; - -const errorMessageSuffix = - "Please open an issue at our Github repo: https://github.com/browserstack/browserstack-mcp-server/issues to request support for your project configuration"; - -export const getInstructionsForProjectConfiguration = ( - detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFramework, - detectedTestingFramework: SDKSupportedTestingFramework, - detectedLanguage: SDKSupportedLanguage, - username: string, - accessKey: string, -) => { - const configuration = SUPPORTED_CONFIGURATIONS[detectedLanguage]; - - if (!configuration) { - throw new Error( - `BrowserStack MCP Server currently does not support ${detectedLanguage}, ${errorMessageSuffix}`, - ); - } - - if (!configuration[detectedBrowserAutomationFramework]) { - throw new Error( - `BrowserStack MCP Server currently does not support ${detectedBrowserAutomationFramework} for ${detectedLanguage}, ${errorMessageSuffix}`, - ); - } - - if ( - !configuration[detectedBrowserAutomationFramework][detectedTestingFramework] - ) { - throw new Error( - `BrowserStack MCP Server currently does not support ${detectedTestingFramework} for ${detectedBrowserAutomationFramework} on ${detectedLanguage}, ${errorMessageSuffix}`, - ); - } - - const instructionFunction = - configuration[detectedBrowserAutomationFramework][detectedTestingFramework] - .instructions; - - return instructionFunction(username, accessKey); -}; - -export function generateBrowserStackYMLInstructions( - desiredPlatforms: string[], - enablePercy: boolean = false, -) { - let ymlContent = ` -# ====================== -# BrowserStack Reporting -# ====================== -# Project and build names help organize your test runs in BrowserStack dashboard and Percy. -# TODO: Replace these sample values with your actual project details -projectName: Sample Project -buildName: Sample Build - -# ======================================= -# Platforms (Browsers / Devices to test) -# ======================================= -# Platforms object contains all the browser / device combinations you want to test on. -# Generate this on the basis of the following platforms requested by the user: -# Requested platforms: ${desiredPlatforms} -platforms: - - os: Windows - osVersion: 11 - browserName: chrome - browserVersion: latest - -# ======================= -# Parallels per Platform -# ======================= -# The number of parallel threads to be used for each platform set. -# BrowserStack's SDK runner will select the best strategy based on the configured value -# -# Example 1 - If you have configured 3 platforms and set \`parallelsPerPlatform\` as 2, a total of 6 (2 * 3) parallel threads will be used on BrowserStack -# -# Example 2 - If you have configured 1 platform and set \`parallelsPerPlatform\` as 5, a total of 5 (1 * 5) parallel threads will be used on BrowserStack -parallelsPerPlatform: 1 - -# ================= -# Local Testing -# ================= -# Set to true to test local -browserstackLocal: true - -# =================== -# Debugging features -# =================== -debug: true # Visual logs, text logs, etc. -testObservability: true # For Test Observability`; - - if (enablePercy) { - ymlContent += ` - -# ===================== -# Percy Visual Testing -# ===================== -# Set percy to true to enable visual testing. -# Set percyCaptureMode to 'manual' to control when screenshots are taken. -percy: true -percyCaptureMode: manual`; - } - return ` - Create a browserstack.yml file in the project root. The file should be in the following format: - - \`\`\`yaml${ymlContent} - \`\`\` - \n`; -} - -export function formatInstructionsWithNumbers( - instructionText: string, - separator: string = "---STEP---", -): string { - // Split the instructions by the separator - const steps = instructionText - .split(separator) - .map((step) => step.trim()) - .filter((step) => step.length > 0); - - // If no separators found, treat the entire text as one step - if (steps.length === 1 && !instructionText.includes(separator)) { - return `**Step 1:**\n${instructionText.trim()}\n\n**✅ Verification:**\nPlease verify that you have completed all the steps above to ensure proper setup.`; - } - - // Format each step with numbering - const formattedSteps = steps - .map((step, index) => { - return `**Step ${index + 1}:**\n${step.trim()}`; - }) - .join("\n\n"); - - // Add verification statement at the end - const verificationText = `\n\n**✅ Verification:**\nPlease verify that you have completed all ${steps.length} steps above to ensure proper setup. If you encounter any issues, double-check each step and ensure all commands executed successfully.`; - - return formattedSteps + verificationText; -} diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts new file mode 100644 index 00000000..be28b192 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -0,0 +1,375 @@ +import { PERCY_SNAPSHOT_INSTRUCTION } from "../common/constants.js"; +export const percyAutomateReviewSnapshotsStep = ` +---STEP--- +Review the snapshots + - Go to your Percy project on https://percy.io to review snapshots and approve/reject any visual changes. +`; + +export const pythonPytestSeleniumInstructions = ` +Install Percy Automate dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy Python SDK for Automate: + pip install percy-selenium + +---STEP--- +Update your Pytest test script +${PERCY_SNAPSHOT_INSTRUCTION} + - Import the Percy snapshot helper: + from percy import percy_screenshot + - In your test, take snapshots at key points: + percy_screenshot(driver, "Your snapshot name") + +Example: +\`\`\`python +import pytest +from selenium import webdriver +from percy import percy_screenshot + +@pytest.fixture +def driver(): + driver = webdriver.Chrome() + yield driver + driver.quit() + +def test_homepage(driver): + driver.get("http://localhost:8000") + percy_screenshot(driver, "Home page") + # ... more test steps ... + percy_screenshot(driver, "After login") +\`\`\` + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- browserstack-sdk pytest + +${percyAutomateReviewSnapshotsStep} +`; + +export const pythonPytestPlaywrightInstructions = ` +Install Percy Automate dependencies + - Install Percy CLI: + npm install --save @percy/cli + - Install Percy Playwright SDK for Automate: + pip install percy-playwright + +---STEP--- +Update your Playwright test script +${PERCY_SNAPSHOT_INSTRUCTION} + - Import the Percy screenshot helper: + from percy import percy_screenshot + - In your test, take snapshots at key points: + percy_screenshot(page, name="Your snapshot name") + # You can pass \`options\`: + percy_screenshot(page, name="Your snapshot name", options={ "full_page": True }) + +Example: +\`\`\`python +from playwright.sync_api import sync_playwright +from percy import percy_screenshot + +def test_visual_regression(): + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.goto("http://localhost:8000") + percy_screenshot(page, name="Home page") + # ... more test steps ... + percy_screenshot(page, name="After login", options={ "full_page": True }) + browser.close() +\`\`\` + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- + +${percyAutomateReviewSnapshotsStep} +`; + +export const jsCypressPercyAutomateInstructions = ` +Install Percy Automate dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy Cypress SDK: + npm install --save-dev @percy/cypress + +---STEP--- +Update your Cypress test script +${PERCY_SNAPSHOT_INSTRUCTION} + - Import and initialize Percy in your cypress/support/index.js: + import '@percy/cypress'; + - In your test, take snapshots at key points: + cy.percySnapshot('Your snapshot name'); + +Example: +\`\`\`javascript +describe('Percy Automate Cypress Example', () => { + it('should take Percy snapshots', () => { + cy.visit('http://localhost:8000'); + cy.percySnapshot('Home page'); + // ... more test steps ... + cy.percySnapshot('After login'); + }); +}); +\`\`\` + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- cypress run + +${percyAutomateReviewSnapshotsStep} +`; + +export const mochaPercyAutomateInstructions = ` +Install Percy Automate dependencies + - Install Percy CLI: + npm install --save @percy/cli + - Install Percy Selenium SDK: + npm install @percy/selenium-webdriver@2.0.1 + +---STEP--- +Update your Mocha Automate test script + - Import the Percy screenshot helper: + const { percyScreenshot } = require('@percy/selenium-webdriver'); + - Use the Percy screenshot command to take required screenshots in your Automate session: + await percyScreenshot(driver, 'Screenshot 1'); + options = { percyCSS: 'h1{color:red;}' }; + await percyScreenshot(driver, 'Screenshot 2', options); + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- + +${percyAutomateReviewSnapshotsStep} +`; + +// Mocha Percy Playwright Instructions +export const mochaPercyPlaywrightInstructions = ` +Install Percy Automate dependencies + - Install the latest Percy CLI: + npm install --save @percy/cli + - Install the Percy Playwright SDK: + npm install @percy/playwright + +---STEP--- +Update your Mocha Playwright test script + - Import the Percy screenshot helper: + const { percyScreenshot } = require("@percy/playwright"); + - Use the Percy screenshot command to take required screenshots in your Automate session. + +Example: +\`\`\`javascript +const { percyScreenshot } = require("@percy/playwright"); +await percyScreenshot(page, "Screenshot 1"); +// With options +await percyScreenshot(page, "Screenshot 2", { percyCSS: "h1{color:green;}" }); +\`\`\` + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- + +${percyAutomateReviewSnapshotsStep} +`; + +export const jestPercyAutomateInstructions = ` +Install or upgrade the BrowserStack SDK: + - Install the SDK: + npm i -D browserstack-node-sdk@latest + - Run the setup: + npx setup --username "YOUR_USERNAME" --key "YOUR_ACCESS_KEY" + +---STEP--- +Manually capture screenshots: + 1. Import the BrowserStack Percy SDK in your test script: + const { percy } = require('browserstack-node-sdk'); + 2. Use \`percy.screenshot(driver, name)\` at desired points in your test. + +Example: +\`\`\`javascript +const { percy } = require('browserstack-node-sdk'); +describe("JestJS test", () => { + let driver; + const caps = require("../" + conf_file).capabilities; + + beforeAll(() => { + driver = new Builder() + .usingServer("http://example-servername/hub") + .withCapabilities(caps) + .build(); + }); + + test("my test", async () => { + // ... + await percy.screenshot(driver, "My Screenshot"); + // ... + }); +}); +\`\`\` + +---STEP--- +Run your test script: + - Use the following command: + npm run [your-test-script-name]-browserstack + +${percyAutomateReviewSnapshotsStep} +`; + +export const webdriverioPercyAutomateInstructions = ` +Install or upgrade BrowserStack SDK + - Install the BrowserStack SDK: + npm i -D @wdio/browserstack-service + +---STEP--- +Update your WebdriverIO config file + 1. Set \`percy: true\` + 2. Set a \`projectName\` + 3. Set \`percyCaptureMode: auto\` (or another mode as needed) + +Example WebdriverIO config: +\`\`\`js +exports.config = { + user: process.env.BROWSERSTACK_USERNAME || 'YOUR_USERNAME', + key: process.env.BROWSERSTACK_ACCESS_KEY || 'YOUR_ACCESS_KEY', + hostname: 'hub.browserstack.com', + services: [ + [ + 'browserstack', + { browserstackLocal: true, opts: { forcelocal: false }, percy: true, percyCaptureMode: 'auto' } + ], + ], + // add path to the test file +} +\`\`\` + +---STEP--- +(Optional) Manually capture screenshots + 1. Import the BrowserStack Percy SDK in your test script: + const { percy } = require('browserstack-node-sdk'); + 2. Add the \`await percy.screenshot(driver, name)\` method at required points in your test script. + +Example: +\`\`\`javascript + const { percy } = require('browserstack-node-sdk'); + 2. Add the \`await percy.screenshot(driver, name)\` method at required points in your test script. + +Example: +\`\`\`javascript +const { percy } = require('browserstack-node-sdk'); +describe("WebdriverIO Test", () => { + it("my test", async () => { + // .... + await percy.screenshot(driver, "My Screenshot") + // .... + }); +}); +\`\`\` + +---STEP--- +Run your test script + - Use the commands defined in your package.json file to run the tests on BrowserStack. + +${percyAutomateReviewSnapshotsStep} +`; + +export const testcafePercyAutomateInstructions = ` +Install Percy dependencies + - Install the required dependencies: + npm install --save-dev @percy/cli @percy/testcafe + +---STEP--- +Update your test script +${PERCY_SNAPSHOT_INSTRUCTION} + - Import the Percy library and use the percySnapshot function to take screenshots. + +Example: +\`\`\`javascript +import percySnapshot from '@percy/testcafe'; +fixture('MyFixture') + .page('https://devexpress.github.io/testcafe/example/'); +test('Test1', async t => { + await t.typeText('#developer-name', 'John Doe'); + await percySnapshot(t, 'TestCafe Example'); +}); +\`\`\` + +---STEP--- +Run Percy + - Use the following command to run your tests with Percy: + npx percy exec -- testcafe chrome:headless tests + +${percyAutomateReviewSnapshotsStep} +`; + +// Java Playwright Percy Automate Instructions +export const javaPlaywrightJunitInstructions = ` +Install Percy Automate dependencies + - Install the latest Percy CLI: + npm install --save @percy/cli + - Add the Percy Playwright Java SDK to your pom.xml: +\`\`\`xml + + io.percy + percy-playwright-java + 1.0.0 + +\`\`\` + +---STEP--- +Update your Automate test script + - Import the Percy library: + import io.percy.playwright.Percy; + - Use the Percy screenshot command to take required screenshots in your Automate session. + +Example: +\`\`\`java +Percy percy = new Percy(page); +percy.screenshot("screenshot_1"); +// With options +percy.screenshot("screenshot_2", options); +\`\`\` + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- + +${percyAutomateReviewSnapshotsStep} +`; + +// C# Playwright NUnit Percy Automate Instructions +export const csharpPlaywrightNunitInstructions = ` +Install Percy Automate dependencies + - Install the latest Percy CLI: + npm install --save @percy/cli + - Add the Percy Playwright SDK to your .csproj file: +\`\`\`xml + +\`\`\` + +---STEP--- +Update your NUnit Playwright test script + - Import the Percy library: + using PercyIO.Playwright; + - Use the Percy screenshot command to take required screenshots in your Automate session. + +Example: +\`\`\`csharp +using PercyIO.Playwright; +Percy.Screenshot(page, "example_screenshot_1"); +// With options +Percy.Screenshot(page, "example_screenshot_2", options); +\`\`\` + +---STEP--- +Run Percy Automate with your tests + - Use the following command: + npx percy exec -- + +${percyAutomateReviewSnapshotsStep} +`; diff --git a/src/tools/sdk-utils/percy-automate/frameworks.ts b/src/tools/sdk-utils/percy-automate/frameworks.ts new file mode 100644 index 00000000..50ba8444 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/frameworks.ts @@ -0,0 +1,56 @@ +import { ConfigMapping } from "./types.js"; +import * as instructions from "./constants.js"; + +export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { + python: { + selenium: { + pytest: { + instructions: instructions.pythonPytestSeleniumInstructions, + }, + }, + playwright: { + pytest: { + instructions: instructions.pythonPytestPlaywrightInstructions, + }, + }, + }, + java: { + playwright: { + junit: { instructions: instructions.javaPlaywrightJunitInstructions }, + }, + }, + nodejs: { + selenium: { + mocha: { instructions: instructions.mochaPercyAutomateInstructions }, + jest: { instructions: instructions.jestPercyAutomateInstructions }, + webdriverio: { + instructions: instructions.webdriverioPercyAutomateInstructions, + }, + testcafe: { + instructions: instructions.testcafePercyAutomateInstructions, + }, + }, + playwright: { + mocha: { instructions: instructions.mochaPercyPlaywrightInstructions }, + jest: { instructions: instructions.jestPercyAutomateInstructions }, + }, + }, +}; + +/** + * Utility function to check if a given language, driver, and testing framework + * are supported by Percy Automate. + * This now expects the structure: language -> driver -> framework + */ +export function isPercyAutomateFrameworkSupported( + language: string, + driver: string, + framework: string, +): boolean { + const languageConfig = + SUPPORTED_CONFIGURATIONS[language as keyof typeof SUPPORTED_CONFIGURATIONS]; + if (!languageConfig) return false; + const driverConfig = languageConfig[driver as keyof typeof languageConfig]; + if (!driverConfig) return false; + return !!driverConfig[framework as keyof typeof driverConfig]; +} diff --git a/src/tools/sdk-utils/percy-automate/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts new file mode 100644 index 00000000..16296e7b --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -0,0 +1,43 @@ +import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; +import { SetUpPercyInput } from "../common/schema.js"; +import { SUPPORTED_CONFIGURATIONS } from "./frameworks.js"; +import { SDKSupportedLanguage } from "../common/types.js"; + +export function runPercyAutomateOnly( + input: SetUpPercyInput, + percyToken: string, +): RunTestsInstructionResult { + const steps: RunTestsStep[] = []; + + // Assume configuration is supported due to guardrails at orchestration layer + const languageConfig = + SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; + const driverConfig = languageConfig[input.detectedBrowserAutomationFramework]; + const testingFrameworkConfig = driverConfig + ? driverConfig[input.detectedTestingFramework] + : undefined; + + // Generate instructions for the supported configuration with project name + const instructions = testingFrameworkConfig + ? testingFrameworkConfig.instructions + : ""; + + // Prepend a step to set the Percy token in the environment + steps.push({ + type: "instruction", + title: "Set Percy Token in Environment", + content: `---STEP---Set the environment variable generated for your project before running your tests:\n\nexport PERCY_TOKEN="${percyToken}"\n\n(For Windows, use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)---STEP---`, + }); + + steps.push({ + type: "instruction", + title: `Percy Automate Setup for ${input.detectedLanguage} with ${input.detectedTestingFramework}`, + content: instructions, + }); + + return { + steps, + requiresPercy: true, + missingDependencies: [], + }; +} diff --git a/src/tools/sdk-utils/percy-automate/index.ts b/src/tools/sdk-utils/percy-automate/index.ts new file mode 100644 index 00000000..230bfcb5 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/index.ts @@ -0,0 +1,2 @@ +// Percy Automate utilities +export { runPercyAutomateOnly } from "./handler.js"; diff --git a/src/tools/sdk-utils/percy-automate/types.ts b/src/tools/sdk-utils/percy-automate/types.ts new file mode 100644 index 00000000..c1860f80 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/types.ts @@ -0,0 +1,13 @@ +/** + * Type for Percy Automate configuration mapping. + * Structure: language -> driver -> testingFramework -> { instructions: string } + */ +export type ConfigMapping = { + [language: string]: { + [driver: string]: { + [framework: string]: { + instructions: string; + }; + }; + }; +}; diff --git a/src/tools/sdk-utils/percy/constants.ts b/src/tools/sdk-utils/percy-bstack/constants.ts similarity index 72% rename from src/tools/sdk-utils/percy/constants.ts rename to src/tools/sdk-utils/percy-bstack/constants.ts index 39dea6a1..7f9da914 100644 --- a/src/tools/sdk-utils/percy/constants.ts +++ b/src/tools/sdk-utils/percy-bstack/constants.ts @@ -1,10 +1,10 @@ -import { PercyConfigMapping } from "./types.js"; +import { PERCY_SNAPSHOT_INSTRUCTION } from "../common/constants.js"; -const javaSeleniumInstructions = ` +export const javaSeleniumInstructions = ` Import the BrowserStack Percy SDK in your test script: Add the Percy import to your test file. ----STEP--- +${PERCY_SNAPSHOT_INSTRUCTION} Add screenshot capture method at required points: Use the \`PercySDK.screenshot(driver, name)\` method at points in your test script where you want to capture screenshots. @@ -35,6 +35,8 @@ export const nodejsSeleniumInstructions = ` Import the BrowserStack Percy SDK in your test script: Add the Percy import to your test file. +${PERCY_SNAPSHOT_INSTRUCTION} + ---STEP--- Add screenshot capture method at required points: @@ -47,20 +49,20 @@ describe("sample Test", () => { test("my test", async () => { // .... - await percy.snapshot(driver, "My Snapshot") + await percy.screenshot(driver, "My Snapshot") // .... }); }) \`\`\` `; -const webdriverioPercyInstructions = ` +export const webdriverioPercyInstructions = ` Enable Percy in \`wdio.conf.js\`: In your WebdriverIO configuration file, modify the 'browserstack' service options to enable Percy. - Set \`percy: true\`. - Set a \`projectName\`. This is required and will be used for both your Automate and Percy projects. -- Set \`percyCaptureMode\`. The default \`auto\` mode is recommended, which captures screenshots on events like clicks. Other modes are \`testcase\`, \`click\`, \`screenshot\`, and \`manual\`. +- Set \`percyCaptureMode\`. The default \`manual\` as we are adding screenshot commands manually. Here's how to modify the service configuration: \`\`\`javascript @@ -74,7 +76,7 @@ exports.config = { { // ... other service options percy: true, - percyCaptureMode: 'auto' // or 'manual', 'testcase', etc. + percyCaptureMode: 'manual' // or 'auto', etc. }, ], ], @@ -89,6 +91,8 @@ exports.config = { }; \`\`\` +${PERCY_SNAPSHOT_INSTRUCTION} + ---STEP--- Manually Capturing Screenshots (Optional): @@ -117,11 +121,11 @@ describe("My WebdriverIO Test", () => { \`\`\` `; -const csharpSeleniumInstructions = ` +export const csharpSeleniumInstructions = ` Import the BrowserStack Percy SDK in your test script: Add the Percy import to your test file. ----STEP--- +${PERCY_SNAPSHOT_INSTRUCTION} Add screenshot capture method at required points: Use the \`PercySDK.Screenshot(driver, name)\` method at points in your test script where you want to capture screenshots. @@ -130,8 +134,6 @@ Here's an example: \`\`\`csharp using BrowserStackSDK.Percy; -using NUnit.Framework; - namespace Tests; public class MyTest @@ -151,33 +153,3 @@ public class MyTest } \`\`\` `; - -export const PERCY_INSTRUCTIONS: PercyConfigMapping = { - java: { - selenium: { - testng: { script_updates: javaSeleniumInstructions }, - cucumber: { script_updates: javaSeleniumInstructions }, - junit4: { script_updates: javaSeleniumInstructions }, - junit5: { script_updates: javaSeleniumInstructions }, - serenity: { script_updates: javaSeleniumInstructions }, - }, - }, - csharp: { - selenium: { - nunit: { script_updates: csharpSeleniumInstructions }, - }, - }, - nodejs: { - selenium: { - mocha: { - script_updates: nodejsSeleniumInstructions, - }, - jest: { - script_updates: nodejsSeleniumInstructions, - }, - webdriverio: { - script_updates: webdriverioPercyInstructions, - }, - }, - }, -}; diff --git a/src/tools/sdk-utils/percy-bstack/frameworks.ts b/src/tools/sdk-utils/percy-bstack/frameworks.ts new file mode 100644 index 00000000..ed51557e --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/frameworks.ts @@ -0,0 +1,29 @@ +import { ConfigMapping } from "./types.js"; +import * as constants from "./constants.js"; + +export const PERCY_INSTRUCTIONS: ConfigMapping = { + java: { + selenium: { + testng: { instructions: constants.javaSeleniumInstructions }, + cucumber: { instructions: constants.javaSeleniumInstructions }, + junit4: { instructions: constants.javaSeleniumInstructions }, + junit5: { instructions: constants.javaSeleniumInstructions }, + selenide: { instructions: constants.javaSeleniumInstructions }, + jbehave: { instructions: constants.javaSeleniumInstructions }, + }, + }, + csharp: { + selenium: { + nunit: { instructions: constants.csharpSeleniumInstructions }, + xunit: { instructions: constants.csharpSeleniumInstructions }, + specflow: { instructions: constants.csharpSeleniumInstructions }, + }, + }, + nodejs: { + selenium: { + mocha: { instructions: constants.nodejsSeleniumInstructions }, + jest: { instructions: constants.nodejsSeleniumInstructions }, + webdriverio: { instructions: constants.webdriverioPercyInstructions }, + }, + }, +}; diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts new file mode 100644 index 00000000..e0711779 --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -0,0 +1,158 @@ +// Percy + BrowserStack SDK combined handler +import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; +import { RunTestsOnBrowserStackInput } from "../common/schema.js"; +import { getBrowserStackAuth } from "../../../lib/get-auth.js"; +import { getSDKPrefixCommand } from "../bstack/commands.js"; +import { generateBrowserStackYMLInstructions } from "../bstack/configUtils.js"; +import { getInstructionsForProjectConfiguration } from "../common/instructionUtils.js"; +import { + formatPercyInstructions, + getPercyInstructions, +} from "./instructions.js"; +import { BrowserStackConfig } from "../../../lib/types.js"; +import { + SDKSupportedBrowserAutomationFramework, + SDKSupportedTestingFramework, + SDKSupportedLanguage, +} from "../common/types.js"; + +export function runPercyWithBrowserstackSDK( + input: RunTestsOnBrowserStackInput, + config: BrowserStackConfig, +): RunTestsInstructionResult { + const steps: RunTestsStep[] = []; + const authString = getBrowserStackAuth(config); + const [username, accessKey] = authString.split(":"); + + // Check if Percy is supported for this configuration + const percyResult = getPercyInstructions( + input.detectedLanguage as SDKSupportedLanguage, + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, + input.detectedTestingFramework as SDKSupportedTestingFramework, + ); + + if (!percyResult) { + // Percy not supported for this configuration + return { + steps: [ + { + type: "error", + title: "Percy Not Supported", + content: `Percy is not supported for this ${input.detectedBrowserAutomationFramework} framework configuration. Please use BrowserStack SDK only mode or try a different framework combination.`, + isError: true, + }, + ], + requiresPercy: true, + shouldSkipFormatting: true, + missingDependencies: [], + }; + } + + // Handle frameworks with unique setup instructions that don't use browserstack.yml + if ( + input.detectedBrowserAutomationFramework === "cypress" || + input.detectedTestingFramework === "webdriverio" + ) { + const frameworkInstructions = getInstructionsForProjectConfiguration( + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, + input.detectedTestingFramework as SDKSupportedTestingFramework, + input.detectedLanguage as SDKSupportedLanguage, + username, + accessKey, + ); + + if (frameworkInstructions && frameworkInstructions.setup) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions.setup, + }); + } + + steps.push({ + type: "instruction", + title: "Percy Setup (BrowserStack SDK + Percy)", + content: formatPercyInstructions(percyResult), + }); + + if (frameworkInstructions && frameworkInstructions.run) { + steps.push({ + type: "instruction", + title: "Run the tests", + content: frameworkInstructions.run, + }); + } + + return { + steps, + requiresPercy: true, + missingDependencies: [], + }; + } + + // Default flow using browserstack.yml with Percy + const sdkSetupCommand = getSDKPrefixCommand( + input.detectedLanguage as SDKSupportedLanguage, + input.detectedTestingFramework as SDKSupportedTestingFramework, + username, + accessKey, + ); + + if (sdkSetupCommand) { + steps.push({ + type: "instruction", + title: "Install BrowserStack SDK", + content: sdkSetupCommand, + }); + } + + const ymlInstructions = generateBrowserStackYMLInstructions( + input.desiredPlatforms as string[], + true, + input.projectName, + ); + + if (ymlInstructions) { + steps.push({ + type: "instruction", + title: "Configure browserstack.yml", + content: ymlInstructions, + }); + } + + const frameworkInstructions = getInstructionsForProjectConfiguration( + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, + input.detectedTestingFramework as SDKSupportedTestingFramework, + input.detectedLanguage as SDKSupportedLanguage, + username, + accessKey, + ); + + if (frameworkInstructions && frameworkInstructions.setup) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions.setup, + }); + } + + steps.push({ + type: "instruction", + title: "Percy Setup (BrowserStack SDK + Percy)", + content: formatPercyInstructions(percyResult), + }); + + if (frameworkInstructions && frameworkInstructions.run) { + steps.push({ + type: "instruction", + title: "Run the tests", + content: frameworkInstructions.run, + }); + } + + return { + steps, + requiresPercy: true, + missingDependencies: [], + }; +} diff --git a/src/tools/sdk-utils/percy-bstack/index.ts b/src/tools/sdk-utils/percy-bstack/index.ts new file mode 100644 index 00000000..6338ed2e --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/index.ts @@ -0,0 +1,8 @@ +// Percy + BrowserStack SDK utilities +export { runPercyWithBrowserstackSDK } from "./handler.js"; +export { + getPercyInstructions, + formatPercyInstructions, +} from "./instructions.js"; +export { PERCY_INSTRUCTIONS } from "./frameworks.js"; +export type { ConfigMapping } from "./types.js"; diff --git a/src/tools/sdk-utils/percy/instructions.ts b/src/tools/sdk-utils/percy-bstack/instructions.ts similarity index 60% rename from src/tools/sdk-utils/percy/instructions.ts rename to src/tools/sdk-utils/percy-bstack/instructions.ts index f642efa5..dab7941b 100644 --- a/src/tools/sdk-utils/percy/instructions.ts +++ b/src/tools/sdk-utils/percy-bstack/instructions.ts @@ -1,19 +1,17 @@ +// Percy + BrowserStack SDK instructions and utilities import { SDKSupportedBrowserAutomationFramework, SDKSupportedLanguage, SDKSupportedTestingFramework, -} from "../types.js"; -import { PERCY_INSTRUCTIONS } from "./constants.js"; -import { PercyInstructions } from "./types.js"; +} from "../common/types.js"; +import { PERCY_INSTRUCTIONS } from "./frameworks.js"; -/** - * Retrieves Percy-specific instructions for a given language and framework. - */ +// Retrieves Percy-specific instructions for a given language and framework export function getPercyInstructions( language: SDKSupportedLanguage, automationFramework: SDKSupportedBrowserAutomationFramework, testingFramework: SDKSupportedTestingFramework, -): PercyInstructions | null { +): { instructions: string } | null { const langConfig = PERCY_INSTRUCTIONS[language]; if (!langConfig) { return null; @@ -32,14 +30,12 @@ export function getPercyInstructions( return percyInstructions; } -/** - * Formats the retrieved Percy instructions into a user-friendly string. - */ -export function formatPercyInstructions( - instructions: PercyInstructions, -): string { - return `\n\n## Percy Visual Testing Setup +// Formats the retrieved Percy instructions into a user-friendly string +export function formatPercyInstructions(instructions: { + instructions: string; +}): string { + return `---STEP--- Percy Visual Testing Setup To enable visual testing with Percy, you need to make the following changes to your project configuration and test scripts. -${instructions.script_updates} +${instructions.instructions} `; } diff --git a/src/tools/sdk-utils/percy-bstack/types.ts b/src/tools/sdk-utils/percy-bstack/types.ts new file mode 100644 index 00000000..b45f9628 --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/types.ts @@ -0,0 +1,14 @@ +/** + * Type for Percy + BrowserStack SDK configuration mapping. + * Structure: language -> automationFramework -> testingFramework -> { instructions: (bsdkToken: string) => string } + */ + +export type ConfigMapping = { + [language: string]: { + [automationFramework: string]: { + [testingFramework: string]: { + instructions: string; + }; + }; + }; +}; diff --git a/src/tools/sdk-utils/percy-web/constants.ts b/src/tools/sdk-utils/percy-web/constants.ts new file mode 100644 index 00000000..307bf4c4 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -0,0 +1,981 @@ +import { PERCY_SNAPSHOT_INSTRUCTION } from "../common/constants.js"; +export const percyReviewSnapshotsStep = ` +---STEP--- +Review the snapshots + - Go to your Percy project on https://percy.io to review snapshots and approve/reject any visual changes. +`; + +export const pythonInstructionsSnapshot = ` +Example: +\`\`\`python +- Import the Percy snapshot helper: +from selenium import webdriver +from percy import percy_snapshot + +driver = webdriver.Chrome() +driver.get('http://localhost:8000') +percy_snapshot(driver, 'Home page') +# ... more test steps ... +percy_snapshot(driver, 'After login') +\`\`\` +`; + +export const nodejsInstructionsSnapshot = ` +- Import the Percy snapshot helper: + const { percySnapshot } = require('@percy/selenium-js'); + - In your test, take snapshots like this: + await percySnapshot(driver, "Your snapshot name"); + +Example: +\`\`\`javascript +const { Builder } = require('selenium-webdriver'); +const percySnapshot = require('@percy/selenium-webdriver'); + +const driver = await new Builder().forBrowser('chrome').build(); +await driver.get('http://localhost:8000'); +await percySnapshot(driver, 'Home page'); +\`\`\` +`; + +export const javaInstructionsSnapshot = ` + - Import the Percy snapshot helper: + import io.percy.selenium.Percy; + - In your test, take snapshots like this: + Percy percy = new Percy(driver); + percy.snapshot("Your snapshot name"); + Example: +\`\`\`java +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import io.percy.selenium.Percy; + +public class PercyExample { + public static void main(String[] args) { + WebDriver driver = new ChromeDriver(); + driver.get("http://localhost:8000"); + Percy percy = new Percy(driver); + percy.snapshot("Home page"); + driver.quit(); + } +} +\`\`\``; + +export const rubyInstructionsSnapshot = ` + - Require the Percy snapshot helper: + require 'percy' + - In your test, take snapshots like this: + Percy.snapshot(page, 'Your snapshot name') + +Example: +\`\`\`ruby +require 'selenium-webdriver' +require 'percy' + +driver = Selenium::WebDriver.for :chrome +driver.get('http://localhost:8000') +Percy.snapshot(driver, 'Your snapshot name') +driver.quit +\`\`\` +`; + +export const rubyCapybaraInstructionsSnapshot = ` + - In your test setup file, require percy/capybara: + require 'percy/capybara' + - In your test, take snapshots like this: + page.percy_snapshot('Capybara snapshot') + +Example: +\`\`\`ruby +require 'percy/capybara' + +describe 'my feature', type: :feature do + it 'renders the page' do + visit 'https://example.com' + page.percy_snapshot('Capybara snapshot') + end +end +\`\`\` + + - The snapshot method arguments are: + page.percy_snapshot(name[, options]) + name - The snapshot name; must be unique to each snapshot; defaults to the test title + options - See per-snapshot configuration options +`; + +export const csharpInstructionsSnapshot = ` + - Import the Percy snapshot helper: + using PercyIO.Selenium; + - In your test, take snapshots like this: + Percy.Snapshot(driver,"Your snapshot name"); + +Example: +\`\`\`csharp +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using PercyIO.Selenium; + +class PercyExample +{ + static void Main() + { + IWebDriver driver = new ChromeDriver(); + driver.Navigate().GoToUrl("http://localhost:8000"); + Percy.Snapshot(driver,"Empty Todo State"); + driver.Quit(); + } +} +\`\`\` +`; + +export const javaPlaywrightInstructionsSnapshot = ` + - Import the Percy library and use the snapshot method: + percy.snapshot("snapshot_1"); + - You can also pass options: + Map options = new HashMap<>(); + options.put("testCase", "Should add product to cart"); + percy.snapshot("snapshot_2", options); + +Example: +\`\`\`java +import com.microsoft.playwright.*; +import io.percy.playwright.*; + +public class PercyPlaywrightExample { + public static void main(String[] args) { + try (Playwright playwright = Playwright.create()) { + Browser browser = playwright.chromium().launch(); + Page page = browser.newPage(); + Percy percy = new Percy(page); + + page.navigate("http://localhost:8000"); + percy.snapshot("Home page"); + + // ... more test steps ... + percy.snapshot("After login"); + + browser.close(); + } + } +} +\`\`\` +`; + +export const nodejsPlaywrightInstructionsSnapshot = ` + - Import the Percy snapshot helper: + const percySnapshot = require('@percy/playwright'); + - In your test, take snapshots like this: + await percySnapshot(page, "Your snapshot name"); + +Example: +\`\`\`javascript +const { chromium } = require('playwright'); +const percySnapshot = require('@percy/playwright'); + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto('http://example.com/', { waitUntil: 'networkidle' }); + await percySnapshot(page, 'Example Site'); + await browser.close(); +})(); +\`\`\` +`; + +export const nodejsWebdriverioInstructionsSnapshot = ` + - Import the Percy snapshot helper: + const percySnapshot = require('@percy/selenium-webdriver'); + - In your test, take snapshots like this: + await percySnapshot(driver, "Your snapshot name"); + +Example: +\`\`\`javascript +const { remote } = require('webdriverio'); +const percySnapshot = require('@percy/selenium-webdriver'); + +(async () => { + const browser = await remote({ + logLevel: 'error', + capabilities: { browserName: 'chrome' } + }); + + await browser.url('https://example.com'); + await percySnapshot(browser, 'WebdriverIO example'); + await browser.deleteSession(); +})(); +\`\`\` +`; + +export const nodejsEmberInstructionsSnapshot = ` + - Import the Percy snapshot helper: + import percySnapshot from '@percy/ember'; + - In your test, take snapshots like this: + await percySnapshot('My Snapshot'); + +Example: +\`\`\`javascript +import percySnapshot from '@percy/ember'; +describe('My ppp', () => { + // ...app setup + it('about page should look good', async () => { + await visit('/about'); + await percySnapshot('My Snapshot'); + }); +}); +\`\`\` + + - The snapshot method arguments are: + percySnapshot(name[, options]) + name - The snapshot name; must be unique to each snapshot; defaults to the test title + options - See per-snapshot configuration options +`; + +export const nodejsCypressInstructionsSnapshot = ` + - Import the Percy snapshot helper in your cypress/support/e2e.js file: + import '@percy/cypress'; + - If you’re using TypeScript, include "types": ["cypress", "@percy/cypress"] in your tsconfig.json file. + - In your test, take snapshots like this: + cy.percySnapshot(); + +Example: +\`\`\`javascript +import '@percy/cypress'; + +describe('Integration test with visual testing', function() { + it('Loads the homepage', function() { + // Load the page or perform any other interactions with the app. + cy.visit(''); + // Take a snapshot for visual diffing + cy.percySnapshot(); + }); +}); +\`\`\` + + - The snapshot method arguments are: + cy.percySnapshot([name][, options]) + name - The snapshot name; must be unique to each snapshot; defaults to the test title + options - See per-snapshot configuration options + + - For example: + cy.percySnapshot(); + cy.percySnapshot('Homepage test'); + cy.percySnapshot('Homepage responsive test', { widths: [768, 992, 1200] }); +`; + +export const nodejsPuppeteerInstructionsSnapshot = ` + - Import the Percy snapshot helper: + const percySnapshot = require('@percy/puppeteer'); + - In your test, take snapshots like this: + await percySnapshot(page, 'Snapshot name'); + +Example: +\`\`\`javascript +const puppeteer = require('puppeteer'); +const percySnapshot = require('@percy/puppeteer'); + +describe('Integration test with visual testing', function() { + it('Loads the homepage', async function() { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await percySnapshot(page, this.test.fullTitle()); + await browser.close(); + }); +}); +\`\`\` + + - The snapshot method arguments are: + percySnapshot(page, name[, options]) + page (required) - A puppeteer page instance + name (required) - The snapshot name; must be unique to each snapshot + options - See per-snapshot configuration options + + - For example: + percySnapshot(page, 'Homepage test'); + percySnapshot(page, 'Homepage responsive test', { widths: [768, 992, 1200] }); +`; + +export const nodejsNightmareInstructionsSnapshot = ` + - Import the Percy snapshot helper: + const Nightmare = require('nightmare'); + const percySnapshot = require('@percy/nightmare'); + - In your test, take snapshots like this: + .use(percySnapshot('Snapshot name')) + +Example: +\`\`\`javascript +const Nightmare = require('nightmare'); +const percySnapshot = require('@percy/nightmare'); + +Nightmare() + .goto('http://example.com') + // ... other actions ... + .use(percySnapshot('Example Snapshot')) + // ... more actions ... + .end() + .then(() => { + // ... + }); +\`\`\` + + - The snapshot method arguments are: + percySnapshot(name[, options]) + name (required) - The snapshot name; must be unique to each snapshot + options - See per-snapshot configuration options +`; + +export const nodejsNightwatchInstructionsSnapshot = ` + - Import the Percy library and add the path exported by @percy/nightwatch to your Nightwatch configuration’s custom_commands_path property: + const percy = require('@percy/nightwatch'); + module.exports = { + // ... + custom_commands_path: [percy.path], + // ... + }; + - In your test, take snapshots like this: + browser.percySnapshot('Snapshot name'); + +Example: +\`\`\`javascript +const percy = require('@percy/nightwatch'); +module.exports = { + // ... + custom_commands_path: [percy.path], + // ... +}; + +// Example test +module.exports = { + 'Snapshots pages': function(browser) { + browser + .url('http://example.com') + .assert.containsText('h1', 'Example Domain') + .percySnapshot('Example snapshot'); + browser + .url('http://google.com') + .assert.elementPresent('img[alt="Google"]') + .percySnapshot('Google homepage'); + browser.end(); + } +}; +\`\`\` + + - The snapshot method arguments are: + percySnapshot([name][, options]) + name (required) - The snapshot name; must be unique to each snapshot + options - See per-snapshot configuration options +`; + +export const nodejsProtractorInstructionsSnapshot = ` + - Import the Percy snapshot helper: + import percySnapshot from '@percy/protractor'; + - In your test, take snapshots like this: + await percySnapshot('Snapshot name'); + // or + await percySnapshot(browser, 'Snapshot name'); + +Example: +\`\`\`javascript +import percySnapshot from '@percy/protractor'; +describe('angularjs homepage', function() { + it('should greet the named user', async function() { + await browser.get('https://www.angularjs.org'); + await percySnapshot('AngularJS homepage'); + await element(by.model('yourName')).sendKeys('Percy'); + var greeting = element(by.binding('yourName')); + expect(await greeting.getText()).toEqual('Hello Percy!'); + await percySnapshot('AngularJS homepage greeting'); + }); +}); +\`\`\` + + - The snapshot method arguments are: + percySnapshot(name[, options]) + Standalone mode: + percySnapshot(browser, name[, options]) + browser (required) - The Protractor browser object + name (required) - The snapshot name; must be unique to each snapshot + options - See per-snapshot configuration options +`; + +export const nodejsTestcafeInstructionsSnapshot = ` + - Import the Percy snapshot helper: + import percySnapshot from '@percy/testcafe'; + - In your test, take snapshots like this: + await percySnapshot(t, 'Snapshot name'); + +Example: +\`\`\`javascript +import percySnapshot from '@percy/testcafe'; +fixture('MyFixture') + .page('https://devexpress.github.io/testcafe/example'); +test('Test1', async t => { + await t.typeText('#developer-name', 'John Doe'); + await percySnapshot(t, 'TestCafe Example'); +}); +\`\`\` + + - The snapshot method arguments are: + percySnapshot(t, name[, options]) + t (required) - The test controller instance passed from test + name (required) - The snapshot name; must be unique to each snapshot + options - See per-snapshot configuration options +`; + +export const nodejsGatsbyInstructionsSnapshot = ` + - Add the Percy plugin to your gatsby-config.js file: + module.exports = { + plugins: [\`gatsby-plugin-percy\`] + } + + - The plugin will take snapshots of discovered pages during the build process. + + - Example gatsby-config.js with options: +\`\`\`javascript +module.exports = { + plugins: [{ + resolve: \`gatsby-plugin-percy\`, + options: { + // gatsby specific options + query: \`{ + allSitePage { nodes { path } } + allOtherPage { nodes { path } } + }\`, + resolvePages: ({ + allSitePage: { nodes: allPages }, + allOtherPage: { nodes: otherPages } + }) => { + return [...allPages, ...otherPages] + .map(({ path }) => path); + }, + // percy static snapshot options + exclude: [ + '/dev-404-page/', + '/offline-plugin-app-shell-fallback/' + ], + overrides: [{ + include: '/foobar/', + waitForSelector: '.done-loading', + additionalSnapshots: [{ + suffix: ' - after btn click', + execute: () => document.querySelector('.btn').click() + }] + }] + } + }] +} +\`\`\` +`; + +export const nodejsStorybookInstructionsSnapshot = ` + - Add Percy parameters to your stories to customize snapshots: +\`\`\`js +MyStory.parameters = { + percy: { + name: 'My snapshot', + additionalSnapshots: [ + { prefix: '[Dark mode] ', args: { colorScheme: 'dark' } }, + { suffix: ' with globals', globals: { textDirection: 'rtl' } }, + { name: 'Search snapshot', queryParams: { search: 'foobar' } } + ] + } +}; +\`\`\` + - Use argument names and values defined in your codebase. +`; + +export const pythonPlaywrightInstructionsSnapshot = ` + - Import the Percy snapshot helper and use the snapshot method: + percy_snapshot(page, name="Your snapshot name") + - You can also use: + percy_screenshot(page, name="Your snapshot name", options={}) + +Example: +\`\`\`python +from playwright.sync_api import sync_playwright +from percy import percy_snapshot + +with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + page.goto("http://localhost:8000") + percy_snapshot(page, name="Home page") + + # ... more test steps ... + percy_snapshot(page, name="After login") + + browser.close() +\`\`\` +`; + +export const csharpPlaywrightInstructionsSnapshot = ` + - Import the Percy snapshot helper and use the snapshot method: + Percy.Snapshot(page, "Your snapshot name"); + - You can also pass options: + Percy.Snapshot(page, "Your snapshot name", options); + +Example: +\`\`\`csharp +using Microsoft.Playwright; +using PercyIO.Playwright; + +class PercyPlaywrightExample +{ + public static async Task Main() + { + using var playwright = await Playwright.CreateAsync(); + var browser = await playwright.Chromium.LaunchAsync(); + var page = await browser.NewPageAsync(); + + await page.GotoAsync("http://localhost:8000"); + Percy.Snapshot(page, "Home page"); + + // ... more test steps ... + Percy.Snapshot(page, "After login"); + + await browser.CloseAsync(); + } +} +\`\`\` +`; + +export const pythonInstructions = ` +Install Percy dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy Selenium Python package: + pip install percy-selenium + +Update your Python Selenium script +${PERCY_SNAPSHOT_INSTRUCTION} +${pythonInstructionsSnapshot} + +Run Percy with your tests + - Use the following command: + npx percy exec -- + +Example output: + [percy] Percy has started! + [percy] Created build #1: https://percy.io/your-project + [percy] Snapshot taken "Home page" + [percy] Finalized build #1: https://percy.io/your-project + [percy] Done! + +${percyReviewSnapshotsStep} +`; + +export const nodejsInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy SDK for Node.js: + npm install @percy/selenium-webdriver +---STEP--- +Update your Node.js Selenium script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- node scripts/test.js + +${percyReviewSnapshotsStep} +`; + +export const javaInstructions = ` +---STEP--- +Add Percy dependencies to your project + - For Maven, add to your pom.xml: + + io.percy + percy-java-selenium + 1.0.0 + + - For Gradle, add to your build.gradle: + implementation 'io.percy:percy-java-selenium:1.0.0' + - For CLI usage, install Percy CLI: + npm install --save-dev @percy/cli + +---STEP--- +Update your Java Selenium test +${PERCY_SNAPSHOT_INSTRUCTION} +${javaInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- mvn test + +${percyReviewSnapshotsStep} +`; + +export const rubyInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy Ruby Selenium gem: + gem install percy-selenium + +---STEP--- +Update your Ruby Selenium test +${PERCY_SNAPSHOT_INSTRUCTION} +${rubyInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- + +${percyReviewSnapshotsStep} +`; + +// Percy Capybara instructions for Ruby +export const rubyCapybaraInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy Capybara gem: + gem install percy-capybara + +---STEP--- +Update your Capybara or Rails test script +${PERCY_SNAPSHOT_INSTRUCTION} +${rubyCapybaraInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- bundle exec rspec + +${percyReviewSnapshotsStep} +`; + +export const csharpInstructions = ` +Install Percy CLI by running the following command: +npm install --save-dev @percy/cli + +---STEP--- +Add Percy dependencies to your project + - Add the Percy .NET Selenium NuGet package: + dotnet add package PercyIO.Selenium + +---STEP--- +Update your C# Selenium test +${PERCY_SNAPSHOT_INSTRUCTION} +${csharpInstructionsSnapshot} + +Run Percy with your tests + - Use the following command: + npx percy exec -- + +${percyReviewSnapshotsStep} +`; + +export const javaPlaywrightInstructions = ` +Install Percy dependencies + - For Maven, add to your pom.xml: + + io.percy + percy-playwright-java + 1.0.0 + + +---STEP--- +Update your Java Playwright test +${PERCY_SNAPSHOT_INSTRUCTION} +${javaPlaywrightInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- + +${percyReviewSnapshotsStep} +`; + +export const nodejsPlaywrightInstructions = ` +Install Percy dependencies + - Install Percy Playwright SDK: + npm install @percy/playwright + +---STEP--- +Update your Playwright JavaScript test +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsPlaywrightInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- +${percyReviewSnapshotsStep} +`; + +// Percy WebdriverIO instructions for JavaScript +export const nodejsWebdriverioInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI: + npm install --save-dev @percy/cli + - Install Percy Selenium Webdriver package: + npm install --save-dev @percy/selenium-webdriver + +---STEP--- +Update your WebdriverIO test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsWebdriverioInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- wdio run wdio.conf.js + +${percyReviewSnapshotsStep} +`; + +// Percy Ember instructions for JavaScript +export const nodejsEmberInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Ember SDK: + npm install --save-dev @percy/cli @percy/ember + +---STEP--- +Update your Ember test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsEmberInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- ember test + +${percyReviewSnapshotsStep} +`; + +// Percy Cypress instructions for JavaScript +export const nodejsCypressInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Cypress SDK: + npm install --save-dev @percy/cli @percy/cypress + +---STEP--- +Update your Cypress test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsCypressInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- cypress run + +${percyReviewSnapshotsStep} +`; + +// Percy Puppeteer instructions for JavaScript +export const nodejsPuppeteerInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Puppeteer SDK: + npm install --save-dev @percy/cli @percy/puppeteer + +---STEP--- +Update your Puppeteer test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsPuppeteerInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- mocha + +${percyReviewSnapshotsStep} +`; + +// Percy Nightmare instructions for JavaScript +export const nodejsNightmareInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Nightmare SDK: + npm install --save-dev @percy/cli @percy/nightmare + +---STEP--- +Update your Nightmare test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsNightmareInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- node script.js + +${percyReviewSnapshotsStep} +`; + +// Percy Nightwatch instructions for JavaScript +export const nodejsNightwatchInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Nightwatch SDK: + npm install --save-dev @percy/cli @percy/nightwatch + +---STEP--- +Update your Nightwatch configuration and test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsNightwatchInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- nightwatch + +${percyReviewSnapshotsStep} +`; + +// Percy Protractor instructions for JavaScript +export const nodejsProtractorInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Protractor SDK: + npm install --save-dev @percy/cli @percy/protractor + +---STEP--- +Update your Protractor test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsProtractorInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- protractor conf.js + +${percyReviewSnapshotsStep} +`; + +// Percy TestCafe instructions for JavaScript +export const nodejsTestcafeInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and TestCafe SDK: + npm install --save-dev @percy/cli @percy/testcafe + +---STEP--- +Update your TestCafe test script +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsTestcafeInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- testcafe chrome:headless tests + +${percyReviewSnapshotsStep} +`; + +// Percy Gatsby instructions for JavaScript +export const nodejsGatsbyInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Gatsby plugin: + npm install --save @percy/cli gatsby-plugin-percy + +---STEP--- +Update your Gatsby configuration +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsGatsbyInstructionsSnapshot} + +---STEP--- +Run Percy with your Gatsby build + - Use the following command: + npx percy exec -- gatsby build + +${percyReviewSnapshotsStep} +`; + +// Percy Storybook instructions for JavaScript +export const nodejsStorybookInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Storybook SDK: + npm install --save-dev @percy/cli @percy/storybook + +---STEP--- +Update your Storybook stories +${PERCY_SNAPSHOT_INSTRUCTION} +${nodejsStorybookInstructionsSnapshot} + +---STEP--- +Run Percy with your Storybook + - With a static Storybook build: + percy storybook ./storybook-build + - With a local or live Storybook URL: + percy storybook http://localhost:9009 + percy storybook https://storybook.foobar.com + - Automatically run start-storybook: + percy storybook:start --port=9009 --static-dir=./public + + - Example output: + [percy] Snapshot found: My snapshot + [percy] - url: [...]?id=component--my-story + [percy] Snapshot found: [Dark mode] My snapshot + [percy] - url: [...]?id=component--my-story&args=colorScheme:dark + [percy] Snapshot found: My snapshot with globals + [percy] - url: [...]?id=component--my-story&globals=textDirection:rtl + [percy] Snapshot found: Search snapshot + [percy] - url: [...]?id=component--my-story&search=foobar + +${percyReviewSnapshotsStep} +`; + +export const pythonPlaywrightInstructions = ` +---STEP--- +Create a Percy project + - Sign in to Percy and create a project of type "Web". Name the project and note the generated token. + +---STEP--- +Set the project token as an environment variable + - On macOS/Linux: + export PERCY_TOKEN="" + - On Windows PowerShell: + $env:PERCY_TOKEN="" + - On Windows CMD: + set PERCY_TOKEN= + +---STEP--- +Install Percy dependencies + - Install Percy Playwright SDK: + pip install percy-playwright + +---STEP--- +Update your Playwright Python test +${PERCY_SNAPSHOT_INSTRUCTION} +${pythonPlaywrightInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- python your_test_script.py + +${percyReviewSnapshotsStep} +`; + +export const csharpPlaywrightInstructions = ` +Install Percy dependencies + - Add the Percy Playwright NuGet package: + + +---STEP--- +Update your Playwright .NET test +${PERCY_SNAPSHOT_INSTRUCTION} +${csharpPlaywrightInstructionsSnapshot} + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- dotnet test + +${percyReviewSnapshotsStep} +`; diff --git a/src/tools/sdk-utils/percy-web/fetchPercyToken.ts b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts new file mode 100644 index 00000000..7b0579b6 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts @@ -0,0 +1,45 @@ +import { PercyIntegrationTypeEnum } from "../common/types.js"; + +export async function fetchPercyToken( + projectName: string, + authorization: string, + options: { type?: PercyIntegrationTypeEnum } = {}, +): Promise { + try { + const authHeader = `Basic ${Buffer.from(authorization).toString("base64")}`; + const baseUrl = + "https://api.browserstack.com/api/app_percy/get_project_token"; + const params = new URLSearchParams({ name: projectName }); + + if (options.type) { + params.append("type", options.type); + } + + const url = `${baseUrl}?${params.toString()}`; + + const response = await fetch(url, { + headers: { + Authorization: authHeader, + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Percy token (status: ${response.status})`, + ); + } + + const data = await response.json(); + + if (!data?.token || !data?.success) { + throw new Error( + "Project exists but is likely set up for Automate. Please use a different project name.", + ); + } + + return data.token; + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + throw new Error(`Error retrieving Percy token: ${message}`); + } +} diff --git a/src/tools/sdk-utils/percy-web/frameworks.ts b/src/tools/sdk-utils/percy-web/frameworks.ts new file mode 100644 index 00000000..4484eed9 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/frameworks.ts @@ -0,0 +1,109 @@ +import { ConfigMapping } from "./types.js"; +import * as constants from "./constants.js"; + +export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { + python: { + selenium: { + instructions: constants.pythonInstructions, + snapshotInstruction: constants.pythonInstructionsSnapshot, + }, + playwright: { + instructions: constants.pythonPlaywrightInstructions, + snapshotInstruction: constants.pythonPlaywrightInstructionsSnapshot, + }, + }, + nodejs: { + selenium: { + instructions: constants.nodejsInstructions, + snapshotInstruction: constants.nodejsInstructionsSnapshot, + }, + playwright: { + instructions: constants.nodejsPlaywrightInstructions, + snapshotInstruction: constants.nodejsPlaywrightInstructionsSnapshot, + }, + webdriverio: { + instructions: constants.nodejsWebdriverioInstructions, + snapshotInstruction: constants.nodejsWebdriverioInstructionsSnapshot, + }, + ember: { + instructions: constants.nodejsEmberInstructions, + snapshotInstruction: constants.nodejsEmberInstructionsSnapshot, + }, + cypress: { + instructions: constants.nodejsCypressInstructions, + snapshotInstruction: constants.nodejsCypressInstructionsSnapshot, + }, + puppeteer: { + instructions: constants.nodejsPuppeteerInstructions, + snapshotInstruction: constants.nodejsPuppeteerInstructionsSnapshot, + }, + nightmare: { + instructions: constants.nodejsNightmareInstructions, + snapshotInstruction: constants.nodejsNightmareInstructionsSnapshot, + }, + nightwatch: { + instructions: constants.nodejsNightwatchInstructions, + snapshotInstruction: constants.nodejsNightwatchInstructionsSnapshot, + }, + protractor: { + instructions: constants.nodejsProtractorInstructions, + snapshotInstruction: constants.nodejsProtractorInstructionsSnapshot, + }, + testcafe: { + instructions: constants.nodejsTestcafeInstructions, + snapshotInstruction: constants.nodejsTestcafeInstructionsSnapshot, + }, + gatsby: { + instructions: constants.nodejsGatsbyInstructions, + snapshotInstruction: constants.nodejsGatsbyInstructionsSnapshot, + }, + storybook: { + instructions: constants.nodejsStorybookInstructions, + snapshotInstruction: constants.nodejsStorybookInstructionsSnapshot, + }, + }, + java: { + selenium: { + instructions: constants.javaInstructions, + snapshotInstruction: constants.javaInstructionsSnapshot, + }, + playwright: { + instructions: constants.javaPlaywrightInstructions, + snapshotInstruction: constants.javaPlaywrightInstructionsSnapshot, + }, + }, + ruby: { + selenium: { + instructions: constants.rubyInstructions, + snapshotInstruction: constants.rubyInstructionsSnapshot, + }, + capybara: { + instructions: constants.rubyCapybaraInstructions, + snapshotInstruction: constants.rubyCapybaraInstructionsSnapshot, + }, + }, + csharp: { + selenium: { + instructions: constants.csharpInstructions, + snapshotInstruction: constants.csharpInstructionsSnapshot, + }, + playwright: { + instructions: constants.csharpPlaywrightInstructions, + snapshotInstruction: constants.csharpPlaywrightInstructionsSnapshot, + }, + }, +}; + +/** + * Utility function to check if a given language and testing framework + * are supported by Percy Web. + */ +export function isPercyWebFrameworkSupported( + language: string, + framework: string, +): boolean { + const languageConfig = + SUPPORTED_CONFIGURATIONS[language as keyof typeof SUPPORTED_CONFIGURATIONS]; + if (!languageConfig) return false; + return !!languageConfig[framework as keyof typeof languageConfig]; +} diff --git a/src/tools/sdk-utils/percy-web/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts new file mode 100644 index 00000000..ea2cbfe6 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -0,0 +1,49 @@ +// Handler for Percy Web only mode - Visual testing without BrowserStack infrastructure +import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; +import { SetUpPercyInput } from "../common/schema.js"; +import { SUPPORTED_CONFIGURATIONS } from "./frameworks.js"; + +import { + SDKSupportedBrowserAutomationFramework, + SDKSupportedLanguage, +} from "../common/types.js"; + +export let percyWebSetupInstructions = ""; + +export function runPercyWeb( + input: SetUpPercyInput, + percyToken: string, +): RunTestsInstructionResult { + const steps: RunTestsStep[] = []; + + // Assume configuration is supported due to guardrails at orchestration layer + const languageConfig = + SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; + const frameworkConfig = + languageConfig[ + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework + ]; + + // Generate instructions for the supported configuration + const instructions = frameworkConfig.instructions; + percyWebSetupInstructions = frameworkConfig.snapshotInstruction; + + // Prepend a step to set the Percy token in the environment + steps.push({ + type: "instruction", + title: "Set Percy Token in Environment", + content: `---STEP---Set the environment variable generated for your project before running your tests:\n\nexport PERCY_TOKEN="${percyToken}"\n\n(For Windows, use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)---STEP---`, + }); + + steps.push({ + type: "instruction", + title: `Percy Web Setup Instructions`, + content: instructions, + }); + + return { + steps, + requiresPercy: true, + missingDependencies: [], + }; +} diff --git a/src/tools/sdk-utils/percy-web/index.ts b/src/tools/sdk-utils/percy-web/index.ts new file mode 100644 index 00000000..6dd10c6b --- /dev/null +++ b/src/tools/sdk-utils/percy-web/index.ts @@ -0,0 +1,5 @@ +// Percy Web utilities +export { runPercyWeb } from "./handler.js"; +export { SUPPORTED_CONFIGURATIONS } from "./frameworks.js"; +export * as constants from "./constants.js"; +export type { ConfigMapping } from "./types.js"; diff --git a/src/tools/sdk-utils/percy-web/types.ts b/src/tools/sdk-utils/percy-web/types.ts new file mode 100644 index 00000000..0445800a --- /dev/null +++ b/src/tools/sdk-utils/percy-web/types.ts @@ -0,0 +1,12 @@ +/** + * Type for Percy Web configuration mapping. + * Structure: language -> automationFramework -> { instructions: string } + */ +export type ConfigMapping = { + [language: string]: { + [automationFramework: string]: { + instructions: string; + snapshotInstruction: string; + }; + }; +}; diff --git a/src/tools/sdk-utils/percy/types.ts b/src/tools/sdk-utils/percy/types.ts deleted file mode 100644 index 1ddd464f..00000000 --- a/src/tools/sdk-utils/percy/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - SDKSupportedBrowserAutomationFramework, - SDKSupportedLanguage, - SDKSupportedTestingFramework, -} from "../types.js"; - -export interface PercyInstructions { - script_updates: string; -} - -export type PercyConfigMapping = Partial< - Record< - SDKSupportedLanguage, - Partial< - Record< - SDKSupportedBrowserAutomationFramework, - Partial> - > - > - > ->; From e2796b8fc2c9a3583aed4e0963c8502330159486 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 28 Aug 2025 16:52:41 +0530 Subject: [PATCH 07/84] refactor: Improve code formatting and structure across multiple files --- .../appautomate-utils/appium-sdk/handler.ts | 8 +++-- .../appium-sdk/languages/ruby.ts | 14 ++++----- .../appautomate-utils/appium-sdk/types.ts | 11 +++++-- .../appautomate-utils/appium-sdk/utils.ts | 30 ++++++++++++------- src/tools/appautomate.ts | 3 +- 5 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index c325665d..405b203d 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -2,8 +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 { getAppUploadInstruction, validateSupportforAppAutomate, SupportedFramework } from "./utils.js"; -import logger from "../../../logger.js"; +import { + getAppUploadInstruction, + validateSupportforAppAutomate, + SupportedFramework, +} from "./utils.js"; import { getAppSDKPrefixCommand, @@ -23,7 +26,6 @@ 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(":"); diff --git a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts index dc5e8ac7..10ff6a6f 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/ruby.ts @@ -28,7 +28,7 @@ browser_caps: "os_version": "9.0" "app": "" "name": "first_test" -\`\`\`` +\`\`\``, ); const envStep = createStep( @@ -63,14 +63,14 @@ end at_exit do $driver.quit if $driver end -\`\`\`` +\`\`\``, ); const runStep = createStep( "Run the test:", `\`\`\`bash bundle exec cucumber -\`\`\`` +\`\`\``, ); return combineInstructions(configStep, envStep, runStep); @@ -79,7 +79,7 @@ bundle exec cucumber export function getRubySDKCommand( framework: string, username: string, - accessKey: string + accessKey: string, ): string { const { isWindows, getPlatformLabel } = PLATFORM_UTILS; @@ -88,7 +88,7 @@ export function getRubySDKCommand( accessKey, isWindows, getPlatformLabel(), - "Set your BrowserStack credentials as environment variables:" + "Set your BrowserStack credentials as environment variables:", ); const installStep = createStep( @@ -102,7 +102,7 @@ gem install appium_lib # Install Cucumber gem install cucumber -\`\`\`` +\`\`\``, ); const gemfileStep = createStep( @@ -118,7 +118,7 @@ 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 index 07fc7e5f..e17f0da3 100644 --- a/src/tools/appautomate-utils/appium-sdk/types.ts +++ b/src/tools/appautomate-utils/appium-sdk/types.ts @@ -9,7 +9,7 @@ export enum AppSDKSupportedLanguageEnum { export type AppSDKSupportedLanguage = keyof typeof AppSDKSupportedLanguageEnum; export enum AppSDKSupportedFrameworkEnum { - appium = "appium" + appium = "appium", } export type AppSDKSupportedFramework = @@ -60,7 +60,14 @@ export interface AppSDKInstruction { export const SUPPORTED_CONFIGURATIONS = { appium: { ruby: ["cucumberRuby"], - java: ["junit5", "junit4", "testng", "cucumberTestng", "selenide", "jbehave"], + 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 index 312286ce..9c042ba5 100644 --- a/src/tools/appautomate-utils/appium-sdk/utils.ts +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -1,9 +1,10 @@ +import { uploadApp } from "../native-execution/appautomate.js"; import { AppSDKSupportedTestingFramework, AppSDKSupportedTestingFrameworkEnum, createStep, } from "./index.js"; -import {SUPPORTED_CONFIGURATIONS} from "./types.js" +import { SUPPORTED_CONFIGURATIONS } from "./types.js"; export function isBrowserStackAppUrl(appPath: string): boolean { return appPath.startsWith("bs://"); @@ -51,10 +52,12 @@ export async function getAppUploadInstruction( if ( detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.nightwatch || - detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.webdriverio || - detectedTestingFramework === AppSDKSupportedTestingFrameworkEnum.cucumberRuby + detectedTestingFramework === + AppSDKSupportedTestingFrameworkEnum.webdriverio || + detectedTestingFramework === + AppSDKSupportedTestingFrameworkEnum.cucumberRuby ) { - const app_url = "bs://ff4e358328a3e914fe4f0e46ec7af73f9c08cd55"; + const app_url = await uploadApp(appPath, username, accessKey); if (app_url) { return createStep( "Updating app_path with app_url", @@ -66,32 +69,37 @@ export async function getAppUploadInstruction( } export type SupportedFramework = keyof typeof SUPPORTED_CONFIGURATIONS; -type SupportedLanguage = keyof typeof SUPPORTED_CONFIGURATIONS[SupportedFramework]; +type SupportedLanguage = + keyof (typeof SUPPORTED_CONFIGURATIONS)[SupportedFramework]; type SupportedTestingFramework = string; export function validateSupportforAppAutomate( framework: SupportedFramework, language: SupportedLanguage, - testingFramework: SupportedTestingFramework + testingFramework: SupportedTestingFramework, ) { - const frameworks = Object.keys(SUPPORTED_CONFIGURATIONS) as SupportedFramework[]; + const frameworks = Object.keys( + SUPPORTED_CONFIGURATIONS, + ) as SupportedFramework[]; if (!SUPPORTED_CONFIGURATIONS[framework]) { throw new Error( - `Unsupported framework '${framework}'. Supported frameworks: ${frameworks.join(", ")}` + `Unsupported framework '${framework}'. Supported frameworks: ${frameworks.join(", ")}`, ); } - const languages = Object.keys(SUPPORTED_CONFIGURATIONS[framework]) as SupportedLanguage[]; + 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(", ")}` + `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(", ")}` + `Unsupported testing framework '${testingFramework}' for language '${language}' and framework '${framework}'. Supported testing frameworks: ${testingFrameworks.join(", ")}`, ); } } diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 4ff8eba9..d13f8a46 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -393,7 +393,8 @@ export default function addAppAutomationTools( try { return await setupAppAutomateHandler(args, config); } catch (error) { - const error_message = error instanceof Error ? error.message : "Unknown error"; + const error_message = + error instanceof Error ? error.message : "Unknown error"; return { content: [ { From 75179500b2a0af17da2f192ce51417945624459f Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 29 Aug 2025 11:47:09 +0530 Subject: [PATCH 08/84] fix : Update Maven command functions --- .../appium-sdk/languages/java.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/languages/java.ts b/src/tools/appautomate-utils/appium-sdk/languages/java.ts index 794b249a..ea5ad958 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/java.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/java.ts @@ -52,6 +52,8 @@ export function getJavaAppFrameworkForMaven(framework: string): string { function getMavenCommandForWindows( framework: string, mavenFramework: string, + username: string, + accessKey: string, ): string { return ( `mvn archetype:generate -B ` + @@ -61,8 +63,8 @@ function getMavenCommandForWindows( `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + - `-DBROWSERSTACK_USERNAME="${process.env.BROWSERSTACK_USERNAME}" ` + - `-DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" ` + + `-DBROWSERSTACK_USERNAME="${username}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}" ` + `-DBROWSERSTACK_FRAMEWORK="${framework}"` ); } @@ -70,6 +72,8 @@ function getMavenCommandForWindows( function getMavenCommandForUnix( framework: string, mavenFramework: string, + username: string, + accessKey: string, ): string { return ( `mvn archetype:generate -B ` + @@ -79,8 +83,8 @@ function getMavenCommandForUnix( `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + - `-DBROWSERSTACK_USERNAME="${process.env.BROWSERSTACK_USERNAME}" ` + - `-DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" ` + + `-DBROWSERSTACK_USERNAME="${username}" ` + + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}" ` + `-DBROWSERSTACK_FRAMEWORK="${framework}"` ); } @@ -98,12 +102,22 @@ export function getJavaSDKCommand( let mavenCommand: string; if (isWindows) { - mavenCommand = getMavenCommandForWindows(framework, mavenFramework); + mavenCommand = getMavenCommandForWindows( + framework, + mavenFramework, + username, + accessKey, + ); if (appPath) { mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; } } else { - mavenCommand = getMavenCommandForUnix(framework, mavenFramework); + mavenCommand = getMavenCommandForUnix( + framework, + mavenFramework, + username, + accessKey, + ); if (appPath) { mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; } From ea2cd5a32b0454059d4bc3925c613f578703cf84 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Sun, 31 Aug 2025 00:00:48 +0530 Subject: [PATCH 09/84] Percy Integration with new flow --- src/tools/percy-change.ts | 79 +++++++++++++ src/tools/percy-sdk.ts | 45 +++++++- src/tools/percy-snapshot-utils/utils.ts | 2 +- src/tools/review-agent-utils/build-counts.ts | 33 ++++++ .../percy-approve-reject.ts | 53 +++++++++ src/tools/review-agent-utils/percy-builds.ts | 43 ++++++++ src/tools/review-agent-utils/percy-diffs.ts | 76 +++++++++++++ .../review-agent-utils/percy-snapshots.ts | 45 ++++++++ src/tools/run-percy-scan.ts | 61 ++++++++++ src/tools/sdk-utils/common/schema.ts | 32 ++++++ .../sdk-utils/percy-automate/constants.ts | 47 ++------ src/tools/sdk-utils/percy-automate/handler.ts | 2 +- src/tools/sdk-utils/percy-web/constants.ts | 104 ++++-------------- .../sdk-utils/percy-web/fetchPercyToken.ts | 82 ++++++++------ src/tools/sdk-utils/percy-web/handler.ts | 2 +- src/tools/sdk-utils/types.ts | 56 ---------- 16 files changed, 546 insertions(+), 216 deletions(-) create mode 100644 src/tools/percy-change.ts create mode 100644 src/tools/review-agent-utils/build-counts.ts create mode 100644 src/tools/review-agent-utils/percy-approve-reject.ts create mode 100644 src/tools/review-agent-utils/percy-builds.ts create mode 100644 src/tools/review-agent-utils/percy-diffs.ts create mode 100644 src/tools/review-agent-utils/percy-snapshots.ts create mode 100644 src/tools/run-percy-scan.ts delete mode 100644 src/tools/sdk-utils/types.ts diff --git a/src/tools/percy-change.ts b/src/tools/percy-change.ts new file mode 100644 index 00000000..615c3069 --- /dev/null +++ b/src/tools/percy-change.ts @@ -0,0 +1,79 @@ +import logger from "../logger.js"; +import { BrowserStackConfig } from "../lib/types.js"; +import { getBrowserStackAuth } from "../lib/get-auth.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { getPercyBuildCount } from "./review-agent-utils/build-counts.js"; +import { getPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js"; +import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js"; +import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js"; + +import { + getPercySnapshotDiffs, + PercySnapshotDiff, +} from "./review-agent-utils/percy-diffs.js"; + +export async function fetchPercyChanges( + args: { project_name: string }, + config: BrowserStackConfig, +): Promise { + const { project_name } = args; + const authorization = getBrowserStackAuth(config); + + // Get Percy token for the project + const percyToken = await fetchPercyToken(project_name, authorization, { + type: PercyIntegrationTypeEnum.WEB, + }); + + // Get build info (noBuilds, isFirstBuild, lastBuildId) + const { noBuilds, isFirstBuild, lastBuildId } = + await getPercyBuildCount(percyToken); + + if (noBuilds) { + return { + content: [ + { + type: "text", + text: "No Percy builds found. Please run your first Percy scan to start visual testing.", + }, + ], + }; + } + + if (isFirstBuild || !lastBuildId) { + return { + content: [ + { + type: "text", + text: "This is the first Percy build. No baseline exists to compare changes.", + }, + ], + }; + } + + // Get snapshot IDs for the latest build + const snapshotIds = await getPercySnapshotIds(lastBuildId, percyToken); + logger.info( + `Fetched ${snapshotIds.length} snapshot IDs for build: ${lastBuildId} as ${snapshotIds.join(", ")}`, + ); + + // Fetch all diffs concurrently and flatten results + const allDiffs = await getPercySnapshotDiffs(snapshotIds, percyToken); + + if (allDiffs.length === 0) { + return { + content: [ + { + type: "text", + text: "AI Summary is not yet available for this build/framework. There may still be visual changes—please review the build on the dashboard. Support for AI Summary will be added soon.", + }, + ], + }; + } + + return { + content: allDiffs.map((diff: PercySnapshotDiff) => ({ + type: "text", + text: `${diff.name} → ${diff.title}: ${diff.description ?? ""}`, + })), + }; +} diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 7e2e4631..ed9b7777 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -1,24 +1,36 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { trackMCP } from "../index.js"; import { BrowserStackConfig } from "../lib/types.js"; +import { fetchPercyChanges } from "./percy-change.js"; +import { addListTestFiles } from "./list-test-files.js"; +import { runPercyScan } from "./run-percy-scan.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js"; import { updateTestsWithPercyCommands } from "./add-percy-snapshots.js"; -import { addListTestFiles } from "./list-test-files.js"; -import { trackMCP } from "../index.js"; +import { approveOrDeclinePercyBuild } from "./review-agent-utils/percy-approve-reject.js"; + import { setUpPercyHandler, setUpSimulatePercyChangeHandler, } from "./sdk-utils/handler.js"; + import { SETUP_PERCY_DESCRIPTION, SIMULATE_PERCY_CHANGE_DESCRIPTION, LIST_TEST_FILES_DESCRIPTION, PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, } from "./sdk-utils/common/constants.js"; + import { ListTestFilesParamsShape, UpdateTestFileWithInstructionsParams, } from "./percy-snapshot-utils/constants.js"; +import { + RunPercyScanParamsShape, + FetchPercyChangesParamsShape, + ManagePercyBuildApprovalParamsShape, +} from "./sdk-utils/common/schema.js"; + export function registerPercyTools( server: McpServer, config: BrowserStackConfig, @@ -153,6 +165,33 @@ export function registerPercyTools( }, ); + tools.runPercyScan = server.tool( + "runPercyScan", + "Run a Percy visual test scan. Example prompts : Run this Percy build/scan.Never run percy scan/build without this tool", + RunPercyScanParamsShape, + async (args) => { + return runPercyScan(args, config); + }, + ); + + tools.fetchPercyChanges = server.tool( + "fetchPercyChanges", + "Retrieves and summarizes all visual changes detected by Percy between the latest and previous builds, helping quickly review what has changed in your project.", + FetchPercyChangesParamsShape, + async (args) => { + return await fetchPercyChanges(args, config); + }, + ); + + tools.managePercyBuildApproval = server.tool( + "managePercyBuildApproval", + "Approve or reject a Percy build", + ManagePercyBuildApprovalParamsShape, + async (args) => { + return await approveOrDeclinePercyBuild(args, config); + }, + ); + return tools; } diff --git a/src/tools/percy-snapshot-utils/utils.ts b/src/tools/percy-snapshot-utils/utils.ts index 396c64e6..67625a68 100644 --- a/src/tools/percy-snapshot-utils/utils.ts +++ b/src/tools/percy-snapshot-utils/utils.ts @@ -34,7 +34,7 @@ export async function updateFileAndStep( if (nextIndex === total) { content.push({ type: "text", - text: `Step 4: Percy snapshot commands have been added to all files. You can now run the Percy build using the above command.`, + text: `Step 3: Percy snapshot commands have been added to all files. You can now run the tool runPercyScan to run the percy scan.`, }); } diff --git a/src/tools/review-agent-utils/build-counts.ts b/src/tools/review-agent-utils/build-counts.ts new file mode 100644 index 00000000..8b50b132 --- /dev/null +++ b/src/tools/review-agent-utils/build-counts.ts @@ -0,0 +1,33 @@ +// Utility for fetching the count of Percy builds. +export async function getPercyBuildCount(percyToken: string) { + const apiUrl = `https://percy.io/api/v1/builds`; + + const response = await fetch(apiUrl, { + headers: { + Authorization: `Token token=${percyToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch Percy builds: ${response.statusText}`); + } + + const data = await response.json(); + const builds = data.data ?? []; + + let isFirstBuild = false; + let lastBuildId; + + if (builds.length === 0) { + return { noBuilds: true, isFirstBuild: false, lastBuildId: undefined }; + } else if (builds.length === 1) { + isFirstBuild = true; + lastBuildId = builds[0].id; + } else { + isFirstBuild = false; + lastBuildId = builds[0].id; + } + + return { noBuilds: false, isFirstBuild, lastBuildId }; +} diff --git a/src/tools/review-agent-utils/percy-approve-reject.ts b/src/tools/review-agent-utils/percy-approve-reject.ts new file mode 100644 index 00000000..0a084355 --- /dev/null +++ b/src/tools/review-agent-utils/percy-approve-reject.ts @@ -0,0 +1,53 @@ +import { BrowserStackConfig } from "../../lib/types.js"; +import { getBrowserStackAuth } from "../../lib/get-auth.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +export async function approveOrDeclinePercyBuild( + args: { buildId: string; action: "approve" | "unapprove" | "reject" }, + config: BrowserStackConfig, +): Promise { + const { buildId, action } = args; + + // Get Basic Auth credentials + const [username, accessKey] = getBrowserStackAuth(config).split(":"); + const authHeader = `Basic ${Buffer.from(`${username}:${accessKey}`).toString("base64")}`; + + // Prepare request body + const body = { + data: { + type: "reviews", + attributes: { action }, + relationships: { + build: { data: { type: "builds", id: buildId } }, + }, + }, + }; + + // Send request to Percy API + const response = await fetch("https://percy.io/api/v1/reviews", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: authHeader, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Percy build ${action} failed: ${response.status} ${errorText}`, + ); + } + + const result = await response.json(); + + return { + content: [ + { + type: "text", + text: `Percy build ${buildId} was ${result.data.attributes["review-state"]} by ${result.data.attributes["action-performed-by"].user_name}`, + }, + ], + }; +} diff --git a/src/tools/review-agent-utils/percy-builds.ts b/src/tools/review-agent-utils/percy-builds.ts new file mode 100644 index 00000000..749e57e5 --- /dev/null +++ b/src/tools/review-agent-utils/percy-builds.ts @@ -0,0 +1,43 @@ +/** + * Utility for fetching Percy build information. + */ + +export interface PercyBuildInfo { + noBuilds: boolean; + isFirstBuild: boolean; + lastBuildId?: string; +} + +/** + * Fetches Percy build information for a given token. + * @param percyToken - The Percy API token. + * @returns PercyBuildInfo object. + */ +export async function getPercyBuildInfo( + percyToken: string, +): Promise { + const apiUrl = "https://percy.io/api/v1/builds"; + + const response = await fetch(apiUrl, { + headers: { + Authorization: `Token token=${percyToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch Percy builds: ${response.statusText}`); + } + + const data = await response.json(); + const builds = data.data ?? []; + + if (builds.length === 0) { + return { noBuilds: true, isFirstBuild: false, lastBuildId: undefined }; + } + + const isFirstBuild = builds.length === 1; + const lastBuildId = builds[0].id; + + return { noBuilds: false, isFirstBuild, lastBuildId }; +} diff --git a/src/tools/review-agent-utils/percy-diffs.ts b/src/tools/review-agent-utils/percy-diffs.ts new file mode 100644 index 00000000..94a34f03 --- /dev/null +++ b/src/tools/review-agent-utils/percy-diffs.ts @@ -0,0 +1,76 @@ +/** + * Utility for fetching and aggregating Percy snapshot diffs. + */ + +export interface PercySnapshotDiff { + id: string; + name: string | null; + title: string; + description: string | null; + coordinates: any; +} + +/** + * Fetches diffs for a single Percy snapshot. + * @param snapshotId - The Percy snapshot ID. + * @param percyToken - The Percy API token. + * @returns Array of PercySnapshotDiff objects. + */ +export async function getPercySnapshotDiff( + snapshotId: string, + percyToken: string, +): Promise { + const apiUrl = `https://percy.io/api/v1/snapshots/${snapshotId}`; + + const response = await fetch(apiUrl, { + headers: { + Authorization: `Token token=${percyToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Percy snapshot ${snapshotId}: ${response.statusText}`, + ); + } + + const data = await response.json(); + const pageUrl = data.data.attributes?.name || null; + + const changes: PercySnapshotDiff[] = []; + const comparisons = + data.included?.filter((item: any) => item.type === "comparisons") ?? []; + + for (const comparison of comparisons) { + const appliedRegions = comparison.attributes?.["applied-regions"] ?? []; + for (const region of appliedRegions) { + if (region.ignored) continue; + changes.push({ + id: String(region.id), + name: pageUrl, + title: region.change_title, + description: region.change_description ?? null, + coordinates: region.coordinates ?? null, + }); + } + } + + return changes; +} + +/** + * Fetches and flattens all diffs for an array of Percy snapshot IDs. + * @param snapshotIds - Array of Percy snapshot IDs. + * @param percyToken - The Percy API token. + * @returns Flat array of PercySnapshotDiff objects. + */ +export async function getPercySnapshotDiffs( + snapshotIds: string[], + percyToken: string, +): Promise { + const allDiffs = await Promise.all( + snapshotIds.map((id) => getPercySnapshotDiff(id, percyToken)), + ); + return allDiffs.flat(); +} diff --git a/src/tools/review-agent-utils/percy-snapshots.ts b/src/tools/review-agent-utils/percy-snapshots.ts new file mode 100644 index 00000000..5d2d0214 --- /dev/null +++ b/src/tools/review-agent-utils/percy-snapshots.ts @@ -0,0 +1,45 @@ +// Utility for fetching Percy snapshot IDs for a given build (up to `maxSnapshots`). +export async function getPercySnapshotIds( + buildId: string, + percyToken: string, + maxSnapshots = 300, +): Promise { + const perPage = 30; + const allSnapshotIds: string[] = []; + let cursor: string | undefined = undefined; + + while (allSnapshotIds.length < maxSnapshots) { + const url = new URL(`https://percy.io/api/v1/snapshots`); + url.searchParams.set("build_id", buildId); + url.searchParams.set("page[limit]", String(perPage)); + if (cursor) url.searchParams.set("page[cursor]", cursor); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Token token=${percyToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Percy snapshots: ${response.statusText}`, + ); + } + + const data = await response.json(); + const snapshots = data.data ?? []; + if (snapshots.length === 0) break; // no more snapshots + + allSnapshotIds.push(...snapshots.map((s: any) => String(s.id))); + + // Set cursor to last snapshot ID of this page for next iteration + cursor = snapshots[snapshots.length - 1].id; + + // Stop if we've collected enough + if (allSnapshotIds.length >= maxSnapshots) break; + } + + // Return only up to maxSnapshots + return allSnapshotIds.slice(0, maxSnapshots); +} diff --git a/src/tools/run-percy-scan.ts b/src/tools/run-percy-scan.ts new file mode 100644 index 00000000..fac071a0 --- /dev/null +++ b/src/tools/run-percy-scan.ts @@ -0,0 +1,61 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js"; +import { BrowserStackConfig } from "../lib/types.js"; +import { getBrowserStackAuth } from "../lib/get-auth.js"; +import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js"; + +export async function runPercyScan( + args: { + projectName: string; + integrationType: PercyIntegrationTypeEnum; + instruction?: string; + }, + config: BrowserStackConfig, +): Promise { + const { projectName, integrationType, instruction } = args; + const authorization = getBrowserStackAuth(config); + const percyToken = await fetchPercyToken(projectName, authorization, { + type: integrationType, + }); + + const steps: string[] = [generatePercyTokenInstructions(percyToken)]; + + if (instruction) { + steps.push( + `Use the provided test command with Percy:\n${instruction}`, + `If this command fails or is incorrect, fall back to the default approach below.`, + ); + } + + steps.push( + `Attempt to infer the project's test command from context (high confidence commands first): +- Java → mvn test +- Python → pytest +- Node.js → npm test or yarn test +- Cypress → cypress run +or from package.json scripts`, + `Wrap the inferred command with Percy:\nnpx percy exec -- `, + `If the test command cannot be inferred confidently, ask the user directly for the correct test command.`, + ); + + const instructionContext = steps + .map((step, index) => `${index + 1}. ${step}`) + .join("\n\n"); + + return { + content: [ + { + type: "text", + text: instructionContext, + }, + ], + }; +} + +function generatePercyTokenInstructions(percyToken: string): string { + return `Set the environment variable for your project: + +export PERCY_TOKEN="${percyToken}" + +(For Windows: use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)`; +} diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 5b41c395..73f8aa0a 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -50,3 +50,35 @@ export type SetUpPercyInput = z.infer; export type RunTestsOnBrowserStackInput = z.infer< typeof RunTestsOnBrowserStackSchema >; + +export const RunPercyScanParamsShape = { + projectName: z.string().describe("The name of the project to run Percy on."), + percyRunCommand: z + .string() + .optional() + .describe( + "The test command to run with Percy. Optional — the LLM should try to infer it first from project context.", + ), + integrationType: z + .nativeEnum(PercyIntegrationTypeEnum) + .describe( + "Specifies whether to integrate with Percy Web or Percy Automate. If not explicitly provided, prompt the user to select the desired integration type.", + ), +}; + +export const FetchPercyChangesParamsShape = { + project_name: z + .string() + .describe( + "The name of the BrowserStack project. If not found, ask user directly.", + ), +}; + +export const ManagePercyBuildApprovalParamsShape = { + buildId: z + .string() + .describe("The ID of the Percy build to approve or reject."), + action: z + .enum(["approve", "unapprove", "reject"]) + .describe("The action to perform on the Percy build."), +}; diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index be28b192..e73e7bde 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -40,10 +40,7 @@ def test_homepage(driver): \`\`\` ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- browserstack-sdk pytest - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- browserstack-sdk pytest'). ${percyAutomateReviewSnapshotsStep} `; @@ -81,10 +78,7 @@ def test_visual_regression(): \`\`\` ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- '). ${percyAutomateReviewSnapshotsStep} `; @@ -116,10 +110,7 @@ describe('Percy Automate Cypress Example', () => { \`\`\` ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- cypress run - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- cypress run'). ${percyAutomateReviewSnapshotsStep} `; @@ -140,10 +131,7 @@ Update your Mocha Automate test script await percyScreenshot(driver, 'Screenshot 2', options); ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- mocha'). ${percyAutomateReviewSnapshotsStep} `; @@ -170,10 +158,7 @@ await percyScreenshot(page, "Screenshot 2", { percyCSS: "h1{color:green;}" }); \`\`\` ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- '). ${percyAutomateReviewSnapshotsStep} `; @@ -213,10 +198,7 @@ describe("JestJS test", () => { \`\`\` ---STEP--- -Run your test script: - - Use the following command: - npm run [your-test-script-name]-browserstack - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npm run [your-test-script-name]-browserstack'). ${percyAutomateReviewSnapshotsStep} `; @@ -271,9 +253,7 @@ describe("WebdriverIO Test", () => { \`\`\` ---STEP--- -Run your test script - - Use the commands defined in your package.json file to run the tests on BrowserStack. - +To run the Percy build, call the tool runPercyScan with the appropriate test command as defined in your package.json file. ${percyAutomateReviewSnapshotsStep} `; @@ -299,10 +279,7 @@ test('Test1', async t => { \`\`\` ---STEP--- -Run Percy - - Use the following command to run your tests with Percy: - npx percy exec -- testcafe chrome:headless tests - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- testcafe chrome:headless tests'). ${percyAutomateReviewSnapshotsStep} `; @@ -335,9 +312,7 @@ percy.screenshot("screenshot_2", options); \`\`\` ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- '). ${percyAutomateReviewSnapshotsStep} `; @@ -367,9 +342,7 @@ Percy.Screenshot(page, "example_screenshot_2", options); \`\`\` ---STEP--- -Run Percy Automate with your tests - - Use the following command: - npx percy exec -- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- '). ${percyAutomateReviewSnapshotsStep} `; diff --git a/src/tools/sdk-utils/percy-automate/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts index 16296e7b..f5e43135 100644 --- a/src/tools/sdk-utils/percy-automate/handler.ts +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -26,7 +26,7 @@ export function runPercyAutomateOnly( steps.push({ type: "instruction", title: "Set Percy Token in Environment", - content: `---STEP---Set the environment variable generated for your project before running your tests:\n\nexport PERCY_TOKEN="${percyToken}"\n\n(For Windows, use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)---STEP---`, + content: `Here is percy token if required {${percyToken}}`, }); steps.push({ diff --git a/src/tools/sdk-utils/percy-web/constants.ts b/src/tools/sdk-utils/percy-web/constants.ts index 307bf4c4..44728a0f 100644 --- a/src/tools/sdk-utils/percy-web/constants.ts +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -544,22 +544,13 @@ Install Percy dependencies npm install --save-dev @percy/cli - Install Percy Selenium Python package: pip install percy-selenium - +If faced any issue create a virtual environment and proceed. Update your Python Selenium script ${PERCY_SNAPSHOT_INSTRUCTION} ${pythonInstructionsSnapshot} -Run Percy with your tests - - Use the following command: - npx percy exec -- - -Example output: - [percy] Percy has started! - [percy] Created build #1: https://percy.io/your-project - [percy] Snapshot taken "Home page" - [percy] Finalized build #1: https://percy.io/your-project - [percy] Done! - +---STEP--- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- python tests.py'). ${percyReviewSnapshotsStep} `; @@ -576,9 +567,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- node scripts/test.js +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- node script.js'). ${percyReviewSnapshotsStep} `; @@ -603,9 +592,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${javaInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- mvn test +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- mvn test'). ${percyReviewSnapshotsStep} `; @@ -624,9 +611,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${rubyInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- bundle exec rspec'). ${percyReviewSnapshotsStep} `; @@ -646,9 +631,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${rubyCapybaraInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- bundle exec rspec +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- bundle exec rspec'). ${percyReviewSnapshotsStep} `; @@ -667,9 +650,8 @@ Update your C# Selenium test ${PERCY_SNAPSHOT_INSTRUCTION} ${csharpInstructionsSnapshot} -Run Percy with your tests - - Use the following command: - npx percy exec -- +---STEP--- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- dotnet test'). ${percyReviewSnapshotsStep} `; @@ -689,9 +671,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${javaPlaywrightInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g. npx percy exec -- ). ${percyReviewSnapshotsStep} `; @@ -707,9 +687,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsPlaywrightInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., npx percy exec -- ). ${percyReviewSnapshotsStep} `; @@ -728,9 +706,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsWebdriverioInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- wdio run wdio.conf.js +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- wdio run wdio.conf.js'). ${percyReviewSnapshotsStep} `; @@ -748,9 +724,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsEmberInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- ember test +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- ember test'). ${percyReviewSnapshotsStep} `; @@ -768,9 +742,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsCypressInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- cypress run +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- cypress run'). ${percyReviewSnapshotsStep} `; @@ -788,9 +760,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsPuppeteerInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- mocha +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- '). ${percyReviewSnapshotsStep} `; @@ -808,9 +778,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsNightmareInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- node script.js +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- node script.js'). ${percyReviewSnapshotsStep} `; @@ -828,9 +796,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsNightwatchInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- nightwatch +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- nightwatch'). ${percyReviewSnapshotsStep} `; @@ -848,9 +814,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsProtractorInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- protractor conf.js +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- protractor conf.js'). ${percyReviewSnapshotsStep} `; @@ -868,10 +832,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsTestcafeInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- testcafe chrome:headless tests - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- testcafe chrome:headless tests'). ${percyReviewSnapshotsStep} `; @@ -888,10 +849,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${nodejsGatsbyInstructionsSnapshot} ---STEP--- -Run Percy with your Gatsby build - - Use the following command: - npx percy exec -- gatsby build - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g., 'npx percy exec -- gatsby build'). ${percyReviewSnapshotsStep} `; @@ -915,17 +873,7 @@ Run Percy with your Storybook percy storybook http://localhost:9009 percy storybook https://storybook.foobar.com - Automatically run start-storybook: - percy storybook:start --port=9009 --static-dir=./public - - - Example output: - [percy] Snapshot found: My snapshot - [percy] - url: [...]?id=component--my-story - [percy] Snapshot found: [Dark mode] My snapshot - [percy] - url: [...]?id=component--my-story&args=colorScheme:dark - [percy] Snapshot found: My snapshot with globals - [percy] - url: [...]?id=component--my-story&globals=textDirection:rtl - [percy] Snapshot found: Search snapshot - [percy] - url: [...]?id=component--my-story&search=foobar + Run this scan using tool runPercyScan with 'npx percy exec -- percy storybook:start --port=9009'. ${percyReviewSnapshotsStep} `; @@ -955,10 +903,7 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${pythonPlaywrightInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- python your_test_script.py - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g. npx percy exec -- ). ${percyReviewSnapshotsStep} `; @@ -973,9 +918,6 @@ ${PERCY_SNAPSHOT_INSTRUCTION} ${csharpPlaywrightInstructionsSnapshot} ---STEP--- -Run Percy with your tests - - Use the following command: - npx percy exec -- dotnet test - +To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g. npx percy exec -- ). ${percyReviewSnapshotsStep} `; diff --git a/src/tools/sdk-utils/percy-web/fetchPercyToken.ts b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts index 7b0579b6..b6ed135f 100644 --- a/src/tools/sdk-utils/percy-web/fetchPercyToken.ts +++ b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts @@ -1,45 +1,55 @@ import { PercyIntegrationTypeEnum } from "../common/types.js"; +let globalPercyToken: string | null = null; +let globalProjectName: string | null = null; + +async function fetchTokenFromAPI( + projectName: string, + authorization: string, + options: { type?: PercyIntegrationTypeEnum } = {}, +): Promise { + const authHeader = `Basic ${Buffer.from(authorization).toString("base64")}`; + const baseUrl = + "https://api.browserstack.com/api/app_percy/get_project_token"; + const params = new URLSearchParams({ name: projectName }); + + if (options.type) { + params.append("type", options.type); + } + + const url = `${baseUrl}?${params.toString()}`; + const response = await fetch(url, { headers: { Authorization: authHeader } }); + + if (!response.ok) { + throw new Error(`Failed to fetch Percy token (status: ${response.status})`); + } + + const data = await response.json(); + + if (!data?.token || !data?.success) { + throw new Error( + "Project exists but is likely set up for Automate. Please use a different project name.", + ); + } + + return data.token; +} + export async function fetchPercyToken( projectName: string, authorization: string, options: { type?: PercyIntegrationTypeEnum } = {}, ): Promise { - try { - const authHeader = `Basic ${Buffer.from(authorization).toString("base64")}`; - const baseUrl = - "https://api.browserstack.com/api/app_percy/get_project_token"; - const params = new URLSearchParams({ name: projectName }); - - if (options.type) { - params.append("type", options.type); - } - - const url = `${baseUrl}?${params.toString()}`; - - const response = await fetch(url, { - headers: { - Authorization: authHeader, - }, - }); - - if (!response.ok) { - throw new Error( - `Failed to fetch Percy token (status: ${response.status})`, - ); - } - - const data = await response.json(); - - if (!data?.token || !data?.success) { - throw new Error( - "Project exists but is likely set up for Automate. Please use a different project name.", - ); - } - - return data.token; - } catch (err) { - const message = err instanceof Error ? err.message : "Unknown error"; - throw new Error(`Error retrieving Percy token: ${message}`); + if (globalProjectName !== projectName) { + globalProjectName = projectName; + globalPercyToken = null; + } + + if (globalPercyToken) { + return globalPercyToken; } + + const token = await fetchTokenFromAPI(projectName, authorization, options); + globalPercyToken = token; + return token; } diff --git a/src/tools/sdk-utils/percy-web/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts index ea2cbfe6..bca09687 100644 --- a/src/tools/sdk-utils/percy-web/handler.ts +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -32,7 +32,7 @@ export function runPercyWeb( steps.push({ type: "instruction", title: "Set Percy Token in Environment", - content: `---STEP---Set the environment variable generated for your project before running your tests:\n\nexport PERCY_TOKEN="${percyToken}"\n\n(For Windows, use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)---STEP---`, + content: `Here is percy token if required {${percyToken}}`, }); steps.push({ diff --git a/src/tools/sdk-utils/types.ts b/src/tools/sdk-utils/types.ts deleted file mode 100644 index caba6eaa..00000000 --- a/src/tools/sdk-utils/types.ts +++ /dev/null @@ -1,56 +0,0 @@ -export enum SDKSupportedLanguageEnum { - nodejs = "nodejs", - python = "python", - java = "java", - csharp = "csharp", -} -export type SDKSupportedLanguage = keyof typeof SDKSupportedLanguageEnum; - -export enum SDKSupportedBrowserAutomationFrameworkEnum { - playwright = "playwright", - selenium = "selenium", - cypress = "cypress", - webdriverio = "webdriverio", -} -export type SDKSupportedBrowserAutomationFramework = - keyof typeof SDKSupportedBrowserAutomationFrameworkEnum; - -export enum SDKSupportedTestingFrameworkEnum { - jest = "jest", - codeceptjs = "codeceptjs", - playwright = "playwright", - pytest = "pytest", - robot = "robot", - behave = "behave", - cucumber = "cucumber", - nightwatch = "nightwatch", - webdriverio = "webdriverio", - mocha = "mocha", - junit4 = "junit4", - junit5 = "junit5", - testng = "testng", - serenity = "serenity", - cypress = "cypress", - nunit = "nunit", - mstest = "mstest", - xunit = "xunit", - specflow = "specflow", - reqnroll = "reqnroll", -} -export type SDKSupportedTestingFramework = - keyof typeof SDKSupportedTestingFrameworkEnum; - -export type ConfigMapping = Record< - SDKSupportedLanguageEnum, - Partial< - Record< - SDKSupportedBrowserAutomationFrameworkEnum, - Partial< - Record< - SDKSupportedTestingFrameworkEnum, - { instructions: (username: string, accessKey: string) => string } - > - > - > - > ->; From 31907490eb876a21b0212adf9d980d4336699a3c Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 1 Sep 2025 12:52:48 +0530 Subject: [PATCH 10/84] fix: remove redundant note about AI Summary availability in Percy changes --- src/tools/percy-change.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/percy-change.ts b/src/tools/percy-change.ts index 615c3069..c4713d39 100644 --- a/src/tools/percy-change.ts +++ b/src/tools/percy-change.ts @@ -64,7 +64,7 @@ export async function fetchPercyChanges( content: [ { type: "text", - text: "AI Summary is not yet available for this build/framework. There may still be visual changes—please review the build on the dashboard. Support for AI Summary will be added soon.", + text: "AI Summary is not yet available for this build/framework. There may still be visual changes—please review the build on the dashboard.", }, ], }; From 69553603ae832f3dac3af182a9ae23be7813fbc1 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 2 Sep 2025 12:23:01 +0530 Subject: [PATCH 11/84] refactor: update Percy integration to fetch changed snapshot IDs and remove unused simulate handler --- src/tools/percy-change.ts | 10 +- src/tools/percy-sdk.ts | 42 +----- src/tools/review-agent-utils/build-counts.ts | 21 ++- src/tools/review-agent-utils/percy-diffs.ts | 18 +-- .../review-agent-utils/percy-snapshots.ts | 125 +++++++++++++----- src/tools/sdk-utils/handler.ts | 41 ------ 6 files changed, 118 insertions(+), 139 deletions(-) diff --git a/src/tools/percy-change.ts b/src/tools/percy-change.ts index c4713d39..f416638c 100644 --- a/src/tools/percy-change.ts +++ b/src/tools/percy-change.ts @@ -3,7 +3,7 @@ import { BrowserStackConfig } from "../lib/types.js"; import { getBrowserStackAuth } from "../lib/get-auth.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { getPercyBuildCount } from "./review-agent-utils/build-counts.js"; -import { getPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js"; +import { getChangedPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js"; import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js"; import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js"; @@ -25,7 +25,7 @@ export async function fetchPercyChanges( }); // Get build info (noBuilds, isFirstBuild, lastBuildId) - const { noBuilds, isFirstBuild, lastBuildId } = + const { noBuilds, isFirstBuild, lastBuildId, orgId } = await getPercyBuildCount(percyToken); if (noBuilds) { @@ -51,7 +51,11 @@ export async function fetchPercyChanges( } // Get snapshot IDs for the latest build - const snapshotIds = await getPercySnapshotIds(lastBuildId, percyToken); + const snapshotIds = await getChangedPercySnapshotIds( + lastBuildId, + config, + orgId, + ); logger.info( `Fetched ${snapshotIds.length} snapshot IDs for build: ${lastBuildId} as ${snapshotIds.join(", ")}`, ); diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index ed9b7777..1af0a484 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,15 +7,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js"; import { updateTestsWithPercyCommands } from "./add-percy-snapshots.js"; import { approveOrDeclinePercyBuild } from "./review-agent-utils/percy-approve-reject.js"; - -import { - setUpPercyHandler, - setUpSimulatePercyChangeHandler, -} from "./sdk-utils/handler.js"; +import { setUpPercyHandler } from "./sdk-utils/handler.js"; import { SETUP_PERCY_DESCRIPTION, - SIMULATE_PERCY_CHANGE_DESCRIPTION, LIST_TEST_FILES_DESCRIPTION, PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, } from "./sdk-utils/common/constants.js"; @@ -70,39 +65,6 @@ export function registerPercyTools( }, ); - // Register simulatePercyChange - tools.simulatePercyChange = server.tool( - "simulatePercyChange", - SIMULATE_PERCY_CHANGE_DESCRIPTION, - SetUpPercyParamsShape, - async (args) => { - try { - trackMCP( - "simulatePercyChange", - server.server.getClientVersion()!, - config, - ); - return setUpSimulatePercyChangeHandler(args, config); - } catch (error) { - trackMCP( - "simulatePercyChange", - server.server.getClientVersion()!, - error, - config, - ); - return { - content: [ - { - type: "text", - text: error instanceof Error ? error.message : String(error), - }, - ], - isError: true, - }; - } - }, - ); - // Register addPercySnapshotCommands tools.addPercySnapshotCommands = server.tool( "addPercySnapshotCommands", @@ -167,7 +129,7 @@ export function registerPercyTools( tools.runPercyScan = server.tool( "runPercyScan", - "Run a Percy visual test scan. Example prompts : Run this Percy build/scan.Never run percy scan/build without this tool", + "Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool", RunPercyScanParamsShape, async (args) => { return runPercyScan(args, config); diff --git a/src/tools/review-agent-utils/build-counts.ts b/src/tools/review-agent-utils/build-counts.ts index 8b50b132..71027fe5 100644 --- a/src/tools/review-agent-utils/build-counts.ts +++ b/src/tools/review-agent-utils/build-counts.ts @@ -1,4 +1,4 @@ -// Utility for fetching the count of Percy builds. +// Utility for fetching the count of Percy builds and orgId. export async function getPercyBuildCount(percyToken: string) { const apiUrl = `https://percy.io/api/v1/builds`; @@ -15,12 +15,19 @@ export async function getPercyBuildCount(percyToken: string) { const data = await response.json(); const builds = data.data ?? []; + const included = data.included ?? []; let isFirstBuild = false; - let lastBuildId; + let lastBuildId: string | undefined; + let orgId: string | undefined; if (builds.length === 0) { - return { noBuilds: true, isFirstBuild: false, lastBuildId: undefined }; + return { + noBuilds: true, + isFirstBuild: false, + lastBuildId: undefined, + orgId, + }; } else if (builds.length === 1) { isFirstBuild = true; lastBuildId = builds[0].id; @@ -29,5 +36,11 @@ export async function getPercyBuildCount(percyToken: string) { lastBuildId = builds[0].id; } - return { noBuilds: false, isFirstBuild, lastBuildId }; + // Extract orgId from the `included` projects block + const project = included.find((item: any) => item.type === "projects"); + if (project?.relationships?.organization?.data?.id) { + orgId = project.relationships.organization.data.id; + } + + return { noBuilds: false, isFirstBuild, lastBuildId, orgId }; } diff --git a/src/tools/review-agent-utils/percy-diffs.ts b/src/tools/review-agent-utils/percy-diffs.ts index 94a34f03..007d3c2b 100644 --- a/src/tools/review-agent-utils/percy-diffs.ts +++ b/src/tools/review-agent-utils/percy-diffs.ts @@ -1,7 +1,3 @@ -/** - * Utility for fetching and aggregating Percy snapshot diffs. - */ - export interface PercySnapshotDiff { id: string; name: string | null; @@ -10,18 +6,12 @@ export interface PercySnapshotDiff { coordinates: any; } -/** - * Fetches diffs for a single Percy snapshot. - * @param snapshotId - The Percy snapshot ID. - * @param percyToken - The Percy API token. - * @returns Array of PercySnapshotDiff objects. - */ export async function getPercySnapshotDiff( snapshotId: string, percyToken: string, ): Promise { const apiUrl = `https://percy.io/api/v1/snapshots/${snapshotId}`; - + const response = await fetch(apiUrl, { headers: { Authorization: `Token token=${percyToken}`, @@ -59,12 +49,6 @@ export async function getPercySnapshotDiff( return changes; } -/** - * Fetches and flattens all diffs for an array of Percy snapshot IDs. - * @param snapshotIds - Array of Percy snapshot IDs. - * @param percyToken - The Percy API token. - * @returns Flat array of PercySnapshotDiff objects. - */ export async function getPercySnapshotDiffs( snapshotIds: string[], percyToken: string, diff --git a/src/tools/review-agent-utils/percy-snapshots.ts b/src/tools/review-agent-utils/percy-snapshots.ts index 5d2d0214..23c71c68 100644 --- a/src/tools/review-agent-utils/percy-snapshots.ts +++ b/src/tools/review-agent-utils/percy-snapshots.ts @@ -1,45 +1,102 @@ -// Utility for fetching Percy snapshot IDs for a given build (up to `maxSnapshots`). -export async function getPercySnapshotIds( +import { getBrowserStackAuth } from "../../lib/get-auth.js"; +import { BrowserStackConfig } from "../../lib/types.js"; +import { sanitizeUrlParam } from "../../lib/utils.js"; + +// Utility for fetching only the IDs of changed Percy snapshots for a given build. +export async function getChangedPercySnapshotIds( buildId: string, - percyToken: string, - maxSnapshots = 300, + config: BrowserStackConfig, + orgId: string | undefined, ): Promise { - const perPage = 30; - const allSnapshotIds: string[] = []; - let cursor: string | undefined = undefined; + + if (!buildId || !orgId) { + throw new Error( + "Failed to fetch AI Summary: Missing build ID or organization ID", + ); + } - while (allSnapshotIds.length < maxSnapshots) { - const url = new URL(`https://percy.io/api/v1/snapshots`); - url.searchParams.set("build_id", buildId); - url.searchParams.set("page[limit]", String(perPage)); - if (cursor) url.searchParams.set("page[cursor]", cursor); + const urlStr = constructPercyBuildItemsUrl({ + buildId, + orgId, + category: ["changed"], + subcategories: ["unreviewed","approved","changes_requested"], + groupSnapshotsBy: "similar_diff", + browserIds: ["63", "64", "69", "70", "71"], + widths: ["375","1280","1920"], + }); - const response = await fetch(url.toString(), { - headers: { - Authorization: `Token token=${percyToken}`, - "Content-Type": "application/json", - }, - }); + const authString = getBrowserStackAuth(config); + const auth = Buffer.from(authString).toString("base64"); + const response = await fetch(urlStr, { + headers: { + Authorization: `Basic ${auth}`, + "Content-Type": "application/json", + }, + }); - if (!response.ok) { - throw new Error( - `Failed to fetch Percy snapshots: ${response.statusText}`, - ); - } + if (!response.ok) { + throw new Error( + `Failed to fetch changed Percy snapshots: ${response.status} ${response.statusText}`, + ); + } - const data = await response.json(); - const snapshots = data.data ?? []; - if (snapshots.length === 0) break; // no more snapshots + const responseData = await response.json(); + const buildItems = responseData.data ?? []; - allSnapshotIds.push(...snapshots.map((s: any) => String(s.id))); + if (buildItems.length === 0) { + return []; + } - // Set cursor to last snapshot ID of this page for next iteration - cursor = snapshots[snapshots.length - 1].id; + const snapshotIds = buildItems + .flatMap((item: any) => item.attributes?.["snapshot-ids"] ?? []) + .map((id: any) => String(id)); - // Stop if we've collected enough - if (allSnapshotIds.length >= maxSnapshots) break; - } + return snapshotIds; +} - // Return only up to maxSnapshots - return allSnapshotIds.slice(0, maxSnapshots); +export function constructPercyBuildItemsUrl({ + buildId, + orgId, + category = [], + subcategories = [], + browserIds = [], + widths = [], + groupSnapshotsBy, +}: { + buildId: string; + orgId: string; + category?: string[]; + subcategories?: string[]; + browserIds?: string[]; + widths?: string[]; + groupSnapshotsBy?: string; +}): string { + const url = new URL("https://percy.io/api/v1/build-items"); + url.searchParams.set("filter[build-id]", sanitizeUrlParam(buildId)); + url.searchParams.set("filter[organization-id]", sanitizeUrlParam(orgId)); + + if (category && category.length > 0) { + category.forEach((cat) => + url.searchParams.append("filter[category][]", sanitizeUrlParam(cat)) + ); + } + if (subcategories && subcategories.length > 0) { + subcategories.forEach((sub) => + url.searchParams.append("filter[subcategories][]", sanitizeUrlParam(sub)) + ); + } + if (browserIds && browserIds.length > 0) { + browserIds.forEach((id) => + url.searchParams.append("filter[browser_ids][]", sanitizeUrlParam(id)) + ); + } + if (widths && widths.length > 0) { + widths.forEach((w) => + url.searchParams.append("filter[widths][]", sanitizeUrlParam(w)) + ); + } + if (groupSnapshotsBy) { + url.searchParams.set("filter[group_snapshots_by]", sanitizeUrlParam(groupSnapshotsBy)); + } + return url.toString(); } diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index aaf7592b..4b2bc942 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -17,11 +17,6 @@ import { runPercyAutomateOnly } from "./percy-automate/handler.js"; import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js"; import { checkPercyIntegrationSupport } from "./common/utils.js"; -import { - PERCY_SIMULATE_INSTRUCTION, - PERCY_REPLACE_REGEX, - PERCY_SIMULATION_DRIVER_INSTRUCTION, -} from "./common/constants.js"; export async function runTestsOnBrowserStackHandler( rawInput: unknown, @@ -166,39 +161,3 @@ export async function setUpPercyHandler( throw new Error(getBootstrapFailedMessage(error, { config })); } } - -export async function setUpSimulatePercyChangeHandler( - rawInput: unknown, - config: BrowserStackConfig, -): Promise { - try { - const percyInstruction = await setUpPercyHandler(rawInput, config); - - if (percyInstruction.isError) { - return percyInstruction; - } - - if (Array.isArray(percyInstruction.content)) { - percyInstruction.content.forEach((item) => { - if ( - typeof item.text === "string" && - PERCY_REPLACE_REGEX.test(item.text) - ) { - item.text = item.text.replace( - PERCY_REPLACE_REGEX, - PERCY_SIMULATE_INSTRUCTION, - ); - } - }); - } - - percyInstruction.content?.push({ - type: "text" as const, - text: PERCY_SIMULATION_DRIVER_INSTRUCTION, - }); - - return percyInstruction; - } catch (error) { - throw new Error(getBootstrapFailedMessage(error, { config })); - } -} From 2ff91761b434ec3015e9fa7690a52f3f314323d2 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 2 Sep 2025 14:10:33 +0530 Subject: [PATCH 12/84] include browser IDs in build info and snapshot fetching --- src/tools/percy-change.ts | 3 ++- src/tools/review-agent-utils/build-counts.ts | 14 ++++++++----- src/tools/review-agent-utils/percy-diffs.ts | 2 +- .../review-agent-utils/percy-snapshots.ts | 20 +++++++++++-------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/tools/percy-change.ts b/src/tools/percy-change.ts index f416638c..4b8e4d2b 100644 --- a/src/tools/percy-change.ts +++ b/src/tools/percy-change.ts @@ -25,7 +25,7 @@ export async function fetchPercyChanges( }); // Get build info (noBuilds, isFirstBuild, lastBuildId) - const { noBuilds, isFirstBuild, lastBuildId, orgId } = + const { noBuilds, isFirstBuild, lastBuildId, orgId, browserIds } = await getPercyBuildCount(percyToken); if (noBuilds) { @@ -55,6 +55,7 @@ export async function fetchPercyChanges( lastBuildId, config, orgId, + browserIds, ); logger.info( `Fetched ${snapshotIds.length} snapshot IDs for build: ${lastBuildId} as ${snapshotIds.join(", ")}`, diff --git a/src/tools/review-agent-utils/build-counts.ts b/src/tools/review-agent-utils/build-counts.ts index 71027fe5..311e2ee3 100644 --- a/src/tools/review-agent-utils/build-counts.ts +++ b/src/tools/review-agent-utils/build-counts.ts @@ -20,6 +20,7 @@ export async function getPercyBuildCount(percyToken: string) { let isFirstBuild = false; let lastBuildId: string | undefined; let orgId: string | undefined; + let browserIds: string[] = []; if (builds.length === 0) { return { @@ -27,13 +28,16 @@ export async function getPercyBuildCount(percyToken: string) { isFirstBuild: false, lastBuildId: undefined, orgId, + browserIds: [], }; - } else if (builds.length === 1) { - isFirstBuild = true; - lastBuildId = builds[0].id; } else { - isFirstBuild = false; + isFirstBuild = builds.length === 1; lastBuildId = builds[0].id; + + browserIds = + builds[0]?.relationships?.browsers?.data + ?.map((b: any) => b.id) + ?.filter((id: any) => typeof id === "string") ?? []; } // Extract orgId from the `included` projects block @@ -42,5 +46,5 @@ export async function getPercyBuildCount(percyToken: string) { orgId = project.relationships.organization.data.id; } - return { noBuilds: false, isFirstBuild, lastBuildId, orgId }; + return { noBuilds: false, isFirstBuild, lastBuildId, orgId, browserIds }; } diff --git a/src/tools/review-agent-utils/percy-diffs.ts b/src/tools/review-agent-utils/percy-diffs.ts index 007d3c2b..1b6a9efe 100644 --- a/src/tools/review-agent-utils/percy-diffs.ts +++ b/src/tools/review-agent-utils/percy-diffs.ts @@ -11,7 +11,7 @@ export async function getPercySnapshotDiff( percyToken: string, ): Promise { const apiUrl = `https://percy.io/api/v1/snapshots/${snapshotId}`; - + const response = await fetch(apiUrl, { headers: { Authorization: `Token token=${percyToken}`, diff --git a/src/tools/review-agent-utils/percy-snapshots.ts b/src/tools/review-agent-utils/percy-snapshots.ts index 23c71c68..5143ad56 100644 --- a/src/tools/review-agent-utils/percy-snapshots.ts +++ b/src/tools/review-agent-utils/percy-snapshots.ts @@ -7,6 +7,7 @@ export async function getChangedPercySnapshotIds( buildId: string, config: BrowserStackConfig, orgId: string | undefined, + browserIds: string[], ): Promise { if (!buildId || !orgId) { @@ -19,10 +20,10 @@ export async function getChangedPercySnapshotIds( buildId, orgId, category: ["changed"], - subcategories: ["unreviewed","approved","changes_requested"], + subcategories: ["unreviewed", "approved", "changes_requested"], groupSnapshotsBy: "similar_diff", - browserIds: ["63", "64", "69", "70", "71"], - widths: ["375","1280","1920"], + browserIds, + widths: ["375", "1280", "1920"], }); const authString = getBrowserStackAuth(config); @@ -77,26 +78,29 @@ export function constructPercyBuildItemsUrl({ if (category && category.length > 0) { category.forEach((cat) => - url.searchParams.append("filter[category][]", sanitizeUrlParam(cat)) + url.searchParams.append("filter[category][]", sanitizeUrlParam(cat)), ); } if (subcategories && subcategories.length > 0) { subcategories.forEach((sub) => - url.searchParams.append("filter[subcategories][]", sanitizeUrlParam(sub)) + url.searchParams.append("filter[subcategories][]", sanitizeUrlParam(sub)), ); } if (browserIds && browserIds.length > 0) { browserIds.forEach((id) => - url.searchParams.append("filter[browser_ids][]", sanitizeUrlParam(id)) + url.searchParams.append("filter[browser_ids][]", sanitizeUrlParam(id)), ); } if (widths && widths.length > 0) { widths.forEach((w) => - url.searchParams.append("filter[widths][]", sanitizeUrlParam(w)) + url.searchParams.append("filter[widths][]", sanitizeUrlParam(w)), ); } if (groupSnapshotsBy) { - url.searchParams.set("filter[group_snapshots_by]", sanitizeUrlParam(groupSnapshotsBy)); + url.searchParams.set( + "filter[group_snapshots_by]", + sanitizeUrlParam(groupSnapshotsBy), + ); } return url.toString(); } From be33a722bf14034a80c7c7a53cf1017d25554767 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 2 Sep 2025 17:13:05 +0530 Subject: [PATCH 13/84] refactor: streamline browser ID extraction and update authentication handling in Percy integration --- src/tools/review-agent-utils/build-counts.ts | 11 ++--- .../percy-approve-reject.ts | 6 +-- src/tools/review-agent-utils/percy-builds.ts | 43 ------------------- 3 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 src/tools/review-agent-utils/percy-builds.ts diff --git a/src/tools/review-agent-utils/build-counts.ts b/src/tools/review-agent-utils/build-counts.ts index 311e2ee3..63acbda9 100644 --- a/src/tools/review-agent-utils/build-counts.ts +++ b/src/tools/review-agent-utils/build-counts.ts @@ -33,13 +33,14 @@ export async function getPercyBuildCount(percyToken: string) { } else { isFirstBuild = builds.length === 1; lastBuildId = builds[0].id; - - browserIds = - builds[0]?.relationships?.browsers?.data - ?.map((b: any) => b.id) - ?.filter((id: any) => typeof id === "string") ?? []; } + // Extract browserIds from the latest build if available + browserIds = + builds[0]?.relationships?.browsers?.data + ?.map((b: any) => b.id) + ?.filter((id: any) => typeof id === "string") ?? []; + // Extract orgId from the `included` projects block const project = included.find((item: any) => item.type === "projects"); if (project?.relationships?.organization?.data?.id) { diff --git a/src/tools/review-agent-utils/percy-approve-reject.ts b/src/tools/review-agent-utils/percy-approve-reject.ts index 0a084355..ba4bd439 100644 --- a/src/tools/review-agent-utils/percy-approve-reject.ts +++ b/src/tools/review-agent-utils/percy-approve-reject.ts @@ -9,8 +9,8 @@ export async function approveOrDeclinePercyBuild( const { buildId, action } = args; // Get Basic Auth credentials - const [username, accessKey] = getBrowserStackAuth(config).split(":"); - const authHeader = `Basic ${Buffer.from(`${username}:${accessKey}`).toString("base64")}`; + const authString = getBrowserStackAuth(config); + const auth = Buffer.from(authString).toString("base64"); // Prepare request body const body = { @@ -28,7 +28,7 @@ export async function approveOrDeclinePercyBuild( method: "POST", headers: { "Content-Type": "application/json", - Authorization: authHeader, + Authorization: `Basic ${auth}`, }, body: JSON.stringify(body), }); diff --git a/src/tools/review-agent-utils/percy-builds.ts b/src/tools/review-agent-utils/percy-builds.ts deleted file mode 100644 index 749e57e5..00000000 --- a/src/tools/review-agent-utils/percy-builds.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Utility for fetching Percy build information. - */ - -export interface PercyBuildInfo { - noBuilds: boolean; - isFirstBuild: boolean; - lastBuildId?: string; -} - -/** - * Fetches Percy build information for a given token. - * @param percyToken - The Percy API token. - * @returns PercyBuildInfo object. - */ -export async function getPercyBuildInfo( - percyToken: string, -): Promise { - const apiUrl = "https://percy.io/api/v1/builds"; - - const response = await fetch(apiUrl, { - headers: { - Authorization: `Token token=${percyToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch Percy builds: ${response.statusText}`); - } - - const data = await response.json(); - const builds = data.data ?? []; - - if (builds.length === 0) { - return { noBuilds: true, isFirstBuild: false, lastBuildId: undefined }; - } - - const isFirstBuild = builds.length === 1; - const lastBuildId = builds[0].id; - - return { noBuilds: false, isFirstBuild, lastBuildId }; -} From 8b5a86cc3f6c6c267b113c0afe5745a6aa22a7d0 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 2 Sep 2025 17:23:11 +0530 Subject: [PATCH 14/84] refactor: enhance walkDir function to support depth parameter for directory traversal --- src/tools/percy-snapshot-utils/detect-test-files.ts | 13 +++++++++---- src/tools/review-agent-utils/percy-snapshots.ts | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/tools/percy-snapshot-utils/detect-test-files.ts b/src/tools/percy-snapshot-utils/detect-test-files.ts index bccdf715..876b738f 100644 --- a/src/tools/percy-snapshot-utils/detect-test-files.ts +++ b/src/tools/percy-snapshot-utils/detect-test-files.ts @@ -16,8 +16,13 @@ import { import { DetectionConfig } from "../percy-snapshot-utils/types.js"; -async function walkDir(dir: string, extensions: string[]): Promise { +async function walkDir( + dir: string, + extensions: string[], + depth: number = 6, +): Promise { const result: string[] = []; + if (depth < 0) return result; try { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); @@ -26,7 +31,7 @@ async function walkDir(dir: string, extensions: string[]): Promise { if (entry.isDirectory()) { if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith(".")) { - result.push(...(await walkDir(fullPath, extensions))); + result.push(...(await walkDir(fullPath, extensions, depth - 1))); } } else if (extensions.some((ext) => entry.name.endsWith(ext))) { result.push(fullPath); @@ -112,7 +117,7 @@ export async function listTestFiles( // Step 1: Collect all files with matching extensions let files: string[] = []; try { - files = await walkDir(baseDir, config.extensions); + files = await walkDir(baseDir, config.extensions, 6); } catch { return []; } @@ -151,7 +156,7 @@ export async function listTestFiles( // Step 4: Handle SpecFlow .feature files for C# + SpecFlow if (language === "csharp" && framework === "specflow") { try { - const featureFiles = await walkDir(baseDir, [".feature"]); + const featureFiles = await walkDir(baseDir, [".feature"], 6); featureFiles.forEach((file) => candidateFiles.set(file, 2)); logger.info(`Added ${featureFiles.length} SpecFlow .feature files`); } catch { diff --git a/src/tools/review-agent-utils/percy-snapshots.ts b/src/tools/review-agent-utils/percy-snapshots.ts index 5143ad56..11a48717 100644 --- a/src/tools/review-agent-utils/percy-snapshots.ts +++ b/src/tools/review-agent-utils/percy-snapshots.ts @@ -9,7 +9,6 @@ export async function getChangedPercySnapshotIds( orgId: string | undefined, browserIds: string[], ): Promise { - if (!buildId || !orgId) { throw new Error( "Failed to fetch AI Summary: Missing build ID or organization ID", From 96b6b99708b2298319e1839203ee469f8d87bd74 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 2 Sep 2025 19:38:44 +0530 Subject: [PATCH 15/84] fix: Improve cache handling and data retrieval in getDevicesAndBrowsers function --- src/lib/device-cache.ts | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/lib/device-cache.ts b/src/lib/device-cache.ts index d6dd0aff..65ce7d96 100644 --- a/src/lib/device-cache.ts +++ b/src/lib/device-cache.ts @@ -34,37 +34,41 @@ export async function getDevicesAndBrowsers( fs.mkdirSync(CACHE_DIR, { recursive: true }); } - let cache: any = {}; + let cache: Record = {}; + // Load existing cache if (fs.existsSync(CACHE_FILE)) { - const stats = fs.statSync(CACHE_FILE); - if (Date.now() - stats.mtimeMs < TTL_MS) { - try { - cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8")); - if (cache[type]) { - return cache[type]; - } - } catch (error) { - console.error("Error parsing cache file:", error); - // Continue with fetching fresh data - } + try { + cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8")); + } catch (err) { + console.error("Error parsing cache file:", err); + cache = {}; + } + + // Check per-product TTL + const cachedEntry = cache[type]; + if (cachedEntry?.timestamp && Date.now() - cachedEntry.timestamp < TTL_MS) { + return cachedEntry.data; } } + // Fetch fresh data from BrowserStack const liveRes = await apiClient.get({ url: URLS[type], raise_error: false }); - if (!liveRes.ok) { throw new Error( - `Failed to fetch configuration from BrowserStack : ${type}=${liveRes.statusText}`, + `Failed to fetch configuration from BrowserStack: ${type} = ${liveRes.statusText}`, ); } - cache = { - [type]: liveRes.data, + // Save to cache with timestamp and data directly under product key + cache[type] = { + timestamp: Date.now(), + data: liveRes.data, }; - fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8"); - return cache[type]; + fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8"); + + return liveRes.data; } // Rate limiter for started event (3H) From a95b196d1e71dd86a6b2d5389c2324756ef69f49 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Wed, 3 Sep 2025 00:03:12 +0530 Subject: [PATCH 16/84] adding remote mcp installation instruction images --- assets/authentication1.png | Bin 0 -> 50484 bytes assets/authentication2.png | Bin 0 -> 106688 bytes assets/http_option.png | Bin 0 -> 157418 bytes assets/remotemcp_json_file.png | Bin 0 -> 87723 bytes assets/server_id.png | Bin 0 -> 100209 bytes assets/server_url.png | Bin 0 -> 112252 bytes assets/signin_success.png | Bin 0 -> 70533 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/authentication1.png create mode 100644 assets/authentication2.png create mode 100644 assets/http_option.png create mode 100644 assets/remotemcp_json_file.png create mode 100644 assets/server_id.png create mode 100644 assets/server_url.png create mode 100644 assets/signin_success.png diff --git a/assets/authentication1.png b/assets/authentication1.png new file mode 100644 index 0000000000000000000000000000000000000000..5ca7de8bdc9765ee69b092b035926c00cec1801a GIT binary patch literal 50484 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 G0{;&&o7zSI literal 0 HcmV?d00001 diff --git a/assets/authentication2.png b/assets/authentication2.png new file mode 100644 index 0000000000000000000000000000000000000000..e76cf5f23407a503e6bad7b183ec19975a109d28 GIT binary patch literal 106688 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 t6 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 literal 0 HcmV?d00001 diff --git a/assets/http_option.png b/assets/http_option.png new file mode 100644 index 0000000000000000000000000000000000000000..4b63c7c67c891684affb0f91f001a432d807bee0 GIT binary patch literal 157418 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

NPo%WV+MAnU`^PUfR_ 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 S`Vx)|CMn2~ z;46z1F^fWFupdSwcanMOK)A9*aPfx^SNIk*5L+JZk3r}gq*HC(Z};BIRjRxc&1HHO zpdSlX9xvw9owt3SG7 zSj<2@SKrRn%#MzS`aA0JAqjsp=yuOJP)VhV`Va5DDqZ1IvIEshMLVe7?8UpTiv||4_R0ufv%>L$veLt)aSzFeIoI>ZVK1BIv_pN@vZh%qa^$O5 zf0q4e+|qH%c$foKm!KeA!OX?_(OFNM?wX9x$u`O0o?z*Wer}!j?e;Ig1kZ>-kcfT_ zTzQ9u%Va#VN%5ugx;Ezi#7Sed`n{+c@lKncSMmHF=N_l8n5|4qJhe0F@?O*&L6xoQl+XwDn3*JntgP zk^B7p^Pfps&&&IpfCG-V#P14D2U`C&BMlm$bFRP3%gbMUOiBhNJQsP;D}nJ@9YiOF zbxJ>#j-IALM2^O}4kQfDJ0H!H!&i9#lljZPFCT#8;|ckVnVj<^{9@wA7Pg0}>Lqu^ z3zS+0>Ze!gwK_=%S~(w(Uw%t+I`nz?rXlU>^YhklYx%LSb{SJG)kTk=jF|qju(sVX zE16TDfZ;@`_1>xc3ucm@IE^2(enM;t

AaT6<>xAuO_`sr%&_^u<;*z|zz4%>zYQ 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 literal 0 HcmV?d00001 diff --git a/assets/server_url.png b/assets/server_url.png new file mode 100644 index 0000000000000000000000000000000000000000..fb049ae271b6bfcc4d57336d24c37fe2732bec14 GIT binary patch literal 112252 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≈b;Gr}HFVtul6bdEJ&9fq_|pOD`x-`eO^ zYTxH_^Kukfs*2WiYY?UQXb;*A`%64iBuFrm#@YhQ_{Y}7w8$*R8fb|gG}0+Sz2&uO)#;6bJ(MrpD2=h8faW{LaHxouGW}4`@UYzU#1!e&sx6H*S7$kLBG~A2z5S zNG9gg%_>MJQM}KN2&IZVq8gk{hRZ%Rs`ob$Ic~C;`8!?nxPRoH0$K;)=P1&m3#pOAM^J^~F~N>6I090>7puJj@y`JAPb5VELiB6xg?D zc(K2Im&O~@a}eO>4Jy9~5*QT}X0WzA$H zF)Kjs4;tCs{sjOTecb$y_Xpyc$XSyQw#}MW+Xg&^OO69UcWQH=1# z+#7dtZOlHIBQy))ebq%@O|iz4H(s+ZH8>BpH~-7AvZ;UTDjEK-PZD7At11sD*Wa`9 z&L+z?v(EKs8z$OL-U0P2N$Z)c+A4>*Dmg!Yu>LExdQ|Z5gpwx*DXZDt{m{xKovJt( zZP5=iGSloSk|O)+t)dt|D;iFQI^Zw;5DYP#=#(G>d^o3O1Dxypp|1!k#7bqJqJdGz z@<>C)m040AbiRtUysN~!1(g7u+wR!glz#>0Al{lzyD#5x;#%pF^v78oP>o!6Zl?QW z9pb!C;krS?uZqB@7PzNDz`atNtiOASuMaK{cpiZfLA1#nHe+qj91fa(5WF`yzu|7Z z?%uPs{Yi-PK^1OdaxWjr%BWYJa{lgRMJYRa>)m6bOIdG&1?L=e1b4Fe17~bP);)xM zL`dg@Z1*B(qvyC%Lmn;!Cb>T8+4s+L#xPNA8meo{$!VLqYbHjoDZh@Qh5RTErIIAI z4|A=z?##SIWo;?xKEgVh&hdANHO8Bs(I7M3Xd9l9byHPm;3cV--Q9uf=63Wyi04Z0 zhy7!t^j^`MJe?Ce-hM6D%no?+5M0Uh2*a7_hA-T&tDYF2#zcK;-ONGXv`zPf`O?ET-pZUT>(C`jd>b zoRVvYWcU0MP^eg&Q}^U{yISM9YLG>7a64FrUai6HhnpWQqJLr|4k)Qy771gD;S(`!*XbMWZY@0!($R&aoy1teBXxezY z+La@~49?-6Ab8}o*Q$nVaPj0TX-T=kJu}~?Ja{Rw@pIv!1+%J*v1i=*!ZY`1TQgk6 z4N3fdKUoUuC|EUucE!A=Njths z0q0()ono*s{AJu-#M7}M>`H|O?DHR-xjH=~#2kqWIW>;@|8kVXt(x732X4a@Sp!Q= z%l*Q^OUDb+DG}6tlp& z+SA?!QOUQX6mo9V(eW6z77OOAS9r*9sY z-QHh7n-h=waJQz&r7rnI@&-3y+U6b;F>GB#s(Ly0fsNey#(~Q5vvS=gTeiUu&8;)8 zrFve*urWWUx)A)6db;1_3f(GVCaB2PNRb+l};{B#?F zMg9hu5>}AH>93Vtm%G9GZufGV(roI^5eKvyv<%*n%m@%R+xmhsmd~Cqjy^m<)0=QZ zA9~jG(QbjCdgBrnei%G(1PecyKir!O=8+TOJmv+m3iGI#2DmvM9*w5b0^7*=X9USf_ee8S@5J@!KSk&{4&?r4L)9X>TkPn=cMf?bU zkBw3uUv+9}u$rt;YME@2B4|z5(mn_6@%l$1%pIko--&+}>_stcv}Zb`pNv78lqY0| z$lt>BiRE45;x)m-s4|~t867m$I+|=vWMN?0QofR=Lpw%3p(5^{*EGUQEr6=Hb(nJU zw${!qlNKZKO%9KpJj`3(}Y&dHmzJ7*})u*)w7BJG-i3Oe1=8QasntDN9) zHuE4B8g6*rHk^72WS$E17R5R&+3P79%K`T|HA>BLi%F7_I)6RPb*IZ+KWfp$t-oD| zdE^St4J{7H=6t?B!d4t2KL}f9sv11D>o16ZJtLm8B!KF!(w1v>fqnQ$C}c_z=;N6-CRDYmC_*%XNkAmFZaLqJ^ELB zl)sp#B)&t}o6Dy*1%Ag=^9oN#Y;TP!1}6iQLP>@b)Rt4mkx+u8-^N`1L7OoACM{1z zSkRx+X>FZ9!5#m{!#vj0qfjf;KKo`4c4j(S+xcpd2%2HPo#CA%+(jJZxp^Y~j`aV2 znhQ_YewDO+)EWm}<6Vw>&V95)39@#w&t!9DgJpeXU&wl-Po`J89RCfK_dp<`8w?sU zDI}BAO5daaUo};*L$b&c%g-5q!?EJ<8}sT*(KYi54xbXn=`UET5SSl5{zrSHkdU3m zrbbiTuViW2aVZRSYC!i#P+hPLE~tYJk>5AIX)K$iyQBEd=iA@cCdO}#GpCZfFE!Zq zc3OKsZJ|rbKk76cn7CB+dQMzSDN?QbAg#;yjl+_QUv9zx{E5=IqLJ>O`+rVyu%zTv zkF#e&2V&zd7JbKGZ5FGRnmdDEY7#Z^&#P)#Aqgd~z`;aqvx(LPLmB4?52hcyjItP z*Ux?%V@ydPDrhJcseVppj64pT(U3s2w>F7+REQRvpV>)Shu4k;cN~i$5j^U!2l-o333;%~wO#R>C>2hMg*(ueZx0ZX?}GXOxmcwtBCPYK2n!2VHhMu}{?OM-L`54xWX) zYm-jESj#OvVq9@S#anC4H2R$Q_jpFZqp7$3@xq~7Qv?i zs=)F!_r}3?4E4Ab?Z)hszT8ddi)`9-r037aQd)!LtGd!kv>({762Lv^vY!W^4*OoQ z33MziQa#j}5|i5Z$k{*QYEEoU*jx0Qigbz4nbU2_g0HbJ&ZB!qFC(RvD>?WU9i3X@ zzTmH^#ng&WImPQ+p4_&s_$lPkRlv=fn)Qy)_6lAm?ax32;S<6uy+Le8P^gfsM6!{tUstWbMd6uSWEaNtq|p*nw=u0OsW(*_^OD3RMSe5p1MC& z&J&rC6}%%?8T@JeWp)V9mgj@K)Pc^$m@k55o&qiEES=Q4@9@|Mxo2EC#UdUoUOdyq z)YAM-G~V>bYr>#B9%_kSoIl2l)Na;(2NoXo9_Bs1z55iDjCSztJMeI{8B+4k_jp$S zsEAcnyE3rX<|2;jDZ4o(G`Lb}Ec;v5S=L{8N-eRGI_jk8x=!#pD|Bl@IPx;oXxZ17 z`;TE^pG;$urFnMT#d+Zl%&&MUAO`Au$?9LH4-8q5CK_sWZFAq`OdDi7>ZQD;VL8Jdyr8Y$74L?tbNg&yNZtuk~sx zj~C2Bom6w)9nqVGC?R3BmBn9{nCND7n`dh#8OOB8LbXQ)je8n@yrkALA|Q)6p)d~tMPlis{3(3>l8b&j z!a`(TIE$@DSrS~)c#fUZ#+2klrN+dAx5`w0c)ne;z09zmd`UaiV($(gDznZ_73Aa0 z5bqgmwO{evDbJM;aO+%5yU5@5XV z(hssnL6RqFKP5D&!5q$Wm=J`bPWd7`Z}07|0~te(*3S=2BpB<@y{vnLUsvhrd-)?I z-Aq|6FoHZ%K^N;&{gZ&OHXBDj^W^-nq_$O~hd>7hpQ){u%zOXg)!@{z|AaH*1EbBE z<^$3YwU8`JaXGQa+?K=V*-Gd#iX;uEZyK1;?m1S7(niaJH3>SL6F|3{t<~|A*b~=u zOR`VbU-k)bG}MH)Q>h2*6}VBPb|Yuw18Y;NAbhgxy)Nxo2!5(pwya)XP_(u9)qN#u z?f`ew4ImVU2yYymWH$di=Turgn1uWLql^bGe#Yd?l&kUsYkU~x;dzh3?(rb#$AD=g z_<%-ajk~Xhvc{CARK;)p6GdM31en zckU3ou7R52j-i0_3!A#;CSq`DF!TQYGIvQ_M}ln;zsyfl5|I=0IQQMB!K=B5>Os zi++af<;kF1kW>F15+B)h6#l&oXPAbbQ>q{{2@|;Kr9f)qBIjev<$?*)318;iAj%0> zVV>6b>RV+c@f(T>50%4YNJ|7#==gKX|I{qs3|WT^!xUT?Hld!U4x`UeR_N6u zy=qx8%5{4%8Niu^C#*$!FXF)hdx#!|I*=|~g8xZAx53;`g$K!-JQ^LmUG&&Ub6w4g zEQ=7g#UTwnx*dovz!{!m!=RKfJq>vY!U0dx0f}e&UpJSKBOtK6$#MeVk`nKmJWJzI z_P*P_+?lTr8NK_(2H-3w-*NTBC&TQKhou#FV)FY=gl3#(Z@B>ZGImxtdcrIDJ>+{< z07D!wkG{^k(i5&jOQx%Xaq}6CJ3LJ%k=3cdsg1IMzuEGPs9QETUOe<2{=L$4yGaIj zuj76@l{YlQ8Do|K?ERMa@NuBRO$3HNr7ZEPoqyRCX+TqS_QBPNmX2jAuDHz23b0u= zR4z|m>;;Y;6kGU{Zv=#7rh9FXCf;S4SCjg zwGe(-vvJUFUK-&pE!u~9z5r!4s+}#C3KyFEJr{0YAi-iJdK_-yRHxS#>`?7jZGIUo zmVrvZST_g{a>XTCZR}29?-Q>I?O_I6hQAE8*<}jI>I&$qN zLQrZt>|@?@kiQzxezSlV;~n`oPMF~6v1ewm2wEA8_!r@}^tR?Ebgb&<5`{NgO~B(n zQQ%Rs=f%M|e*dQymb4*9cj%`-D|Hy^*%Jy9j(`l3FOcMV&XyF7AVsr0_AVoYN?Xht zIh_DYj~7xp4Mf<*@h{f>eP;dNW;`y18CPC6+iJ@ZAMfJceosz?MiCN z$ay%T!qk@p`Z}OIZ7Q4@!RF@G#1*Cx>P&%lk9v4b+^exG;iJC)5h&h~y9T92$Rm zqv@|x(o^)=0_Pbnl;@d7?k36fxL`MXp2+tM?8WNJ^&nYj(yikMR%$LASv-65Qba*9 z4E`w~Kt0zSw1|K$T7vqRT2*-VaB3s4+sPL6 zGu4CbjbCG`!{XdCe_y>#ZHZDil(mr`G(GhGIlVF|aP6`S)oSP?S29OT1wb>7n_QXZZj-Gp4MY zT&Sd|rt*k+ig6 z?kq`{IHZL#?&zpwMpaP~&C5cg=H0%wW`m}mEnR{3=6|7Jov;>}r<`a?u!bF}BC$6k zt-TwbXTz(e34IIJ^Stl4nOP=1yr+-?G;Mm5OSB8~>+?NTWHM`O4Gt1!EznlkYSS<0 z;I!1xi@sHKl@^*bqUMQ@g_V^4xWW{T6)m-dxa zLB$V49Q2TH>RW=sHhz@0CL2$E9E)*kv7@XshiFOt$AXToi;B;|?)OqU{ zL}-fS^!Y4)e#DdNnTN6lw^`udohiDkKHR(5{Rv&yrewf2MH#*9Y$M97*x=8!e@bCY z^GeBuc^n(Y#fN?z!M=d?WMdX3*_S4QRZsB9v0LE<4!tQGIV=#gY-7liD#k8`iR$2m zX9qoYsx z&B!LF>e4$F<1r*22(fQy9{RjlJ>!E%)EVZWy9M^Fc2lFIfLfi$`(_a-sRhcW+s?&Q zwrB&)21`V4P7L91!@|ouz`!sZ|5O89v((SfA;_FqlDoSJojKxe$wCcx#7l^hzEWP?<1hYlccr zKP`uKpBqSCf5bZSzgR>r2sFOpAI71xSb3~!%;;E`sg3qgt>VVE+(T>wdo_lz`bcC= zE&L<>$(SEXq9xqT5BRw8KlwuoEOJ6qezzG7SJ(JwAQUvn zI=bS|+h(#7r50SrW8%+?1e(Ykrfn86YhxYUMbr^J_*ph8nTh-e3#9AtLz-(#^>bg!mB1tgG0-+hL@H?vNFqpzjMQ| zl_zQ6_Ch?RO|<~BHL^Xj%gWS^4DfmJSH02(d)HmZ1i*jHUwVsAj(yLJ=FDGe7`CXl;{yce zurl)|nHe-6;xcGiN{w@&4YzY8wM*Get0GmuVZ)sCo*=5hUX}fT+aX$`C%%cF$I4ibtw9DJu}P?n;1$Asg{SPF3?3G6eBd>E zesga#eD9mbq*X1lFMlJW*D19s?Z9$fPHhpb%phePU03cHI+Cp{?0iHp4{QfZgE6@cJ7L*UW3zfA{8IiuSwha_DtV z5Ppy7KeahF5Hkn1r!OxE!+YX6N}t8XPaBF7Y=aZZ<44*IUxLd|i%dEvUF&V`Nt z^Enm;mM#W7BAc=)rk%?#B*imn+}TNO2j$&uGTIA@n80bH1(oq{aiF1OaMv!!NwsTT z<#FV_X(;K6lDrPO@T19eBCJEm7aK&y)1!ix78f2uwt%PU<`d;R=GAH#I~HEsxa&%_^E`&PNHR@Oo2VI!j)DPz%uIm z@rC>!4Pk$TSl&B|m`YS&zTn(nHG@vs;l=XBn)a#=jOpi8*z_azQnASr>0;m;Q)Yy2 z$&!6zXIrp1Fgku8$FY5xbTG6^Gi3^Lbr>B8V{7~N;Y@66Ig}IPCOWvV286K$&nfjh zuDDG&1t6;?l-p`GUm|?#@2H9~DK!ZraiFoIWlDVZ#^^qvt{y0Da`Xml5qI+pSmYxX z%&LXum6whL4PoV87#N9Al%Ii^k+5gh%a`G0zK00m4kyOX-=4pRGfgmtv$%LudgpVv zA?D%i{}@INkjb>pH2C4!D=iAoLBH7BF3UccUvzetlIHGW^s97)NAY*|WfP21qY|c< z`&~Hh@$aAFYT`7kEnL?sK4OQ5ZJ`F~IjTK>TT5XoI;o4f1V+O-~s`#Xf+ ziZ)YwJot4K33<*&3&$1K^ob;ck!R!iKfOF1yfDHIWYEj5;?`!eG?jb&6rxCxs&Ma^mxs@rzaZGg?oelMm9dQ6^~QqG%$L7l z)cFOpmfCnnSjU^C)o`$hGvk{s=D8VDO+ zvDhQ$f7Xzi$EvvS4@7@&oC5VNyk+1je^Gtrv0`(jJyiA5DN`r@Z!~b=e^D1_=%0Vt z-@gA$lg>W|MrFik1W`Ny;3 zMKoX94+Q@4eEvXAPbY`-P}ddT=7o#X=PshCr~zD8D$LohD=JCN9ehA#uTt&@TXOn2 zlqG!+^%u>OG3VC?TddOEgBPV2C20^SR-D7ncSYO}uP*}Bju}XSMwm{gC7QzbJ~o|) zQ>)d?)SbkeY_NjYZ`dHqLOl``#QWShXk5g06$kCV78qN#cT)u3Y)#~1GK%XYD?Ba zYoAD%8WnTiC5(Q*=#`OBzu|HT*lPovNj3%;1p%Wg3g3)4J-=CYB{*&FxkjISVFZy^i4YABjWolNvlj_R=Gv|DIx@FH%6hQ>*;ttjL_d|@V16hguVGU(%nS(w&X1}_%B5X?N1Rw8zYay)22LVo2^UW z%^7gH!oGsdm~>EP<(1H)kM_@;=1Ky3YiwNeyDFbnbqY-KT zOMGx%u#UhBYN5_K-~k5IVu(PcYZ^32YW&0>(%N3Wk&N*MMV}X7b}ruHLT8kJmnn zH{(M_O)i$MM+$m)Xe`$-PGldv=6n8Ob;2oWS!7?k-N!4r>l!kZruFwk1+C$&pL z7@rN!8_))SYP*uV-#4kl!PviQP6jid=Rt?0g#;07SX^yqbfXO1BR3`9gku8%8SjWi zUl+as_9=tth);a$WYyCfC2PgJ1otRqebk|?7xKC*;sEX8Z&{0%HJbjK z+INbq8VpyeuvgWE$&576%#X6%iPs&ss$cF5$70IF;XpzQysHx5t^@!R0HDyTB6w3U zkhMSGK#Yf7_AL8NM3BC?R3PE(0+^lve18YzbgIrS*93+C`+_B%(0KMX-i2+}q3a}d zyKD_HBK-n?SW!I`NYj{&7wWO^M=P;eKDq%a@ zf-(Hlf2Hd;T6$ks8q9ty+ekhR-XG2~Ed|NhEmm_LxUd>Mvc=$8pLXxUX(QZH(?kTu zjL5heR#{rz%nK-3mNyWM$xLVFEqZ<3yYp|5HvbiGFB|CtloVx~W56i82iwp@~hUJMV1yV@Fs%x6uA@JZq4-bc5L}Z9Ee#3J zXq?TtON2F`bNFBH70qnkAXIgPnIKaTEl&q6rP0nW=WZ@9RxH(!QWfCIg_;;;*P;0H zPE5&uvjji!@E9}k5FleKY68t9K#+znWcOq@+s@y1RS>Yr_&1bl<1g#NW@4B%FAK&@ zOD0|?9-Iy_L`7f8GLA7`WTRza;6Eq7h#1hXhB6q#J+VNRue=0ozh$6fxkY6kIDUyA z0^p{Y80G>q6szK7_LEnH>-9TZTk;~{OtvW4oAXOA@I}ISE7Gk6ph-cY7|%$DYAqoJ(&U&+4v0@-}wYv`bD2!?t8ovwP&9$Upp+ zAIf-}0F6QMoIiHhGx>R@%Pb*Q6Mis)5eF3F%2IJ=DNE*;cje%8Y=nwV)4RF#LQHYc zC6Z)@!HUaZVSaQ`biI9q0UOBLY08^W>P30HT-+O|T4b^g78JDMx*Ns$u$|uJj|8WP zXN?b<-ar5v0AY>NRMen|)bv|W=--DkC*eeKdQqrNj9{BV2DhK*4+RIj$nfBy^NkDx z|JXT$s>E*yu$=$hpBfXw1H&$LdlhR^ZIoj8p5yYgMwpo#bBFqz6uoD9N)^4zbWn?6 z*Qu$4irYmbCkdmeL4O*5^oeExyoCK2#)stVA9@Wr>d749*H(D(xb_?_fm^Gt!0>nT z%CsZLEciJlVA1vC?dL(12AVa!fvYizt>ldA%Y-XxH*q{_Y~*7AuyYpEEHHwLwUe8? zKd88}!|PiWasQW4(upwnRtHa?gnq8$F9Q?$l9steSYE21AqAjvA=ACe!up;OC57Pi zx0s{G0dD`3IVw_lHLLqfWqkWB-B(l4r|SQwyYGH$I@|W1QAbb_Yy(m=jv}K{M353f zqB4Srm{C+fN>o5ZO2CAkkO2!KB`QipN>spzlt}N16bZdU=n(>h9ug8#N#4x4XU@6z zz0Z07fH%K4L9R2Ti!@2us@^YZ67mePv6T?MU; z>d4Wow|?A79l&ooMK-A3SRR`5`?IR^c|uxm#%0cF>$u)t)d1uWw)bWIjW_T56+OT1 zF6jJOUG1bztbvQ4+HVA-5%xSCU^z>VCckra6W_KJ6Bd*6?D+-2cK+2g{A+Bc0%{-& zHmY~fR{bJQj0*p8=BInLr@3;$05M*=F4Y4F(i-@ta?PF%F(eb@nns$RmwFVE_deDn zMq8slWq#y@ZiFs*`+e6JL;5Jq45hT~lC+|I25%%D6PWKEya&YdD(0g*^h68GTDvje zTTzET>Zn2@q&9T-d#HnE)Ua3_=`7e{-gqg$5FBe=)8nSe9`>_(4T{KiZ}#`M@j{_Pagu@Wr9jze-jeO4d+8)5v{lpB-2cOEuvlkDC#{I*3fHyF2# zw%okhYyP?UIV|I+x*hL_=NceVUp>CA{u0b2F8#o-*;Vf}O_{nwE^PE#N!&}J)f&gU9FRoW&P|cp ziMC-<>DMk6sjXDrol7)c|>N<=R`I5@$|(GigH}?TJ7aSugK4_ z-KLTmb>e0$ToOwqaXaCOyq%j^xD-p3EJ-(ElpVU*uRy^qjUDJaY{An9Q7Pw7n8(Aa z!c|P%6~O{;&Mhb>al)?7sl3T$ljyDF+D}^kg+YC6A(^re`(B4WP*vKM@WHcjdMPam z^cuE=U8D6-zfFFD(pp9~=bd_|viMa6TeT`nDWpZx}_x7FD$LU zMSKt1-1btHa_^Z%(^0Ef{-s%~Ve8Btm9|r#+5jf?GxwKtUWYo|L?`PXy8MRI;=&s1 z>*T0g20G8^DPQsRYa94|^7p~%;yD@GMcUcsrtu?vS?+ZFvn}Dl{#$(6B)`ws$|tTV zoOm{IRH(+?zuvdg!}$tVCCW}KYO&t)UJl9wUi8u#OVlg%aPS#`-Sc;}v?_fMXnIH+ z{MAIPazp#}8f`7_p7`_FOWit^+jPeC38g%3J5oEa^{N!Cjm%26i!iY0A;rNiXL-K# zzq&#z9z&H?mG2EN*z8S|xwPpB#9GX+bBcxOOw zUf25qC5EFlXNK90n1`LuEpsS$>Ym9(`FyYs=VXU2QU_0qD){zlb0A)W?| z3Del`F@s9zwpi&t{pH`3(2F*-0jQwRSjxhb^AaOCy&@0Nb*5o)G{*+6XYefK?X(Ei*`X-PcuOUd+b z7U?-sD7rEES-eM!@8r7EeJ5CpaIDLOkZ4FQ{C3ym$N96+cxX5P0@ISqw&4I07O}=} z4d?whwy916`43gKVn(ue(B(&5%q(;PTO_@?`T3NISNzk2@8+HY#GkZxszQFrqx0Q) zrIH&DZ){dohu;ByA-di@57C(8#Pqk%E+!{;mJUB82-_&Mi^Hx`K6ex;$(|OPuEU-# z!q*P;SFDxfdMa>oVnRCRUZ}J{PDC~zy;p~H`{MkK_Dzf2cK6e%utm8o7A0}fJRAH# zhOg12;ug&JqLEi|mZCM)8M(DxiY}AGN)i0mwBdyO=Wu145A3O;cG?rz^{TLhUFR=i zHeVNtF{solqc zx)8jn=cd154l7;$;fJF?+Wvm_S_|!N&F~v9_RancuRDaQyODM$ZL~uCk7MH&@1Cl7 z{-if$wqg9&J3AeJyD@j;1#X9R?`Kf3T=VDrHuyy%>#g8+3R`b%<-Ff#wd4NBpPehd zl;EF#WqI}_a#A@$Jk|fy(5z|BVD~n3I|5PU_drkG)##$?JwDUv$6)0GrUVtFYJTgLUmRd5!{UVBD-bVv-j9 z@}bmTd|4L@nFd(ivAN^5qZi_py8Ih}YsxNT^>xK#k5fN(FCEzrjAFjfAu(Bc*i%&- z0fw~>5cHYNy+=h)6ir}v51%W)Vk`_Ojt?zN;eX@=-}nF0Rn?%U?a_bY(SA$xL-kQr z;w`+mr(oSd|H%sx@2)$L-?7HRgK<=RZY}_i~<6fN>w(oR;$eEai;rQX{=e$1nn+37dM?`5dZa! zyTR_j)8;k4Z`+RMAR3d>gW+lZW4brX71zElyd@{@K{pjQANKnh3lH$!>f`)s(DVtZ zRc{N@4_mjWpbOHvv4feTTDz8qlyF&bk6Igm0v4NUaw8IOSeZhO5X?{&H+1SYud9wBY_8XwzeGw z9&I91H_wMJ2V1sve=^7<4D((sERdQV)x#KX><=@2Sh5^0;Up}P=j?m|`4rZA zcd=%!y>-7Y9H7(PEDCFruK#-1pE+^%{a#|Da<~0iWY9h(bYE^o4&+biyShL@0`sf? zOH=XPaSQ^YkD-l2Z<&DAr8`qtO1!il1u%;oCq)mdpzqc$+(oJJn(Q&yq;`~t;DJXu zrre-ZDF;$jVwn*D8U>vClHfkFN#C1TVF{j)@=G}}!kPu;zEqXC3i`K{KtxcPg1Ty| z)K-^EoO8?|jbhDA!QG3DR>DVh{~!j$2brAe>V3-h)C^rdYBKs!^Y0^`3vFL5@`Aj# z|Jh1VP^Hd^(}QjGs8sgn7+_YbiRKyLZ8#}rrz#XTzz&6Fq|Oz&HU9q8vgqvO$Cla2 zJ>(I)!1plzZS35~JKiq^6BR&SQuZC5Xji-x>K ztsZ9<4MJ;H8g35_4kw!+upPU|ZoW{1P@ewDv|M-L^0j@LBPe-d+RUR>u*boC?oNG< z6Rxvb^LW~VqFZ;dVy|1VpUU={nIfLkO|F&bpx?L`l<_akWzN*S{+Bn*H;ta1KO={D zMd6vBE=7oDZI1_|g0{J*d;D5%XNOw9LJAmv)ya0FT$vNQiJ6Dteh)@hhW&QFFz0Bw z{N6tIf~WLq)WK;V9lq%dPicW&{#g&1Te|NbJL+Z(wZv|gWE7%xpKn&b*uC%2t~phr8@J~P z1ANnX>12Ol`(5*;Vmm@>lI7*>uk}Rh5!aT^H;VTX>SLEHZK-x+aZ(}pEWxh5u65N8 zNAtu#*ncE#w9tx(_ScYM0#_-0HAF&pG>$ET=*7f3ev}xaf^ORwB>PJak!m=@Iyz|j z9!PNGez(PCda5`r>`5X0U%|44@-vx6G~$EA3f~{y3T>=6gJA7PUlwywI!H~Ja_W4Fu*SkE(SeT1G;oYnn_!V?3O?ma9bbG^375n)%WYnGNx^KbV? zRYX-Zz{6jq;0y?>cgjr$<@~hb1{sV|9}-qSAaGXdVt)HT_!6JgX4e(7^{3;%W2?tU zRviSF$g@TIR8138wgA1eW}eup-mwJb7tkHrOiSZ0h3Ki$GygylTNBeY#S27B2h8(F zQO{JWJ1Y+`(%Wo?oi7+TqsS(mJRMBF2s~m-r0c`>IMH7s z>FaIuWKm|Ac&teQ;HBRNIA@9_&Y8X6I|exjF&U7QjK|*6*@g|SeKlDn9&v%!J?4%) zY`qfllg(E6(ONdjVX%wW zR`Lq*r9Qm{s)k4^IsB&CnL>HRNb3m(%?y+e>O$!{b=%DZ2vmYNRbnEfGSw3e3_`!L zLuQj5BD2PzeM@G>GSSBBQU8=J83Jrx>9tL{nJ}N;lfRxk>9QJsIbKV8Tsh>M+a!OP z9Y1xFt7EWfKf0Y$8(zABpF$*)3))$!vz9sRTPG<{Td69qw2^UET1q`6qDHBs_|n3N zV^Yv}6%p0!B9Riyonfy(f(E{&my`FrS(RHbjZ>H5Mr>v=7^+G`L0F|wb-+;A(SmNl zse_~)HNvR`%!bO{>MB$ht!;tJa9+izSVU*P*Ig_&%zokKG7}!!D68XD8lu$;nSGw+ zI_(N;%Pd-V>>yT;gdaK2v>Q3YL@c4S+GUXh*1zof+7z%;)#bs%unr-O5Kw;)*U@}m z<#-0~^Dkd{2;pxg-x%MjE<02H&Ura=cH8{X zqZ9AG9XjaP`rH#&Q$IDdo<;B+d#1n#;1N-0__Fg2ViDN>hS;qwiy6Z9+NbG&a!jyY znl!)`*6Svoj=<+aOnXE}PB@)wJW4F{`zwC`?=b%B*0(HmI!6re>)}HACeyhfkChXM zk^Tz)s&YQdiNMrgyvA^vb*o!;toL-y8t~&Px;0Lp^)MS?hYBF0@8& zR%+0pAS~W8D$^&bq2hrwB;!j7&3_lMS=)Ju3!7DF3y$h@i;6^LqE~!jF4m8Xn^mW) ziV%tZh2PxY&aOAB!~Hayca^fYbksU)`Y&djExXHlb~y0#Zyvjg3; z9LcrkgI;Oky{ioR&YU?&n}L!6AynhcDT7SGH}e^a;e(gAYT~XzuIGb5b3$BCg@8KO ztU4y0WR3H8(e~);VO~5Kr%g%IIT_Q&KMzwY8sm-aetS1 zQe3ugiG3M{rWJRSzoYj@)wB^(!WT|)2<6=bJi2jPG$k^3NC_tW883w- zUi|QWtb|fCxcN@B{*wqngVm5NXC>uTfYda3N*7>zL3NraQSj(ASvaC{F!BdSH~1RQ z`-E?`O2B$GWFit%-LfOQwERiu-9OaKFRzIO>p@!<;$h-x-@>0Puu&u>f0NfUv9U{v ziC@9n%6Fxqw|TeVH~9+7Z_ghN4B8gLjYmgmjG|{%95;U6yGMhUif}=}r)yF|UCe#K z2p}-M&4PQ^9ldF`$E-{b5Ia;YcuO&X9U zq#Zi~VgzIE%~e;e@~8GxhHP0FzaJP=hR}wGB$*p<;uefycozzx#Xz&7s7g9ilWj1K z-XT%_!~_(i@DJ{(d<3=JUNgVuP+H4;=zg7e-945-9IfUyr$^8EG~MLtJq; zeF!yc^VPC_DbRhJ{vfS8&kJGk=qss+YdE=|4i_Q8p5BXEA8b`6^CgC_I*{d`2 zr!Ail&}I>3wyl}?CFiXEUiw1g*#>++Umk!Fvnn$)jv?EU0gSx_lwK{9UNo8-Zdj=^kZm)t5(f9Jd(mt z^$v)qmsCtf_)?Oe_%zf|4z$Zv{MP0(hqXNx+Zu4#=3LqyErfg9K00oXxh-Hn?PHfA z%DXPdF))Td*D zu;h<6UP2=rX=R|@26MjzOXg1- z@Ddt_jjztxFQ?ycF|c%{n16du^a{#FmaUf$87u^AL@JIDy52tkre6gw1ty5o_;gk^ z?!3scI^Sa4o?9a>-FWD@W%;qgD19d57sH z3+W%+BF3~EtQ1p7rDu_0H~8M-9?ZUS@eMJ+%6#IK78p}Esany9=~0Mm^IB)mxY4c7 zny&4Fa8U`3^UOZ5NNguLIA+)7jZfLRU>4JV%!skjq zpU}`FFX1)Gd{vn0ddYp5M48qLwhaxR>f`%uM#JUt-qIEo8oi0YwHHx_0Lo0hSQsn1 z38qI`Ops#|`MHzQdA)fl;;Ch;3xTT|mJbO=M6PfdLl@dn0k_KQ0~7JcbMx?Y@+E%g zX3bP6T<5uQakHA@m!L{x5M@1_HT|tT>ll4cOtCS?>H8&aqrvR7--XZf?FaAv^6IN(};}>4MXP=NtksMKQvQfT?z;LTncENKx=a3l(CbiC#-3Vhb?Gsz=I)D~F&!yaVS=IN-C&q>-hV!66BAYlyCr zL*TYmwfVrbz=YKrzE&~gKxUDpp>rX&jNxQ?dCHvD7&?+!J0JF}E>{%(lwpECz5za; z!Q48=7ouD4tK!a|aVf{cl(}l04IoqxNOh8_f2NxbEmK1Nj_^?`Rp7 zITzy&>lC35&EgU~sC%3+^SkjLoR3sBtXo8TO&kEcOpaTg zT!#l>`K|u^&XCo<6YRUyHEqsRSi8M!=%O-)+}Y<0Qc?hZP2h^M!(s6(do>6ur+aU= zP&12pvRabu-q#CNQ|>#*wt`S*w41boANse2eB;aK;+!&tCFyPp9?fM{ur3s~e&MLV zNI#7BJD%;qNvbQsbO;HkB$j<-hhR}Vf4vbV@$0^%Qn#Oen*J75^gQIiE`$I~BJ@nf zd(UV#V({a!9p3BqZ0xt$qhM?m$n2?w>!CnD4C=Fl76F6tjI|Oe+7zF|yCe*G(*;{= z@d)>lK}L9NSf)~{G5e*bY37#g z%{r%;4Tw4XQmU-T3_BRK^uZ67;VhlcGy@IphIkb!CFbPnSVOQs8!-hDg+{RMV1}3* zjJ!nRR5cn3t>}(He__rJ#R^?o#ui$oTS)U$Uj&;@+6f)5~psL zS|KSdc)>~y_=ty_x8-Dh`@*a4>SaC&_yoYTE(3-Z5GzyI6#&Msp9= zTqe7EV3Mj;(;j3X?{_W;fU}}Iyr$E29K&!?Scs(Y(AumvM_Y& z5HzDHcRl>6cP-r4U^zyBLLY}zKY}j0f3_cb@GBEM?cvE>m(_S$VtEclI!;uHKQ1|w z1;URJgje0bE-Gd1P4HE!PIygNj&5y^6|UH7^H$xx$?E{7Iiz@~2%e{9_jH9ak9ao8 zpF=3XjeTsW^%nj|piTtb_$>2~%RoPA%7WS+ycn<4x}%QoRGk{Ss3SWVLFG#;TbA2;7|du1TFKJHNsvbhwc5+f}@m|jDiE3 z97J6dBj~^k7)X!jW=-C5!d!)jUt+q<4)$Y$rXO3R_6E<$N?}6EH72yralk3OanfS# zHR+$|#wS~6AJRTzI(R8o)td3N6v|#Ud<>kGp|nQMMa_r3li1JJppNjL+6bDYIpt$9 zrJ_4MTfOOIm&}@0m0;+aG#a~sDevX}Ky!GurdiJXbOiIFChJSF`ykuOglYW&q5jt$ zdKmk)&~P?~g{YQ(M>+`feP(`75vJ01kIL$USWp2V=qF5q#B_u@nuDJDoJ2p$u4O-NC#_N^EAbpBz3t`oJI7Tgw+kof@mvF-H^B?Ku#hDRx4qqbV zT^Jj2SnnVR?tDd+(oyCc{`qLl?)m1MaIj|@=&tL{ioX`OhWxU6O}Ax8IJB=S9IUtv=C9Z6 z{K$A*`wobqWM&?}dr#4KHG7luO&X@$yFd;*^xI*Cu;k;IwwypWG$ToojnD-9N_eSH zpkSn(4MZD!p60-xyn#my?$T!@F+bp|3E$B>H5WacFuwSf+>m26N_wS`v~m7f~*j z`d5^?J%WB$w|MBdsH5k;Ze@gnY#@=A**Y4J?3!k5V$Gi{rGv9+_Q;aSd+-lR_KTWU z7?sTp7UrHn#EED@6$7qHv`4E}M(Xr^T-9)dVcLgvw@oUaX+#t;IPoe5%}!yC=5A7n zXyhC8m!Jl7&ksPx>`mu3b1N_+s*M3$=Z?l*O);CDN|xP&3<`8*`(f$ytL|`g-7jC1 zO6=(C2gf{DyhSJFC-l`rUTYhOPdP&)UDdX7hKt&Gv&@%4JIisO$ngwqwtk%LNzjq4ovvUQtYI?tyQ;>j z%Z8zobw%2}-p47L^VnPj&4f5RkxhRqS$~;1#$LaJ7#oqHu;8q|7bTW8SEkLSS}MAC z0rWe=Yqu`+olBa9SiD>cR>KF&rTV-gOafsegWobE- zW%5FEWjX5ricxJegpi=>;_u=?rGgI7<*G?-cp(JxtNBupLtQGxztEslY66_N#yF4x zf`=p|A}-U{?s}RTuIN*G##)s!M-@>W`H;P1f*(-Tw<)Ms}?|zQuGR zZ_Ns!Fp*oFCXs23O!W(<<^%S#zVmaouUkDAb=>=PGT3hWdtMdzBu6Q*Zc@hPk&0TP znZlsm6WU(Z%{kLGi(qQ#N{JG|!C#&{kmjY${1~=sXWH9mQBAWRvKLb~#(&573G#VG z!FPcNC!W}RxrhdZPAN~mx2toGdFd~Aq zwB0p{ zTjjQid=C!rd>49nkLChy$pKlbx8lE6S&;9s)CsJ*d{ZIKVbnuqrDOeI#;?-ym6t+F zOZ9GhzD_f}yt<3+9fgn_qlU?nuWCj;^${6GOV1S%BPdPxnrSyAdt;##8>PZThf_+w z>6?rAGC58uTZ-ta$;*!|gp577WC1EA;a(A;od-LHJE%w}LU(~9Uq?|YtFc@W=j|>M z^4>CCz1>0pK5T|bwNfhdY_nBGyTjK{!GGB?>3fDxXD+u{B#pISqfElx3&RDGO#Rf1 z(C}@UP22kv8qF=5I(WmrY-Zn!q}K=qH%{g5qq&g@>0^D*Uj(XaI`DFvi!lBo5(g-K zKH)nWEc{~v@!IX?+*>ISov-N=+e1r%F2++qLkp+<+o;tY$STDGj!fZlGpt?%Ut|0+ zqE8aBlV%aR6JYa-PFTAe0p89l8jQTY0#x<}W8@!Nc~RQTQUdV#XaR-2d*W?3P_vq% z+;8O3@6o^Vo)g{b;6sjW_%v&>CGjgu-ZcVLWh|xZ`L|JGz9x^IiBR}Dd!*iK@d0&5 z3wroWGef`^9^1O=b`P2>dJY!-Q9QNoq8{6KN7iqis@DYyMLv(Vi( zP8rXrejX=N1s$_81q8F3S%h4Q5cL_6(Z-Y+K%d}^5cknMHp8c}W-&t1EopEH?6uPG zGcQwtvEyjHVv8?VMa)rMkzkFXbhOoeN#%^R#{_M8$2$bCLPx==x43+4~Go!s5 zu}59ow8lK=pOY*))SYE@yN0}FS1ofmJ_@TRTkCm^a|1s!-lCV-!=U~na7UExL}7JC zC~!+6Z{baW@3_tr#;f4V?!K3RoA(a~+S00yFdb{Ik5>O}=YF@hQg2ILG}2{8%eIaH zj7EKqz>h8$IixS8QVMIUYKi|i&i{dI^H0n8e?2IFemzkZZH(WjG_HiAr=$a1GE!tB z?cZ|`1Kdsg81R=V{riD!A&EuigONM&vF|2wL{{^?r~duA@2m2lh27cVY|oenu;f>w zmk8b*+EqiXf-e(c!I}S-_d^qVL>&csrc$23ow6gUH@{>{M$_X z)oX89pU(3+I9-320|NdRLx0=$r}oS{*eJ!K?f${$fBpJXv`$_0K;rPf8%g|!JZRM| z>Wy#w%e@QT8Xfg$#i!K4_pcM-@7ek%JlEeST@MfSs9#Xpj^6d(-v4*wfAiq;i;M=E zm2tEE|N7^j1b;oW6l5|GadO@z@IUuc#*mCgZDru5Y@z&5KK}J>?Up=ed*8#ew3vSn z|9@-dA0rc{_7_e6J;G4&-%f)6J?nB-?@XU-DF~Iv|8wX2&&X&ptu4w@{u4Vr@}Pq< znd98*srde%JD<7zFB;>~-2dU1`JZfmuirDjJ6rA{=_k|w+;E5}-~AWO|DPfxmZERODn6`x Sl>CG2V|)Jc?}g`X#rz*3?3xb% literal 0 HcmV?d00001 diff --git a/assets/signin_success.png b/assets/signin_success.png new file mode 100644 index 0000000000000000000000000000000000000000..68bdefebe2be9be69b4d314c33d8cb9586066af5 GIT binary patch literal 70533 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 literal 0 HcmV?d00001 From ed0f5b2964c4a2f81310a77255d6ac6757d3dd4e Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Wed, 3 Sep 2025 00:24:54 +0530 Subject: [PATCH 17/84] Adding Remote MCP setup instructions with screenshots --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 8d65f89c..dc1220ad 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,10 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and } } ``` + - To setup Remote BrowserStack MCP instead of local BrowserStack MCP you can add the following JSON content instead : +

- In VSCode, make sure to click on `Start` button in the MCP Server to start the server. ![Start MCP Server](assets/vscode_install.png) @@ -456,7 +460,57 @@ As of now we support 20 tools. ```text Upload PRD from /Users/xyz/Desktop/login-flow.pdf and use BrowserStack AI to generate test cases ``` +## Remote MCP Setup + - VSCode (Copilot - Agent Mode): `.vscode/mcp.json`: + + - Locate or Create the Configuration File: + - In the root directory of your project, look for a folder named .vscode. This folder is usually hidden so you will need to find it as mentioned in the expand. + - If this folder doesn't exist, create it. + - Inside the .vscode folder, create a new file named mcp.json + - To setup Remote BrowserStack MCP instead of local BrowserStack MCP you can add the following JSON content : +
+ Remote MCP JSON file +
+ +### Alternative way to Setup Remote MCP + - Step 1.Click on the gear icon to Select Tools +
+ Select Tools +
+ + - Step 2. A tool menu would appear at the top-centre, scroll down on the menu at the top and then Click on Add MCP Server +
+ Add MCP Server +
+ + - Step 3. Click on HTTP option +
+ HTTP Option +
+ + - Step 4. Paste Remote MCP Server URL +
+ Remote MCP Server URL +
+ + - Step 5. Give server id as browserstack +
+ Remote MCP Server ID +
+ + - Step 6. In VSCode Click on start MCP Server and then click on "Allow" +
+ authentication1 +
+
+ authentication2 +
+
+ Sign_in_success +
+ + ## 🤝 Recommended MCP Clients From 0de1ffe936e692cc8ed99fe6f42bbd2577c0b021 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Wed, 3 Sep 2025 00:28:30 +0530 Subject: [PATCH 18/84] RemoteMCP Instructions --- README.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dc1220ad..31fab0c0 100644 --- a/README.md +++ b/README.md @@ -189,9 +189,9 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and } } ``` - - To setup Remote BrowserStack MCP instead of local BrowserStack MCP you can add the following JSON content instead : + - To setup Remote BrowserStack MCP you can add the following JSON content instead of the above :
- Remote MCP JSON file + Remote MCP JSON file
- In VSCode, make sure to click on `Start` button in the MCP Server to start the server. @@ -474,40 +474,47 @@ As of now we support 20 tools. ### Alternative way to Setup Remote MCP + - Step 1.Click on the gear icon to Select Tools +
- Select Tools + Select Tools
- Step 2. A tool menu would appear at the top-centre, scroll down on the menu at the top and then Click on Add MCP Server +
- Add MCP Server + Add MCP Server
- Step 3. Click on HTTP option
- HTTP Option + HTTP Option
- Step 4. Paste Remote MCP Server URL
- Remote MCP Server URL + Remote MCP Server URL
- Step 5. Give server id as browserstack +
- Remote MCP Server ID + Remote MCP Server ID
- Step 6. In VSCode Click on start MCP Server and then click on "Allow" +
- authentication1 + authentication1
+
- authentication2 + authentication2
+
- Sign_in_success + Sign_in_success
From 4377f1ce8292221983d64ef93fed87618b142b55 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Wed, 3 Sep 2025 00:32:01 +0530 Subject: [PATCH 19/84] RemoteMCP Instructions --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 31fab0c0..9ff2909d 100644 --- a/README.md +++ b/README.md @@ -189,10 +189,6 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and } } ``` - - To setup Remote BrowserStack MCP you can add the following JSON content instead of the above : -
- Remote MCP JSON file -
- In VSCode, make sure to click on `Start` button in the MCP Server to start the server. ![Start MCP Server](assets/vscode_install.png) @@ -470,10 +466,10 @@ As of now we support 20 tools. - Inside the .vscode folder, create a new file named mcp.json - To setup Remote BrowserStack MCP instead of local BrowserStack MCP you can add the following JSON content :
- Remote MCP JSON file + Remote MCP JSON file
- -### Alternative way to Setup Remote MCP + + ### Alternative way to Setup Remote MCP - Step 1.Click on the gear icon to Select Tools From 35c8a32f2ba512e68a6a02ed0295a68cb09b0a32 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Wed, 3 Sep 2025 00:34:23 +0530 Subject: [PATCH 20/84] RemoteMCP Installation Instructions --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9ff2909d..5a5eb505 100644 --- a/README.md +++ b/README.md @@ -466,7 +466,7 @@ As of now we support 20 tools. - Inside the .vscode folder, create a new file named mcp.json - To setup Remote BrowserStack MCP instead of local BrowserStack MCP you can add the following JSON content :
- Remote MCP JSON file + Remote MCP JSON file
### Alternative way to Setup Remote MCP @@ -474,43 +474,43 @@ As of now we support 20 tools. - Step 1.Click on the gear icon to Select Tools
- Select Tools + Select Tools
- Step 2. A tool menu would appear at the top-centre, scroll down on the menu at the top and then Click on Add MCP Server
- Add MCP Server + Add MCP Server
- Step 3. Click on HTTP option
- HTTP Option + HTTP Option
- - Step 4. Paste Remote MCP Server URL + - Step 4. Paste Remote MCP Server URL : https://mcp.browserstack.com/mcp
- Remote MCP Server URL + Remote MCP Server URL
- - Step 5. Give server id as browserstack + - Step 5. Give server id as : browserstack
- Remote MCP Server ID + Remote MCP Server ID
- Step 6. In VSCode Click on start MCP Server and then click on "Allow"
- authentication1 + authentication1
- authentication2 + authentication2
- Sign_in_success + Sign_in_success
From 8c3a224d90d9493445a30ed57bfd65344c0a1666 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 5 Sep 2025 17:17:59 +0530 Subject: [PATCH 21/84] feat: Add RCA tools for fetching and formatting Root Cause Analysis data --- src/server-factory.ts | 2 + src/tools/rca-agent-utils/format-rca.ts | 35 +++ src/tools/rca-agent-utils/get-build-id.ts | 32 ++ .../rca-agent-utils/get-failed-test-id.ts | 104 +++++++ src/tools/rca-agent-utils/rca-data.ts | 281 ++++++++++++++++++ src/tools/rca-agent.ts | 88 ++++++ 6 files changed, 542 insertions(+) create mode 100644 src/tools/rca-agent-utils/format-rca.ts create mode 100644 src/tools/rca-agent-utils/get-build-id.ts create mode 100644 src/tools/rca-agent-utils/get-failed-test-id.ts create mode 100644 src/tools/rca-agent-utils/rca-data.ts create mode 100644 src/tools/rca-agent.ts diff --git a/src/server-factory.ts b/src/server-factory.ts index 82f93730..8381f24b 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -17,6 +17,7 @@ import addSelfHealTools from "./tools/selfheal.js"; import addAppLiveTools from "./tools/applive.js"; import { setupOnInitialized } from "./oninitialized.js"; import { BrowserStackConfig } from "./lib/types.js"; +import addRCATools from "./tools/rca-agent.js"; /** * Wrapper class for BrowserStack MCP Server @@ -55,6 +56,7 @@ export class BrowserStackMcpServer { addFailureLogsTools, addAutomateTools, addSelfHealTools, + addRCATools, ]; toolAdders.forEach((adder) => { diff --git a/src/tools/rca-agent-utils/format-rca.ts b/src/tools/rca-agent-utils/format-rca.ts new file mode 100644 index 00000000..d7961040 --- /dev/null +++ b/src/tools/rca-agent-utils/format-rca.ts @@ -0,0 +1,35 @@ +// Utility function to format RCA data for better readability +export function formatRCAData(rcaData: any): string { + if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) { + return "No RCA data available."; + } + + let output = "## Root Cause Analysis Report\n\n"; + + rcaData.testCases.forEach((testCase: any, index: number) => { + // Show test case name first with smaller heading + output += `### ${testCase.displayName || `Test Case ${index + 1}`}\n`; + output += `**Test ID:** ${testCase.id}\n`; + output += `**Status:** ${testCase.state}\n\n`; + + if (testCase.rcaData?.originalResponse?.rcaData) { + const rca = testCase.rcaData.originalResponse.rcaData; + + if (rca.root_cause) { + output += `**Root Cause:** ${rca.root_cause}\n\n`; + } + + if (rca.description) { + output += `**Description:**\n${rca.description}\n\n`; + } + + if (rca.possible_fix) { + output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`; + } + } + + output += "---\n\n"; + }); + + return output; +} diff --git a/src/tools/rca-agent-utils/get-build-id.ts b/src/tools/rca-agent-utils/get-build-id.ts new file mode 100644 index 00000000..6f3dffe7 --- /dev/null +++ b/src/tools/rca-agent-utils/get-build-id.ts @@ -0,0 +1,32 @@ +export async function getBuildId( + projectName: string, + buildName: string, + username: string, + accessKey: string, +): Promise { + const url = new URL( + "https://api-automation.browserstack.com/ext/v1/builds/latest", + ); + url.searchParams.append("project_name", projectName); + url.searchParams.append("build_name", buildName); + url.searchParams.append("user_name", username); + + const authHeader = + "Basic " + Buffer.from(`${username}:${accessKey}`).toString("base64"); + + const response = await fetch(url.toString(), { + headers: { + Authorization: authHeader, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch build ID: ${response.status} ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.build_id; +} diff --git a/src/tools/rca-agent-utils/get-failed-test-id.ts b/src/tools/rca-agent-utils/get-failed-test-id.ts new file mode 100644 index 00000000..46b8ddd1 --- /dev/null +++ b/src/tools/rca-agent-utils/get-failed-test-id.ts @@ -0,0 +1,104 @@ +interface TestDetails { + status: string; + details: any; + children?: TestDetails[]; + display_name?: string; +} + +interface TestRun { + hierarchy: TestDetails[]; + pagination?: { + has_next: boolean; + next_page: string | null; + }; +} + +export interface FailedTestInfo { + id: string; + displayName: string; +} + +export async function getFailedTestIds( + buildId: string, + authString: string, +): Promise { + const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns?test_statuses=failed`; + let nextUrl = baseUrl; + let allFailedTests: FailedTestInfo[] = []; + let requestNumber = 0; + + // Construct Basic auth header + const encodedCredentials = Buffer.from(authString).toString("base64"); + const authHeader = `Basic ${encodedCredentials}`; + + try { + while (true) { + requestNumber++; + + const response = await fetch(nextUrl, { + headers: { + Authorization: authHeader, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch test runs: ${response.status} ${response.statusText}`, + ); + } + + const data = (await response.json()) as TestRun; + + // Extract failed IDs from current page + if (data.hierarchy && data.hierarchy.length > 0) { + const currentFailedTests = extractFailedTestIds(data.hierarchy); + allFailedTests = allFailedTests.concat(currentFailedTests); + } + + // Check for pagination termination conditions + if (!data.pagination?.has_next || !data.pagination.next_page) { + break; + } + + // Safety limit to prevent runaway requests + if (requestNumber >= 5) { + break; + } + + // Prepare next request + nextUrl = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`; + } + + // Return unique failed test IDs + return allFailedTests; + } catch (error) { + console.error("Error fetching failed tests:", error); + throw error; + } +} + +// Recursive function to extract failed test IDs from hierarchy +function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { + let failedTests: FailedTestInfo[] = []; + + for (const node of hierarchy) { + if (node.details?.status === "failed" && node.details?.run_count) { + if (node.details?.observability_url) { + const idMatch = node.details.observability_url.match(/details=(\d+)/); + if (idMatch) { + failedTests.push({ + id: idMatch[1], + displayName: node.display_name || `Test ${idMatch[1]}` + }); + } + } + } + + if (node.children && node.children.length > 0) { + failedTests = failedTests.concat(extractFailedTestIds(node.children)); + } + } + + return failedTests; +} diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts new file mode 100644 index 00000000..95f34afe --- /dev/null +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -0,0 +1,281 @@ +import { FailedTestInfo } from "./get-failed-test-id.js"; + +export enum RCAState { + PENDING = "pending", + COMPLETED = "completed", + FAILED = "failed", +} + +export interface RCATestCase { + id: string; + testRunId: string; + displayName?: string; + state: RCAState; + rcaData?: any; +} + +export interface RCAResponse { + testCases: RCATestCase[]; +} + +interface ScanProgressContext { + sendNotification: (notification: any) => Promise; + _meta?: { + progressToken?: string | number; + }; +} + +// --- Utility functions --- + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +function calculateProgress( + resolvedCount: number, + totalCount: number, + baseProgress: number = 10, +): number { + const progressRange = 90 - baseProgress; + const completionProgress = (resolvedCount / totalCount) * progressRange; + return Math.min(100, baseProgress + completionProgress); +} + +async function notifyProgress( + context: ScanProgressContext | undefined, + message: string, + progress: number, +) { + if (!context?.sendNotification) return; + + await context.sendNotification({ + method: "notifications/progress", + params: { + progressToken: context._meta?.progressToken?.toString(), + message, + progress, + total: 100, + }, + }); +} + +// Helper to send progress based on current test cases +async function updateProgress( + context: ScanProgressContext | undefined, + testCases: RCATestCase[], + message?: string, +) { + const pending = testCases.filter((tc) => tc.state === RCAState.PENDING); + const resolvedCount = testCases.length - pending.length; + await notifyProgress( + context, + message ?? + (pending.length === 0 + ? "RCA analysis completed for all test cases" + : `RCA analysis in progress (${resolvedCount}/${testCases.length} resolved)`), + pending.length === 0 + ? 100 + : calculateProgress(resolvedCount, testCases.length), + ); +} + +// --- Fetch initial RCA for a test case --- +async function fetchInitialRCA( + testInfo: FailedTestInfo, + headers: Record, + baseUrl: string, +): Promise { + const url = baseUrl.replace("{testId}", testInfo.id); + + try { + const response = await fetch(url, { headers }); + if (!response.ok) { + return { + id: testInfo.id, + testRunId: testInfo.id, + displayName: testInfo.displayName, + state: RCAState.FAILED, + rcaData: { + error: `HTTP ${response.status}: Failed to start RCA analysis`, + }, + }; + } + + const data = await response.json(); + if (data.state && !["pending", "completed"].includes(data.state)) { + return { + id: data.id ?? testInfo.id, + testRunId: data.testRunId ?? testInfo.id, + displayName: testInfo.displayName, + state: RCAState.FAILED, + rcaData: { + error: `API returned error state: ${data.state}`, + originalResponse: data, + }, + }; + } + + return { + id: data.id ?? testInfo.id, + testRunId: data.testRunId ?? testInfo.id, + displayName: testInfo.displayName, + state: RCAState.PENDING, + }; + } catch (error) { + return { + id: testInfo.id, + testRunId: testInfo.id, + displayName: testInfo.displayName, + state: RCAState.FAILED, + rcaData: { + error: + error instanceof Error ? error.message : "Network or parsing error", + }, + }; + } +} + +// --- Poll all test cases until completion or timeout --- +async function pollRCAResults( + testCases: RCATestCase[], + headers: Record, + baseUrl: string, + context: ScanProgressContext | undefined, + pollInterval: number, + timeout: number, + initialDelay: number, +): Promise { + const startTime = Date.now(); + + await delay(initialDelay); + + try { + while (true) { + const pendingCases = testCases.filter( + (tc) => tc.state === RCAState.PENDING, + ); + await updateProgress(context, testCases); + + if (pendingCases.length === 0) break; + + if (Date.now() - startTime >= timeout) { + pendingCases.forEach((tc) => { + tc.state = RCAState.FAILED; + tc.rcaData = { error: `Timeout after ${timeout}ms` }; + }); + await updateProgress(context, testCases, "RCA analysis timed out"); + break; + } + + // Poll all pending cases in parallel + await Promise.allSettled( + pendingCases.map(async (tc) => { + try { + const response = await fetch(baseUrl.replace("{testId}", tc.id), { + headers, + }); + if (!response.ok) { + tc.state = RCAState.FAILED; + tc.rcaData = { error: `HTTP ${response.status}: Polling failed` }; + return; + } + const data = await response.json(); + if (tc.state === RCAState.PENDING) { + if (data.state === "completed") { + tc.state = RCAState.COMPLETED; + tc.rcaData = data; + } else if (data.state && data.state !== "pending") { + tc.state = RCAState.FAILED; + tc.rcaData = { + error: `API returned error state: ${data.state}`, + originalResponse: data, + }; + } + } + } catch (err) { + if (tc.state === RCAState.PENDING) { + tc.state = RCAState.FAILED; + tc.rcaData = { + error: + err instanceof Error + ? err.message + : "Network or parsing error", + }; + } + } + }), + ); + + await delay(pollInterval); + } + } catch (err) { + // Fallback in case of unexpected error + testCases + .filter((tc) => tc.state === RCAState.PENDING) + .forEach((tc) => { + tc.state = RCAState.FAILED; + tc.rcaData = { + error: err instanceof Error ? err.message : "Unexpected error", + }; + }); + await updateProgress( + context, + testCases, + "RCA analysis failed due to unexpected error", + ); + } + + return { testCases }; +} + +// --- Public API function --- +export async function getRCAData( + testInfos: FailedTestInfo[], + authString: string, + context?: ScanProgressContext, +): Promise { + const pollInterval = 5000; + const timeout = 30000; + const initialDelay = 20000; + + const baseUrl = + "https://api-observability.browserstack.com/ext/v1/testRun/{testId}/testRca"; + const headers = { + Authorization: `Basic ${Buffer.from(authString).toString("base64")}`, + "Content-Type": "application/json", + }; + + await notifyProgress(context, "Starting RCA analysis for test cases...", 0); + + // Step 1: Fire initial RCA requests in parallel + const testCases = await Promise.all( + testInfos.map((testInfo) => fetchInitialRCA(testInfo, headers, baseUrl)), + ); + + const pendingCount = testCases.filter( + (tc) => tc.state === RCAState.PENDING, + ).length; + await notifyProgress( + context, + `Initial RCA requests completed. ${pendingCount} cases pending analysis...`, + 10, + ); + + if (pendingCount === 0) { + await notifyProgress( + context, + "RCA analysis completed for all test cases", + 100, + ); + return { testCases }; + } + + // Step 2: Poll pending test cases + return pollRCAResults( + testCases, + headers, + baseUrl, + context, + pollInterval, + timeout, + initialDelay, + ); +} diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts new file mode 100644 index 00000000..3ed80c75 --- /dev/null +++ b/src/tools/rca-agent.ts @@ -0,0 +1,88 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import logger from "../logger.js"; +import { BrowserStackConfig } from "../lib/types.js"; +import { getBrowserStackAuth } from "../lib/get-auth.js"; +import { getBuildId } from "./rca-agent-utils/get-build-id.js"; +import { getFailedTestIds } from "./rca-agent-utils/get-failed-test-id.js"; +import { getRCAData } from "./rca-agent-utils/rca-data.js"; +import { formatRCAData } from "./rca-agent-utils/format-rca.js"; + +// wTool function that fetches RCA data +export async function fetchRCADataTool( + args: { projectName: string; buildName: string; buildId?: string }, + config: BrowserStackConfig, +): Promise { + try { + let { projectName, buildName, buildId } = args; + const authString = getBrowserStackAuth(config); + const [username, accessKey] = authString.split(":"); + buildId = buildId || await getBuildId(projectName, buildName, username, accessKey); + const testInfos = await getFailedTestIds(buildId, authString); + const rcaData = await getRCAData(testInfos.slice(0, 3), authString); + + const formattedData = formatRCAData(rcaData); + + return { + content: [ + { + type: "text", + text: formattedData, + }, + ], + }; + } catch (error) { + logger.error("Error fetching RCA data", error); + throw error; + } +} + +// Registers the fetchRCA tool with the MCP server +export default function addRCATools( + server: McpServer, + config: BrowserStackConfig, +) { + const tools: Record = {}; + + tools.fetchRCA = server.tool( + "fetchRCA", + "Retrieves AI-RCA (Root Cause Analysis) data for a BrowserStack Automate session and provides insights into test failures.", + { + projectName: z + .string() + .describe( + "The project name of the test run can be available in browsrestack yml or ask it from user", + ), + buildName: z + .string() + .describe( + "The build name of the test run can be available in browsrestack yml or ask it from user", + ), + buildId: z + .string() + .optional() + .describe( + "The build ID of the test run.", + ), + }, + async (args) => { + try { + return await fetchRCADataTool(args, config); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error during fetching RCA data: ${errorMessage}`, + }, + ], + }; + } + }, + ); + + return tools; +} From 8d21d7c6c69ed65c76dca24af049f0535e9c5c4b Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 8 Sep 2025 14:15:00 +0530 Subject: [PATCH 22/84] Refactor RCA tools and add new types for improved functionality and readability --- src/tools/rca-agent-utils/format-rca.ts | 34 +++- .../rca-agent-utils/get-failed-test-id.ts | 112 +++++----- src/tools/rca-agent-utils/rca-data.ts | 191 ++++++++++-------- src/tools/rca-agent-utils/types.ts | 49 +++++ src/tools/rca-agent.ts | 111 ++++++++-- 5 files changed, 317 insertions(+), 180 deletions(-) create mode 100644 src/tools/rca-agent-utils/types.ts diff --git a/src/tools/rca-agent-utils/format-rca.ts b/src/tools/rca-agent-utils/format-rca.ts index d7961040..154f4570 100644 --- a/src/tools/rca-agent-utils/format-rca.ts +++ b/src/tools/rca-agent-utils/format-rca.ts @@ -1,33 +1,47 @@ +import logger from "../../logger.js"; + // Utility function to format RCA data for better readability export function formatRCAData(rcaData: any): string { + logger.info( + `Formatting RCA data for output: ${JSON.stringify(rcaData, null, 2)}`, + ); if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) { return "No RCA data available."; } let output = "## Root Cause Analysis Report\n\n"; - + rcaData.testCases.forEach((testCase: any, index: number) => { - // Show test case name first with smaller heading - output += `### ${testCase.displayName || `Test Case ${index + 1}`}\n`; + // Show test case ID with smaller heading + output += `### Test Case ${index + 1}\n`; output += `**Test ID:** ${testCase.id}\n`; output += `**Status:** ${testCase.state}\n\n`; - if (testCase.rcaData?.originalResponse?.rcaData) { - const rca = testCase.rcaData.originalResponse.rcaData; - + // Access RCA data from the correct path + const rca = testCase.rcaData?.rcaData; + + if (rca) { if (rca.root_cause) { output += `**Root Cause:** ${rca.root_cause}\n\n`; } - + + if (rca.failure_type) { + output += `**Failure Type:** ${rca.failure_type}\n\n`; + } + if (rca.description) { - output += `**Description:**\n${rca.description}\n\n`; + output += `**Detailed Analysis:**\n${rca.description}\n\n`; } - + if (rca.possible_fix) { output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`; } + } else if (testCase.rcaData?.error) { + output += `**Error:** ${testCase.rcaData.error}\n\n`; + } else if (testCase.state === "failed") { + output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`; } - + output += "---\n\n"; }); diff --git a/src/tools/rca-agent-utils/get-failed-test-id.ts b/src/tools/rca-agent-utils/get-failed-test-id.ts index 46b8ddd1..2004d482 100644 --- a/src/tools/rca-agent-utils/get-failed-test-id.ts +++ b/src/tools/rca-agent-utils/get-failed-test-id.ts @@ -1,76 +1,57 @@ -interface TestDetails { - status: string; - details: any; - children?: TestDetails[]; - display_name?: string; -} +import { TestStatus, FailedTestInfo, TestRun, TestDetails } from "./types.js"; -interface TestRun { - hierarchy: TestDetails[]; - pagination?: { - has_next: boolean; - next_page: string | null; - }; -} +let hasNext = false; +let nextPageUrl: string | null = null; -export interface FailedTestInfo { - id: string; - displayName: string; -} - -export async function getFailedTestIds( +export async function getTestIds( buildId: string, authString: string, + status?: TestStatus, ): Promise { - const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns?test_statuses=failed`; - let nextUrl = baseUrl; + const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`; + + // Build initial URL + const initialUrl = new URL(baseUrl); + if (status) initialUrl.searchParams.set("test_statuses", status); + + // Use stored nextPageUrl if available, otherwise fresh URL + const requestUrl = + hasNext && nextPageUrl ? nextPageUrl : initialUrl.toString(); let allFailedTests: FailedTestInfo[] = []; - let requestNumber = 0; // Construct Basic auth header const encodedCredentials = Buffer.from(authString).toString("base64"); const authHeader = `Basic ${encodedCredentials}`; try { - while (true) { - requestNumber++; - - const response = await fetch(nextUrl, { - headers: { - Authorization: authHeader, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error( - `Failed to fetch test runs: ${response.status} ${response.statusText}`, - ); - } - - const data = (await response.json()) as TestRun; - - // Extract failed IDs from current page - if (data.hierarchy && data.hierarchy.length > 0) { - const currentFailedTests = extractFailedTestIds(data.hierarchy); - allFailedTests = allFailedTests.concat(currentFailedTests); - } - - // Check for pagination termination conditions - if (!data.pagination?.has_next || !data.pagination.next_page) { - break; - } + const response = await fetch(requestUrl, { + headers: { + Authorization: authHeader, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch test runs: ${response.status} ${response.statusText}`, + ); + } - // Safety limit to prevent runaway requests - if (requestNumber >= 5) { - break; - } + const data = (await response.json()) as TestRun; - // Prepare next request - nextUrl = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`; + // Extract failed IDs from current page + if (data.hierarchy && data.hierarchy.length > 0) { + allFailedTests = extractFailedTestIds(data.hierarchy); } - // Return unique failed test IDs + // Update pagination state in memory + hasNext = data.pagination?.has_next || false; + nextPageUrl = + hasNext && data.pagination?.next_page + ? buildNextPageUrl(baseUrl, status, data.pagination.next_page) + : null; + + // Return failed test IDs from current page only return allFailedTests; } catch (error) { console.error("Error fetching failed tests:", error); @@ -78,6 +59,18 @@ export async function getFailedTestIds( } } +// Helper to build next page URL safely +function buildNextPageUrl( + baseUrl: string, + status: TestStatus | undefined, + nextPage: string, +): string { + const url = new URL(baseUrl); + if (status) url.searchParams.set("test_statuses", status); + url.searchParams.set("next_page", nextPage); + return url.toString(); +} + // Recursive function to extract failed test IDs from hierarchy function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { let failedTests: FailedTestInfo[] = []; @@ -87,10 +80,7 @@ function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { if (node.details?.observability_url) { const idMatch = node.details.observability_url.match(/details=(\d+)/); if (idMatch) { - failedTests.push({ - id: idMatch[1], - displayName: node.display_name || `Test ${idMatch[1]}` - }); + failedTests.push({ id: idMatch[1] }); } } } diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts index 95f34afe..65f9489b 100644 --- a/src/tools/rca-agent-utils/rca-data.ts +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -1,22 +1,5 @@ -import { FailedTestInfo } from "./get-failed-test-id.js"; - -export enum RCAState { - PENDING = "pending", - COMPLETED = "completed", - FAILED = "failed", -} - -export interface RCATestCase { - id: string; - testRunId: string; - displayName?: string; - state: RCAState; - rcaData?: any; -} - -export interface RCAResponse { - testCases: RCATestCase[]; -} +import { FailedTestInfo } from "./types.js"; +import { RCAState, RCATestCase, RCAResponse } from "./types.js"; interface ScanProgressContext { sendNotification: (notification: any) => Promise; @@ -25,10 +8,27 @@ interface ScanProgressContext { }; } -// --- Utility functions --- - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +function isInProgressState(state: RCAState): boolean { + return [ + RCAState.PENDING, + RCAState.FETCHING_LOGS, + RCAState.GENERATING_RCA, + RCAState.GENERATED_RCA, + ].includes(state); +} + +function isFailedState(state: RCAState): boolean { + return [ + RCAState.FAILED, + RCAState.LLM_SERVICE_ERROR, + RCAState.LOG_FETCH_ERROR, + RCAState.UNKNOWN_ERROR, + RCAState.TIMEOUT, + ].includes(state); +} + function calculateProgress( resolvedCount: number, totalCount: number, @@ -57,27 +57,24 @@ async function notifyProgress( }); } -// Helper to send progress based on current test cases async function updateProgress( context: ScanProgressContext | undefined, testCases: RCATestCase[], message?: string, ) { - const pending = testCases.filter((tc) => tc.state === RCAState.PENDING); - const resolvedCount = testCases.length - pending.length; + const inProgressCases = testCases.filter((tc) => isInProgressState(tc.state)); + const resolvedCount = testCases.length - inProgressCases.length; + await notifyProgress( context, message ?? - (pending.length === 0 - ? "RCA analysis completed for all test cases" - : `RCA analysis in progress (${resolvedCount}/${testCases.length} resolved)`), - pending.length === 0 + `RCA analysis in progress (${resolvedCount}/${testCases.length} resolved)`, + inProgressCases.length === 0 ? 100 : calculateProgress(resolvedCount, testCases.length), ); } -// --- Fetch initial RCA for a test case --- async function fetchInitialRCA( testInfo: FailedTestInfo, headers: Record, @@ -87,12 +84,12 @@ async function fetchInitialRCA( try { const response = await fetch(url, { headers }); + if (!response.ok) { return { id: testInfo.id, testRunId: testInfo.id, - displayName: testInfo.displayName, - state: RCAState.FAILED, + state: RCAState.LOG_FETCH_ERROR, rcaData: { error: `HTTP ${response.status}: Failed to start RCA analysis`, }, @@ -100,31 +97,41 @@ async function fetchInitialRCA( } const data = await response.json(); - if (data.state && !["pending", "completed"].includes(data.state)) { - return { - id: data.id ?? testInfo.id, - testRunId: data.testRunId ?? testInfo.id, - displayName: testInfo.displayName, - state: RCAState.FAILED, - rcaData: { - error: `API returned error state: ${data.state}`, - originalResponse: data, - }, - }; - } + + const apiState = data.state?.toLowerCase(); + let resultState: RCAState; + + if (apiState === "completed") resultState = RCAState.COMPLETED; + else if (apiState === "pending") resultState = RCAState.PENDING; + else if (apiState === "fetching_logs") resultState = RCAState.FETCHING_LOGS; + else if (apiState === "generating_rca") + resultState = RCAState.GENERATING_RCA; + else if (apiState === "generated_rca") resultState = RCAState.GENERATED_RCA; + else if (apiState === "processing" || apiState === "running") + resultState = RCAState.GENERATING_RCA; + else if (apiState === "failed" || apiState === "error") + resultState = RCAState.UNKNOWN_ERROR; + else if (apiState) resultState = RCAState.UNKNOWN_ERROR; + else resultState = RCAState.PENDING; return { - id: data.id ?? testInfo.id, - testRunId: data.testRunId ?? testInfo.id, - displayName: testInfo.displayName, - state: RCAState.PENDING, + id: testInfo.id, + testRunId: testInfo.id, + state: resultState, + ...(resultState === RCAState.COMPLETED && { rcaData: data }), + ...(isFailedState(resultState) && + data.state && { + rcaData: { + error: `API returned state: ${data.state}`, + originalResponse: data, + }, + }), }; } catch (error) { return { id: testInfo.id, testRunId: testInfo.id, - displayName: testInfo.displayName, - state: RCAState.FAILED, + state: RCAState.LLM_SERVICE_ERROR, rcaData: { error: error instanceof Error ? error.message : "Network or parsing error", @@ -133,7 +140,6 @@ async function fetchInitialRCA( } } -// --- Poll all test cases until completion or timeout --- async function pollRCAResults( testCases: RCATestCase[], headers: Record, @@ -144,55 +150,72 @@ async function pollRCAResults( initialDelay: number, ): Promise { const startTime = Date.now(); - await delay(initialDelay); try { while (true) { - const pendingCases = testCases.filter( - (tc) => tc.state === RCAState.PENDING, + const inProgressCases = testCases.filter((tc) => + isInProgressState(tc.state), ); await updateProgress(context, testCases); - if (pendingCases.length === 0) break; + if (inProgressCases.length === 0) break; if (Date.now() - startTime >= timeout) { - pendingCases.forEach((tc) => { - tc.state = RCAState.FAILED; + inProgressCases.forEach((tc) => { + tc.state = RCAState.TIMEOUT; tc.rcaData = { error: `Timeout after ${timeout}ms` }; }); await updateProgress(context, testCases, "RCA analysis timed out"); break; } - // Poll all pending cases in parallel await Promise.allSettled( - pendingCases.map(async (tc) => { + inProgressCases.map(async (tc) => { try { - const response = await fetch(baseUrl.replace("{testId}", tc.id), { - headers, - }); + const pollUrl = baseUrl.replace("{testId}", tc.id); + const response = await fetch(pollUrl, { headers }); if (!response.ok) { - tc.state = RCAState.FAILED; - tc.rcaData = { error: `HTTP ${response.status}: Polling failed` }; + const errorText = await response.text(); + tc.state = RCAState.LOG_FETCH_ERROR; + tc.rcaData = { + error: `HTTP ${response.status}: Polling failed - ${errorText}`, + }; return; } + const data = await response.json(); - if (tc.state === RCAState.PENDING) { - if (data.state === "completed") { + if (!isFailedState(tc.state)) { + const apiState = data.state?.toLowerCase(); + if (apiState === "completed") { tc.state = RCAState.COMPLETED; tc.rcaData = data; - } else if (data.state && data.state !== "pending") { - tc.state = RCAState.FAILED; + } else if (apiState === "failed" || apiState === "error") { + tc.state = RCAState.UNKNOWN_ERROR; tc.rcaData = { error: `API returned error state: ${data.state}`, originalResponse: data, }; + } else if (apiState === "pending") tc.state = RCAState.PENDING; + else if (apiState === "fetching_logs") + tc.state = RCAState.FETCHING_LOGS; + else if (apiState === "generating_rca") + tc.state = RCAState.GENERATING_RCA; + else if (apiState === "generated_rca") + tc.state = RCAState.GENERATED_RCA; + else if (apiState === "processing" || apiState === "running") + tc.state = RCAState.GENERATING_RCA; + else { + tc.state = RCAState.UNKNOWN_ERROR; + tc.rcaData = { + error: `API returned unknown state: ${data.state}`, + originalResponse: data, + }; } } } catch (err) { - if (tc.state === RCAState.PENDING) { - tc.state = RCAState.FAILED; + if (!isFailedState(tc.state)) { + tc.state = RCAState.LLM_SERVICE_ERROR; tc.rcaData = { error: err instanceof Error @@ -207,11 +230,10 @@ async function pollRCAResults( await delay(pollInterval); } } catch (err) { - // Fallback in case of unexpected error testCases - .filter((tc) => tc.state === RCAState.PENDING) + .filter((tc) => isInProgressState(tc.state)) .forEach((tc) => { - tc.state = RCAState.FAILED; + tc.state = RCAState.UNKNOWN_ERROR; tc.rcaData = { error: err instanceof Error ? err.message : "Unexpected error", }; @@ -226,9 +248,8 @@ async function pollRCAResults( return { testCases }; } -// --- Public API function --- export async function getRCAData( - testInfos: FailedTestInfo[], + testIds: string[], authString: string, context?: ScanProgressContext, ): Promise { @@ -245,31 +266,23 @@ export async function getRCAData( await notifyProgress(context, "Starting RCA analysis for test cases...", 0); - // Step 1: Fire initial RCA requests in parallel const testCases = await Promise.all( - testInfos.map((testInfo) => fetchInitialRCA(testInfo, headers, baseUrl)), + testIds.map((testId) => fetchInitialRCA({ id: testId }, headers, baseUrl)), ); - const pendingCount = testCases.filter( - (tc) => tc.state === RCAState.PENDING, + const inProgressCount = testCases.filter((tc) => + isInProgressState(tc.state), ).length; + await notifyProgress( context, - `Initial RCA requests completed. ${pendingCount} cases pending analysis...`, + `Initial RCA requests completed. ${inProgressCount} cases pending analysis...`, 10, ); - if (pendingCount === 0) { - await notifyProgress( - context, - "RCA analysis completed for all test cases", - 100, - ); - return { testCases }; - } + if (inProgressCount === 0) return { testCases }; - // Step 2: Poll pending test cases - return pollRCAResults( + return await pollRCAResults( testCases, headers, baseUrl, diff --git a/src/tools/rca-agent-utils/types.ts b/src/tools/rca-agent-utils/types.ts new file mode 100644 index 00000000..20413fe5 --- /dev/null +++ b/src/tools/rca-agent-utils/types.ts @@ -0,0 +1,49 @@ +export enum TestStatus { + PASSED = "passed", + FAILED = "failed", + PENDING = "pending", + SKIPPED = "skipped", +} + +export interface TestDetails { + status: TestStatus; + details: any; + children?: TestDetails[]; + display_name?: string; +} + +export interface TestRun { + hierarchy: TestDetails[]; + pagination?: { + has_next: boolean; + next_page: string | null; + }; +} + +export interface FailedTestInfo { + id: string; +} + +export enum RCAState { + PENDING = "pending", + FETCHING_LOGS = "fetching_logs", + GENERATING_RCA = "generating_rca", + GENERATED_RCA = "generated_rca", + COMPLETED = "completed", + FAILED = "failed", + LLM_SERVICE_ERROR = "LLM_SERVICE_ERROR", + LOG_FETCH_ERROR = "LOG_FETCH_ERROR", + UNKNOWN_ERROR = "UNKNOWN_ERROR", + TIMEOUT = "TIMEOUT", +} + +export interface RCATestCase { + id: string; + testRunId: string; + state: RCAState; + rcaData?: any; +} + +export interface RCAResponse { + testCases: RCATestCase[]; +} diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 3ed80c75..7d46e683 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -5,22 +5,23 @@ import logger from "../logger.js"; import { BrowserStackConfig } from "../lib/types.js"; import { getBrowserStackAuth } from "../lib/get-auth.js"; import { getBuildId } from "./rca-agent-utils/get-build-id.js"; -import { getFailedTestIds } from "./rca-agent-utils/get-failed-test-id.js"; +import { getTestIds } from "./rca-agent-utils/get-failed-test-id.js"; import { getRCAData } from "./rca-agent-utils/rca-data.js"; import { formatRCAData } from "./rca-agent-utils/format-rca.js"; +import { TestStatus } from "./rca-agent-utils/types.js"; -// wTool function that fetches RCA data +// Tool function that fetches RCA data export async function fetchRCADataTool( - args: { projectName: string; buildName: string; buildId?: string }, + args: { testId: string[] }, config: BrowserStackConfig, ): Promise { try { - let { projectName, buildName, buildId } = args; const authString = getBrowserStackAuth(config); - const [username, accessKey] = authString.split(":"); - buildId = buildId || await getBuildId(projectName, buildName, username, accessKey); - const testInfos = await getFailedTestIds(buildId, authString); - const rcaData = await getRCAData(testInfos.slice(0, 3), authString); + + // Limit to first 3 test IDs for performance + const testIds = args.testId.slice(0, 3); + + const rcaData = await getRCAData(testIds, authString); const formattedData = formatRCAData(rcaData); @@ -38,6 +39,52 @@ export async function fetchRCADataTool( } } +export async function listTestIdsTool( + args: { + projectName: string; + buildName: string; + buildId?: string; + status?: TestStatus; + }, + config: BrowserStackConfig, +): Promise { + try { + const { projectName, buildName, status } = args; + let { buildId } = args; + const authString = getBrowserStackAuth(config); + const [username, accessKey] = authString.split(":"); + + // Get build ID if not provided + buildId = + buildId || + (await getBuildId(projectName, buildName, username, accessKey)); + + // Get test IDs + const testIds = await getTestIds(buildId, authString, status); + + return { + content: [ + { + type: "text", + text: JSON.stringify(testIds, null, 2), + }, + ], + }; + } catch (error) { + logger.error("Error listing test IDs", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error listing test IDs: ${errorMessage}`, + }, + ], + }; + } +} + // Registers the fetchRCA tool with the MCP server export default function addRCATools( server: McpServer, @@ -49,26 +96,50 @@ export default function addRCATools( "fetchRCA", "Retrieves AI-RCA (Root Cause Analysis) data for a BrowserStack Automate session and provides insights into test failures.", { - projectName: z - .string() - .describe( - "The project name of the test run can be available in browsrestack yml or ask it from user", - ), - buildName: z + testId: z + .array(z.string()) + .describe("Array of test IDs to fetch RCA data for"), + }, + async (args) => { + try { + return await fetchRCADataTool(args, config); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error during fetching RCA data: ${errorMessage}`, + }, + ], + }; + } + }, + ); + + tools.listTestIds = server.tool( + "listTestIds", + "List test IDs from a BrowserStack Automate build, optionally filtered by status (e.g., 'failed', 'passed').", + { + projectName: z.string().describe("The project name of the test run"), + buildName: z.string().describe("The build name of the test run"), + buildId: z .string() + .optional() .describe( - "The build name of the test run can be available in browsrestack yml or ask it from user", + "The build ID of the test run (will be auto-detected if not provided)", ), - buildId: z - .string() + status: z + .nativeEnum(TestStatus) .optional() .describe( - "The build ID of the test run.", + "Filter tests by status. If not provided, all tests are returned.", ), }, async (args) => { try { - return await fetchRCADataTool(args, config); + return await listTestIdsTool(args, config); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; @@ -76,7 +147,7 @@ export default function addRCATools( content: [ { type: "text", - text: `Error during fetching RCA data: ${errorMessage}`, + text: `Error during listing test IDs: ${errorMessage}`, }, ], }; From 3c0fec9d994ff948d90e4c59cad50e164353e3fb Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 8 Sep 2025 14:41:26 +0530 Subject: [PATCH 23/84] RCA data fetching by improving pagination handling --- .../rca-agent-utils/get-failed-test-id.ts | 87 +++++++++---------- src/tools/rca-agent-utils/rca-data.ts | 19 ++-- src/tools/rca-agent-utils/types.ts | 3 +- src/tools/rca-agent.ts | 4 +- 4 files changed, 54 insertions(+), 59 deletions(-) diff --git a/src/tools/rca-agent-utils/get-failed-test-id.ts b/src/tools/rca-agent-utils/get-failed-test-id.ts index 2004d482..bd5b9d1c 100644 --- a/src/tools/rca-agent-utils/get-failed-test-id.ts +++ b/src/tools/rca-agent-utils/get-failed-test-id.ts @@ -1,57 +1,59 @@ import { TestStatus, FailedTestInfo, TestRun, TestDetails } from "./types.js"; -let hasNext = false; -let nextPageUrl: string | null = null; - export async function getTestIds( buildId: string, authString: string, status?: TestStatus, ): Promise { const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`; - - // Build initial URL - const initialUrl = new URL(baseUrl); - if (status) initialUrl.searchParams.set("test_statuses", status); - - // Use stored nextPageUrl if available, otherwise fresh URL - const requestUrl = - hasNext && nextPageUrl ? nextPageUrl : initialUrl.toString(); + let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl; let allFailedTests: FailedTestInfo[] = []; + let requestNumber = 0; // Construct Basic auth header const encodedCredentials = Buffer.from(authString).toString("base64"); const authHeader = `Basic ${encodedCredentials}`; try { - const response = await fetch(requestUrl, { - headers: { - Authorization: authHeader, - "Content-Type": "application/json", - }, - }); + while (true) { + requestNumber++; + + const response = await fetch(url, { + headers: { + Authorization: authHeader, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch test runs: ${response.status} ${response.statusText}`, + ); + } - if (!response.ok) { - throw new Error( - `Failed to fetch test runs: ${response.status} ${response.statusText}`, - ); - } + const data = (await response.json()) as TestRun; - const data = (await response.json()) as TestRun; + // Extract failed IDs from current page + if (data.hierarchy && data.hierarchy.length > 0) { + const currentFailedTests = extractFailedTestIds(data.hierarchy); + allFailedTests = allFailedTests.concat(currentFailedTests); + } - // Extract failed IDs from current page - if (data.hierarchy && data.hierarchy.length > 0) { - allFailedTests = extractFailedTestIds(data.hierarchy); - } + // Check for pagination termination conditions + if (!data.pagination?.has_next || !data.pagination.next_page) { + break; + } + + // Safety limit to prevent runaway requests + if (requestNumber >= 5) { + break; + } - // Update pagination state in memory - hasNext = data.pagination?.has_next || false; - nextPageUrl = - hasNext && data.pagination?.next_page - ? buildNextPageUrl(baseUrl, status, data.pagination.next_page) - : null; + // Prepare next request + url = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`; + } - // Return failed test IDs from current page only + // Return unique failed test IDs return allFailedTests; } catch (error) { console.error("Error fetching failed tests:", error); @@ -59,18 +61,6 @@ export async function getTestIds( } } -// Helper to build next page URL safely -function buildNextPageUrl( - baseUrl: string, - status: TestStatus | undefined, - nextPage: string, -): string { - const url = new URL(baseUrl); - if (status) url.searchParams.set("test_statuses", status); - url.searchParams.set("next_page", nextPage); - return url.toString(); -} - // Recursive function to extract failed test IDs from hierarchy function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { let failedTests: FailedTestInfo[] = []; @@ -80,7 +70,10 @@ function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { if (node.details?.observability_url) { const idMatch = node.details.observability_url.match(/details=(\d+)/); if (idMatch) { - failedTests.push({ id: idMatch[1] }); + failedTests.push({ + test_id: idMatch[1], + test_name: node.display_name || `Test ${idMatch[1]}`, + }); } } } diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts index 65f9489b..97edce69 100644 --- a/src/tools/rca-agent-utils/rca-data.ts +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -1,4 +1,3 @@ -import { FailedTestInfo } from "./types.js"; import { RCAState, RCATestCase, RCAResponse } from "./types.js"; interface ScanProgressContext { @@ -76,19 +75,19 @@ async function updateProgress( } async function fetchInitialRCA( - testInfo: FailedTestInfo, + testId: string, headers: Record, baseUrl: string, ): Promise { - const url = baseUrl.replace("{testId}", testInfo.id); + const url = baseUrl.replace("{testId}", testId); try { const response = await fetch(url, { headers }); if (!response.ok) { return { - id: testInfo.id, - testRunId: testInfo.id, + id: testId, + testRunId: testId, state: RCAState.LOG_FETCH_ERROR, rcaData: { error: `HTTP ${response.status}: Failed to start RCA analysis`, @@ -115,8 +114,8 @@ async function fetchInitialRCA( else resultState = RCAState.PENDING; return { - id: testInfo.id, - testRunId: testInfo.id, + id: testId, + testRunId: testId, state: resultState, ...(resultState === RCAState.COMPLETED && { rcaData: data }), ...(isFailedState(resultState) && @@ -129,8 +128,8 @@ async function fetchInitialRCA( }; } catch (error) { return { - id: testInfo.id, - testRunId: testInfo.id, + id: testId, + testRunId: testId, state: RCAState.LLM_SERVICE_ERROR, rcaData: { error: @@ -267,7 +266,7 @@ export async function getRCAData( await notifyProgress(context, "Starting RCA analysis for test cases...", 0); const testCases = await Promise.all( - testIds.map((testId) => fetchInitialRCA({ id: testId }, headers, baseUrl)), + testIds.map((testId) => fetchInitialRCA(testId, headers, baseUrl)), ); const inProgressCount = testCases.filter((tc) => diff --git a/src/tools/rca-agent-utils/types.ts b/src/tools/rca-agent-utils/types.ts index 20413fe5..c03fbdfa 100644 --- a/src/tools/rca-agent-utils/types.ts +++ b/src/tools/rca-agent-utils/types.ts @@ -21,7 +21,8 @@ export interface TestRun { } export interface FailedTestInfo { - id: string; + test_id: string; + test_name: string; } export enum RCAState { diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 7d46e683..ccd010b0 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -98,7 +98,9 @@ export default function addRCATools( { testId: z .array(z.string()) - .describe("Array of test IDs to fetch RCA data for"), + .describe( + "Array of test IDs to fetch RCA data for If not provided call listTestIds tool first to get the IDs", + ), }, async (args) => { try { From ff81d6ed7322cf54f8a846a25e0b35b7a920ca41 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 9 Sep 2025 14:36:19 +0530 Subject: [PATCH 24/84] refactor: Simplify build ID retrieval and enhance tool descriptions for clarity --- src/tools/rca-agent.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index ccd010b0..eac844f2 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -43,21 +43,22 @@ export async function listTestIdsTool( args: { projectName: string; buildName: string; - buildId?: string; status?: TestStatus; }, config: BrowserStackConfig, ): Promise { try { const { projectName, buildName, status } = args; - let { buildId } = args; const authString = getBrowserStackAuth(config); const [username, accessKey] = authString.split(":"); // Get build ID if not provided - buildId = - buildId || - (await getBuildId(projectName, buildName, username, accessKey)); + const buildId = await getBuildId( + username, + accessKey, + projectName, + buildName, + ); // Get test IDs const testIds = await getTestIds(buildId, authString, status); @@ -94,12 +95,13 @@ export default function addRCATools( tools.fetchRCA = server.tool( "fetchRCA", - "Retrieves AI-RCA (Root Cause Analysis) data for a BrowserStack Automate session and provides insights into test failures.", + "Retrieves AI-RCA (Root Cause Analysis) data for a BrowserStack Automate and App-Automate session and provides insights into test failures.", { testId: z .array(z.string()) + .max(3) .describe( - "Array of test IDs to fetch RCA data for If not provided call listTestIds tool first to get the IDs", + "Array of test IDs to fetch RCA data. Input should be a maximum of 3 IDs at a time. If you get more than 3 Ids ask user to choose less than 3", ), }, async (args) => { @@ -122,16 +124,10 @@ export default function addRCATools( tools.listTestIds = server.tool( "listTestIds", - "List test IDs from a BrowserStack Automate build, optionally filtered by status (e.g., 'failed', 'passed').", + "List test IDs from a BrowserStack Automate build, optionally filtered by status", { projectName: z.string().describe("The project name of the test run"), buildName: z.string().describe("The build name of the test run"), - buildId: z - .string() - .optional() - .describe( - "The build ID of the test run (will be auto-detected if not provided)", - ), status: z .nativeEnum(TestStatus) .optional() From 7858d84afb4d40b2e32dfb6a32aea70044f275e6 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 9 Sep 2025 14:41:56 +0530 Subject: [PATCH 25/84] fix: Update timeout value in getRCAData function and improve description for test ID input --- src/tools/rca-agent-utils/rca-data.ts | 2 +- src/tools/rca-agent.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts index 97edce69..a1b0a601 100644 --- a/src/tools/rca-agent-utils/rca-data.ts +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -253,7 +253,7 @@ export async function getRCAData( context?: ScanProgressContext, ): Promise { const pollInterval = 5000; - const timeout = 30000; + const timeout = 40000; const initialDelay = 20000; const baseUrl = diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index eac844f2..7f623e07 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -101,7 +101,7 @@ export default function addRCATools( .array(z.string()) .max(3) .describe( - "Array of test IDs to fetch RCA data. Input should be a maximum of 3 IDs at a time. If you get more than 3 Ids ask user to choose less than 3", + "Array of test IDs to fetch RCA data for (maximum 3 IDs). Use the listTestIds tool to discover available test IDs if needed. If more than 3 IDs are provided, only the first 3 will be processed.", ), }, async (args) => { From eac1aa653275e683eb538718aaebf228e2185142 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 9 Sep 2025 16:25:32 +0530 Subject: [PATCH 26/84] Enhance descriptions for test ID and project/build name inputs in RCA tools --- src/tools/rca-agent.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 7f623e07..df097dc0 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -101,7 +101,7 @@ export default function addRCATools( .array(z.string()) .max(3) .describe( - "Array of test IDs to fetch RCA data for (maximum 3 IDs). Use the listTestIds tool to discover available test IDs if needed. If more than 3 IDs are provided, only the first 3 will be processed.", + "Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed.", ), }, async (args) => { @@ -126,13 +126,20 @@ export default function addRCATools( "listTestIds", "List test IDs from a BrowserStack Automate build, optionally filtered by status", { - projectName: z.string().describe("The project name of the test run"), - buildName: z.string().describe("The build name of the test run"), + projectName: z + .string() + .describe( + "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user,IF not found or unsure, prompt the user for this value. Do not make assumptions", + ), + buildName: z + .string() + .describe( + "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user,IF not found or unsure, prompt the user for this value. Do not make assumptions", + ), status: z .nativeEnum(TestStatus) - .optional() .describe( - "Filter tests by status. If not provided, all tests are returned.", + "Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status", ), }, async (args) => { From 670614298069f76cdf3058ccf515e0e9b8286619 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 9 Sep 2025 16:42:14 +0530 Subject: [PATCH 27/84] fix: Improve progress calculation and centralize API state mapping for clarity --- src/tools/rca-agent-utils/rca-data.ts | 66 ++++++++++++--------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts index a1b0a601..0f60c0ac 100644 --- a/src/tools/rca-agent-utils/rca-data.ts +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -33,11 +33,33 @@ function calculateProgress( totalCount: number, baseProgress: number = 10, ): number { + if (totalCount === 0) return 100; // ✅ fix divide by zero const progressRange = 90 - baseProgress; const completionProgress = (resolvedCount / totalCount) * progressRange; return Math.min(100, baseProgress + completionProgress); } +// ✅ centralized mapping function +function mapApiState(apiState?: string): RCAState { + const state = apiState?.toLowerCase(); + switch (state) { + case "completed": + return RCAState.COMPLETED; + case "pending": + return RCAState.PENDING; + case "fetching_logs": + return RCAState.FETCHING_LOGS; + case "generating_rca": + return RCAState.GENERATING_RCA; + case "generated_rca": + return RCAState.GENERATED_RCA; + case "error": + return RCAState.UNKNOWN_ERROR; + default: + return RCAState.UNKNOWN_ERROR; + } +} + async function notifyProgress( context: ScanProgressContext | undefined, message: string, @@ -96,22 +118,7 @@ async function fetchInitialRCA( } const data = await response.json(); - - const apiState = data.state?.toLowerCase(); - let resultState: RCAState; - - if (apiState === "completed") resultState = RCAState.COMPLETED; - else if (apiState === "pending") resultState = RCAState.PENDING; - else if (apiState === "fetching_logs") resultState = RCAState.FETCHING_LOGS; - else if (apiState === "generating_rca") - resultState = RCAState.GENERATING_RCA; - else if (apiState === "generated_rca") resultState = RCAState.GENERATED_RCA; - else if (apiState === "processing" || apiState === "running") - resultState = RCAState.GENERATING_RCA; - else if (apiState === "failed" || apiState === "error") - resultState = RCAState.UNKNOWN_ERROR; - else if (apiState) resultState = RCAState.UNKNOWN_ERROR; - else resultState = RCAState.PENDING; + const resultState = mapApiState(data.state); // ✅ reuse helper return { id: testId, @@ -185,29 +192,14 @@ async function pollRCAResults( const data = await response.json(); if (!isFailedState(tc.state)) { - const apiState = data.state?.toLowerCase(); - if (apiState === "completed") { - tc.state = RCAState.COMPLETED; + const mappedState = mapApiState(data.state); // ✅ reuse helper + tc.state = mappedState; + + if (mappedState === RCAState.COMPLETED) { tc.rcaData = data; - } else if (apiState === "failed" || apiState === "error") { - tc.state = RCAState.UNKNOWN_ERROR; - tc.rcaData = { - error: `API returned error state: ${data.state}`, - originalResponse: data, - }; - } else if (apiState === "pending") tc.state = RCAState.PENDING; - else if (apiState === "fetching_logs") - tc.state = RCAState.FETCHING_LOGS; - else if (apiState === "generating_rca") - tc.state = RCAState.GENERATING_RCA; - else if (apiState === "generated_rca") - tc.state = RCAState.GENERATED_RCA; - else if (apiState === "processing" || apiState === "running") - tc.state = RCAState.GENERATING_RCA; - else { - tc.state = RCAState.UNKNOWN_ERROR; + } else if (mappedState === RCAState.UNKNOWN_ERROR) { tc.rcaData = { - error: `API returned unknown state: ${data.state}`, + error: `API returned state: ${data.state}`, originalResponse: data, }; } From 8823dec5baeda7ce072b332c60506cad876421d2 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 9 Sep 2025 19:23:02 +0530 Subject: [PATCH 28/84] refactor: update formatToolResult to accept runType parameter and enhance critical message for non-percy-web runs --- src/tools/sdk-utils/common/schema.ts | 4 +--- src/tools/sdk-utils/common/utils.ts | 8 ++++++++ src/tools/sdk-utils/handler.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 73f8aa0a..754d30d0 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -28,9 +28,7 @@ export const SetUpPercyParamsShape = { export const RunTestsOnBrowserStackParamsShape = { projectName: z .string() - .describe( - "A single name for your project to organize all your tests. This is required for Percy.", - ), + .describe("A single name for your project to organize all your tests."), detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), detectedBrowserAutomationFramework: z.nativeEnum( SDKSupportedBrowserAutomationFrameworkEnum, diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts index 9ddd6793..d21cbc9e 100644 --- a/src/tools/sdk-utils/common/utils.ts +++ b/src/tools/sdk-utils/common/utils.ts @@ -47,6 +47,7 @@ export function checkPercyIntegrationSupport(input: { export async function formatToolResult( resultPromise: Promise | RunTestsInstructionResult, + runType?: "percy-web" | "bstack" | "percy-bstack" | "percy-automate", ): Promise { const { steps, requiresPercy, missingDependencies, shouldSkipFormatting } = await resultPromise; @@ -75,6 +76,13 @@ export async function formatToolResult( { type: "text" as const, text: verificationMessage }, ]; + if (runType != "percy-web") { + finalContent.push({ + type: "text", + text: "CRITICAL: After the build is run, you MUST explicitly ask the user if they want to perform RCA (Root Cause Analysis) for any failed tests. This step is mandatory and should not be skipped.", + }); + } + return { content: finalContent, isError: steps.some((s) => s.isError), diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 4b2bc942..e744580e 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -69,7 +69,7 @@ export async function setUpPercyHandler( ); const result = runPercyWeb(percyInput, percyToken); - return await formatToolResult(result); + return await formatToolResult(result, "percy-web"); } else if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { // First try Percy with BrowserStack SDK const percyWithBrowserstackSDKResult = runPercyWithBrowserstackSDK( From 0f5c13ad0369edade051db8bc6cd64a2239c5c6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 02:24:03 +0000 Subject: [PATCH 29/84] chore(deps-dev): bump vite from 6.3.5 to 6.3.6 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.3.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4120c7e1..f6c6467b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7217,10 +7217,11 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", From bc804f96aa508da3d258917b32a6649a41de2028 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Wed, 10 Sep 2025 13:55:59 +0530 Subject: [PATCH 30/84] Updating instructions for one click setup --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a5eb505..497b585d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,15 @@ Stay in flow—keep all project context in one place and trigger actions directl ## ⚡️ One Click MCP Setup [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor) - +#### Note : Ensure you are using Node version >= `18.0` +- Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) +- To Upgrade Node : +- 1. On macOS `(Homebrew) - brew update && brew upgrade node or if using (nvm) - nvm install 22.15.0 && nvm use 22.15.0 && nvm alias default 22.15.0` +- 2. On Windows `(nvm-windows) : nvm install 22.15.0 && nvm use 22.15.0` +- 👉
Or directly download the Node.js LTS Installer + +. + ## 💡 Usage Examples ### 📱 Manual App Testing @@ -142,6 +150,10 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and ## 🛠️ Installation +### 📋 Prerequisites for MCP Setup +#### Note : Ensure you are using Node version >= `18.0` +- Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) + ### **One Click MCP Setup** [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor) @@ -158,7 +170,9 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and - Once you have an account (and purchased appropriate plan), note down your `username` and `access_key` from [Account Settings](https://www.browserstack.com/accounts/profile/details). -2. Ensure you are using Node version >= `18.0`. Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) +2. #### Note : Ensure you are using Node version >= `18.0` + - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) + 3. **Install the MCP Server** From b17a879f12146972bf998717547952f1ed75c50a Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 14:29:38 +0530 Subject: [PATCH 31/84] refactoring for RCA Tool --- src/tools/rca-agent.ts | 93 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index df097dc0..333a52d8 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -10,6 +10,47 @@ import { getRCAData } from "./rca-agent-utils/rca-data.js"; import { formatRCAData } from "./rca-agent-utils/format-rca.js"; import { TestStatus } from "./rca-agent-utils/types.js"; +// Tool function to fetch build ID +export async function getBuildIdTool( + args: { + projectName: string; + buildName: string; + }, + config: BrowserStackConfig, +): Promise { + try { + const { projectName, buildName } = args; + const authString = getBrowserStackAuth(config); + const [username, accessKey] = authString.split(":"); + const buildId = await getBuildId( + username, + accessKey, + projectName, + buildName, + ); + return { + content: [ + { + type: "text", + text: buildId, + }, + ], + }; + } catch (error) { + logger.error("Error fetching build ID", error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error fetching build ID: ${errorMessage}`, + }, + ], + }; + } +} + // Tool function that fetches RCA data export async function fetchRCADataTool( args: { testId: string[] }, @@ -41,24 +82,14 @@ export async function fetchRCADataTool( export async function listTestIdsTool( args: { - projectName: string; - buildName: string; + buildId: string; status?: TestStatus; }, config: BrowserStackConfig, ): Promise { try { - const { projectName, buildName, status } = args; + const { buildId, status } = args; const authString = getBrowserStackAuth(config); - const [username, accessKey] = authString.split(":"); - - // Get build ID if not provided - const buildId = await getBuildId( - username, - accessKey, - projectName, - buildName, - ); // Get test IDs const testIds = await getTestIds(buildId, authString, status); @@ -122,19 +153,47 @@ export default function addRCATools( }, ); - tools.listTestIds = server.tool( - "listTestIds", - "List test IDs from a BrowserStack Automate build, optionally filtered by status", + tools.getBuildId = server.tool( + "getBuildId", + "Get the BrowserStack build ID for a given project and build name.", { projectName: z .string() .describe( - "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user,IF not found or unsure, prompt the user for this value. Do not make assumptions", + "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", ), buildName: z .string() .describe( - "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user,IF not found or unsure, prompt the user for this value. Do not make assumptions", + "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", + ), + }, + async (args) => { + try { + return await getBuildIdTool(args, config); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error during fetching build ID: ${errorMessage}`, + }, + ], + }; + } + }, + ); + + tools.listTestIds = server.tool( + "listTestIds", + "List test IDs from a BrowserStack Automate build, optionally filtered by status", + { + buildId: z + .string() + .describe( + "The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name", ), status: z .nativeEnum(TestStatus) From a8ead54159ce931817eb2cc3ac0032114e6b2ee7 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 14:35:18 +0530 Subject: [PATCH 32/84] feat: Add build insights tool to fetch and display build details and quality gate results --- src/lib/utils.ts | 24 +++++++++ src/server-factory.ts | 2 + src/tools/build-insights.ts | 98 +++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 src/tools/build-insights.ts diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e19532c9..c12b70f7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,7 @@ import sharp from "sharp"; import type { ApiResponse } from "./apiClient.js"; +import { BrowserStackConfig } from "./types.js"; +import { getBrowserStackAuth } from "./get-auth.js"; export function sanitizeUrlParam(param: string): string { // Remove any characters that could be used for command injection @@ -38,3 +40,25 @@ export async function assertOkResponse( ); } } + +export async function fetchFromBrowserStackAPI( + url: string, + config: BrowserStackConfig, +): Promise { + const authString = getBrowserStackAuth(config); + const auth = Buffer.from(authString).toString("base64"); + + const res = await fetch(url, { + headers: { + Authorization: `Basic ${auth}`, + }, + }); + + if (!res.ok) { + throw new Error( + `Failed to fetch from ${url}: ${res.status} ${res.statusText}`, + ); + } + + return res.json(); +} diff --git a/src/server-factory.ts b/src/server-factory.ts index 82f93730..4a39adf8 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -15,6 +15,7 @@ import addFailureLogsTools from "./tools/get-failure-logs.js"; import addAutomateTools from "./tools/automate.js"; import addSelfHealTools from "./tools/selfheal.js"; import addAppLiveTools from "./tools/applive.js"; +import addBuildInsightsTools from "./tools/build-insights.js"; import { setupOnInitialized } from "./oninitialized.js"; import { BrowserStackConfig } from "./lib/types.js"; @@ -55,6 +56,7 @@ export class BrowserStackMcpServer { addFailureLogsTools, addAutomateTools, addSelfHealTools, + addBuildInsightsTools, ]; toolAdders.forEach((adder) => { diff --git a/src/tools/build-insights.ts b/src/tools/build-insights.ts new file mode 100644 index 00000000..a771441d --- /dev/null +++ b/src/tools/build-insights.ts @@ -0,0 +1,98 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import logger from "../logger.js"; +import { BrowserStackConfig } from "../lib/types.js"; +import { fetchFromBrowserStackAPI } from "../lib/utils.js"; + +// Tool function that fetches build insights from two APIs +export async function fetchBuildInsightsTool( + args: { buildId: string }, + config: BrowserStackConfig, +): Promise { + try { + const buildUrl = `https://api-automation.browserstack.com/ext/v1/builds/${args.buildId}`; + const qualityGateUrl = `https://api-automation.browserstack.com/ext/v1/quality-gates/${args.buildId}`; + + const [buildData, qualityData] = await Promise.all([ + fetchFromBrowserStackAPI(buildUrl, config), + fetchFromBrowserStackAPI(qualityGateUrl, config), + ]); + + // Select useful fields for users + const insights = { + name: buildData.name, + status: buildData.status, + duration: buildData.duration, + user: buildData.user, + tags: buildData.tags, + alerts: buildData.alerts, + status_stats: buildData.status_stats, + failure_categories: buildData.failure_categories, + smart_tags: buildData.smart_tags, + unique_errors: buildData.unique_errors?.overview, + observability_url: buildData?.observability_url, + ci_build_url: buildData.ci_info.build_url, + quality_gate_result: qualityData.quality_gate_result, + }; + + const qualityProfiles = qualityData.quality_profiles?.map( + (profile: any) => ({ + name: profile.name, + result: profile.result, + }), + ); + + const qualityProfilesText = + qualityProfiles && qualityProfiles.length > 0 + ? `Quality Gate Profiles (respond only if explicitly requested): ${JSON.stringify(qualityProfiles, null, 2)}` + : "No Quality Gate Profiles available."; + + return { + content: [ + { + type: "text", + text: "Build insights:\n" + JSON.stringify(insights, null, 2), + }, + { type: "text", text: qualityProfilesText }, + ], + }; + } catch (error) { + logger.error("Error fetching build insights", error); + throw error; + } +} + +// Registers the fetchBuildInsights tool with the MCP server +export default function addBuildInsightsTools( + server: McpServer, + config: BrowserStackConfig, +) { + const tools: Record = {}; + + tools.fetchBuildInsights = server.tool( + "fetchBuildInsights", + "Fetches insights about a BrowserStack build by combining build details and quality gate results.", + { + buildId: z.string().describe("The build UUID of the BrowserStack build"), + }, + async (args) => { + try { + return await fetchBuildInsightsTool(args, config); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return { + content: [ + { + type: "text", + text: `Error during fetching build insights: ${errorMessage}`, + }, + ], + }; + } + }, + ); + + return tools; +} From 9c5f3d531548e17e5b7ea33873c4787ff1b750ec Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 15:33:01 +0530 Subject: [PATCH 33/84] refactor: Remove logger and adjust test ID --- src/tools/rca-agent-utils/format-rca.ts | 5 ----- src/tools/rca-agent-utils/rca-data.ts | 4 ++-- src/tools/rca-agent.ts | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/tools/rca-agent-utils/format-rca.ts b/src/tools/rca-agent-utils/format-rca.ts index 154f4570..c4e84600 100644 --- a/src/tools/rca-agent-utils/format-rca.ts +++ b/src/tools/rca-agent-utils/format-rca.ts @@ -1,10 +1,5 @@ -import logger from "../../logger.js"; - // Utility function to format RCA data for better readability export function formatRCAData(rcaData: any): string { - logger.info( - `Formatting RCA data for output: ${JSON.stringify(rcaData, null, 2)}`, - ); if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) { return "No RCA data available."; } diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts index 0f60c0ac..1e66220e 100644 --- a/src/tools/rca-agent-utils/rca-data.ts +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -118,7 +118,7 @@ async function fetchInitialRCA( } const data = await response.json(); - const resultState = mapApiState(data.state); // ✅ reuse helper + const resultState = mapApiState(data.state); return { id: testId, @@ -192,7 +192,7 @@ async function pollRCAResults( const data = await response.json(); if (!isFailedState(tc.state)) { - const mappedState = mapApiState(data.state); // ✅ reuse helper + const mappedState = mapApiState(data.state); tc.state = mappedState; if (mappedState === RCAState.COMPLETED) { diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 333a52d8..d2e53e85 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -60,7 +60,7 @@ export async function fetchRCADataTool( const authString = getBrowserStackAuth(config); // Limit to first 3 test IDs for performance - const testIds = args.testId.slice(0, 3); + const testIds = args.testId; const rcaData = await getRCAData(testIds, authString); From d12c3ce0c570a677f348c68ce088a9546cd27a01 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 15:35:03 +0530 Subject: [PATCH 34/84] Update src/tools/rca-agent.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tools/rca-agent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index d2e53e85..52770a38 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -23,10 +23,10 @@ export async function getBuildIdTool( const authString = getBrowserStackAuth(config); const [username, accessKey] = authString.split(":"); const buildId = await getBuildId( - username, - accessKey, projectName, buildName, + username, + accessKey, ); return { content: [ From 0b9c146cae90a03e461d1e8142463753b3f8a5b7 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 15:36:17 +0530 Subject: [PATCH 35/84] Replace console.error with logger --- src/tools/rca-agent-utils/get-failed-test-id.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/rca-agent-utils/get-failed-test-id.ts b/src/tools/rca-agent-utils/get-failed-test-id.ts index bd5b9d1c..7e8e6fb2 100644 --- a/src/tools/rca-agent-utils/get-failed-test-id.ts +++ b/src/tools/rca-agent-utils/get-failed-test-id.ts @@ -1,3 +1,4 @@ +import logger from "../../logger.js"; import { TestStatus, FailedTestInfo, TestRun, TestDetails } from "./types.js"; export async function getTestIds( @@ -56,7 +57,7 @@ export async function getTestIds( // Return unique failed test IDs return allFailedTests; } catch (error) { - console.error("Error fetching failed tests:", error); + logger.error("Error fetching failed tests:", error); throw error; } } From d61c9a2f487a92c0c6a7738cf9e3da5f5c6163d3 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 15:38:15 +0530 Subject: [PATCH 36/84] Update src/tools/build-insights.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tools/build-insights.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/build-insights.ts b/src/tools/build-insights.ts index a771441d..2cf4db29 100644 --- a/src/tools/build-insights.ts +++ b/src/tools/build-insights.ts @@ -32,7 +32,7 @@ export async function fetchBuildInsightsTool( smart_tags: buildData.smart_tags, unique_errors: buildData.unique_errors?.overview, observability_url: buildData?.observability_url, - ci_build_url: buildData.ci_info.build_url, + ci_build_url: buildData.ci_info?.build_url, quality_gate_result: qualityData.quality_gate_result, }; From c553dec382a7b8a74339e73b29079efcd2040e9f Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 16:11:54 +0530 Subject: [PATCH 37/84] Renaming to review agent --- src/tools/{percy-change.ts => review-agent.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/tools/{percy-change.ts => review-agent.ts} (100%) diff --git a/src/tools/percy-change.ts b/src/tools/review-agent.ts similarity index 100% rename from src/tools/percy-change.ts rename to src/tools/review-agent.ts From 82cc762d8fee39a059f5056f64958f25031f4600 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 16:14:51 +0530 Subject: [PATCH 38/84] refactor: Move parameter definitions to constants for RCA tools --- src/tools/rca-agent-utils/constants.ts | 37 ++++++++++++++++++++++++++ src/tools/rca-agent.ts | 37 +++----------------------- 2 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 src/tools/rca-agent-utils/constants.ts diff --git a/src/tools/rca-agent-utils/constants.ts b/src/tools/rca-agent-utils/constants.ts new file mode 100644 index 00000000..0ff64d9e --- /dev/null +++ b/src/tools/rca-agent-utils/constants.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import { TestStatus } from "./types.js"; + +export const FETCH_RCA_PARAMS = { + testId: z + .array(z.string()) + .max(3) + .describe( + "Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed." + ), +}; + +export const GET_BUILD_ID_PARAMS = { + projectName: z + .string() + .describe( + "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions" + ), + buildName: z + .string() + .describe( + "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions" + ), +}; + +export const LIST_TEST_IDS_PARAMS = { + buildId: z + .string() + .describe( + "The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name" + ), + status: z + .nativeEnum(TestStatus) + .describe( + "Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status" + ), +}; diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 52770a38..3b723b85 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -1,5 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; +import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, LIST_TEST_IDS_PARAMS } from "./rca-agent-utils/constants.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import logger from "../logger.js"; import { BrowserStackConfig } from "../lib/types.js"; @@ -127,14 +127,7 @@ export default function addRCATools( tools.fetchRCA = server.tool( "fetchRCA", "Retrieves AI-RCA (Root Cause Analysis) data for a BrowserStack Automate and App-Automate session and provides insights into test failures.", - { - testId: z - .array(z.string()) - .max(3) - .describe( - "Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed.", - ), - }, + FETCH_RCA_PARAMS, async (args) => { try { return await fetchRCADataTool(args, config); @@ -156,18 +149,7 @@ export default function addRCATools( tools.getBuildId = server.tool( "getBuildId", "Get the BrowserStack build ID for a given project and build name.", - { - projectName: z - .string() - .describe( - "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", - ), - buildName: z - .string() - .describe( - "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", - ), - }, + GET_BUILD_ID_PARAMS, async (args) => { try { return await getBuildIdTool(args, config); @@ -189,18 +171,7 @@ export default function addRCATools( tools.listTestIds = server.tool( "listTestIds", "List test IDs from a BrowserStack Automate build, optionally filtered by status", - { - buildId: z - .string() - .describe( - "The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name", - ), - status: z - .nativeEnum(TestStatus) - .describe( - "Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status", - ), - }, + LIST_TEST_IDS_PARAMS, async (args) => { try { return await listTestIdsTool(args, config); From 1f4f3fb205aedb8c053607f309bb4e5f65652542 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 22:17:01 +0530 Subject: [PATCH 39/84] fix : Instrumentation,refactoring and bump version --- src/lib/utils.ts | 27 ++++++ src/tools/build-insights.ts | 13 +-- src/tools/list-test-files.ts | 6 ++ src/tools/percy-sdk.ts | 89 ++++++++----------- .../percy-snapshot-utils/detect-test-files.ts | 32 +------ src/tools/rca-agent-utils/constants.ts | 52 +++++------ src/tools/rca-agent.ts | 41 ++------- src/tools/review-agent.ts | 4 - src/tools/run-percy-scan.ts | 10 +-- src/tools/sdk-utils/common/schema.ts | 6 +- src/tools/sdk-utils/handler.ts | 16 ++-- 11 files changed, 127 insertions(+), 169 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c12b70f7..bed98a73 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,6 +2,9 @@ import sharp from "sharp"; import type { ApiResponse } from "./apiClient.js"; import { BrowserStackConfig } from "./types.js"; import { getBrowserStackAuth } from "./get-auth.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { trackMCP } from "../index.js"; export function sanitizeUrlParam(param: string): string { // Remove any characters that could be used for command injection @@ -62,3 +65,27 @@ export async function fetchFromBrowserStackAPI( return res.json(); } + +function errorContent(message: string): CallToolResult { + return { + content: [{ type: "text", text: message }], + isError: true, + }; +} + +export function handleMCPError( + toolName: string, + server: McpServer, + config: BrowserStackConfig, + error: unknown, +) { + trackMCP(toolName, server.server.getClientVersion()!, error, config); + + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + + const readableToolName = toolName.replace(/([A-Z])/g, " $1").toLowerCase(); + + return errorContent( + `Failed to ${readableToolName}: ${errorMessage}. Please open an issue on GitHub if the problem persists`, + ); +} diff --git a/src/tools/build-insights.ts b/src/tools/build-insights.ts index 2cf4db29..60f685fe 100644 --- a/src/tools/build-insights.ts +++ b/src/tools/build-insights.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import logger from "../logger.js"; import { BrowserStackConfig } from "../lib/types.js"; -import { fetchFromBrowserStackAPI } from "../lib/utils.js"; +import { fetchFromBrowserStackAPI, handleMCPError } from "../lib/utils.js"; // Tool function that fetches build insights from two APIs export async function fetchBuildInsightsTool( @@ -80,16 +80,7 @@ export default function addBuildInsightsTools( try { return await fetchBuildInsightsTool(args, config); } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text", - text: `Error during fetching build insights: ${errorMessage}`, - }, - ], - }; + return handleMCPError("fetchBuildInsights", server, config, error); } }, ); diff --git a/src/tools/list-test-files.ts b/src/tools/list-test-files.ts index 7fb1d897..d3fea93b 100644 --- a/src/tools/list-test-files.ts +++ b/src/tools/list-test-files.ts @@ -7,6 +7,12 @@ export async function addListTestFiles(args: any): Promise { const { dirs, language, framework } = args; let testFiles: string[] = []; + if (!dirs || dirs.length === 0) { + throw new Error( + "No directories provided to add the test files. Please provide test directories to add percy snapshot commands.", + ); + } + for (const dir of dirs) { const files = await listTestFiles({ language, diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 1af0a484..37d436ff 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -1,6 +1,6 @@ import { trackMCP } from "../index.js"; import { BrowserStackConfig } from "../lib/types.js"; -import { fetchPercyChanges } from "./percy-change.js"; +import { fetchPercyChanges } from "./review-agent.js"; import { addListTestFiles } from "./list-test-files.js"; import { runPercyScan } from "./run-percy-scan.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -25,6 +25,7 @@ import { FetchPercyChangesParamsShape, ManagePercyBuildApprovalParamsShape, } from "./sdk-utils/common/schema.js"; +import { handleMCPError } from "../lib/utils.js"; export function registerPercyTools( server: McpServer, @@ -32,7 +33,6 @@ export function registerPercyTools( ) { const tools: Record = {}; - // Register setupPercyVisualTesting tools.setupPercyVisualTesting = server.tool( "setupPercyVisualTesting", SETUP_PERCY_DESCRIPTION, @@ -46,26 +46,11 @@ export function registerPercyTools( ); return setUpPercyHandler(args, config); } catch (error) { - trackMCP( - "setupPercyVisualTesting", - server.server.getClientVersion()!, - error, - config, - ); - return { - content: [ - { - type: "text", - text: error instanceof Error ? error.message : String(error), - }, - ], - isError: true, - }; + return handleMCPError("setupPercyVisualTesting", server, config, error); } }, ); - // Register addPercySnapshotCommands tools.addPercySnapshotCommands = server.tool( "addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, @@ -79,26 +64,16 @@ export function registerPercyTools( ); return await updateTestsWithPercyCommands(args); } catch (error) { - trackMCP( + return handleMCPError( "addPercySnapshotCommands", - server.server.getClientVersion()!, - error, + server, config, + error, ); - return { - content: [ - { - type: "text", - text: error instanceof Error ? error.message : String(error), - }, - ], - isError: true, - }; } }, ); - // Register listTestFiles tools.listTestFiles = server.tool( "listTestFiles", LIST_TEST_FILES_DESCRIPTION, @@ -108,21 +83,7 @@ export function registerPercyTools( trackMCP("listTestFiles", server.server.getClientVersion()!, config); return addListTestFiles(args); } catch (error) { - trackMCP( - "listTestFiles", - server.server.getClientVersion()!, - error, - config, - ); - return { - content: [ - { - type: "text", - text: error instanceof Error ? error.message : String(error), - }, - ], - isError: true, - }; + return handleMCPError("listTestFiles", server, config, error); } }, ); @@ -132,16 +93,30 @@ export function registerPercyTools( "Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool", RunPercyScanParamsShape, async (args) => { - return runPercyScan(args, config); + try { + trackMCP("runPercyScan", server.server.getClientVersion()!, config); + return runPercyScan(args, config); + } catch (error) { + return handleMCPError("runPercyScan", server, config, error); + } }, ); tools.fetchPercyChanges = server.tool( "fetchPercyChanges", - "Retrieves and summarizes all visual changes detected by Percy between the latest and previous builds, helping quickly review what has changed in your project.", + "Retrieves and summarizes all visual changes detected by Percy AI between the latest and previous builds, helping quickly review what has changed in your project.", FetchPercyChangesParamsShape, async (args) => { - return await fetchPercyChanges(args, config); + try { + trackMCP( + "fetchPercyChanges", + server.server.getClientVersion()!, + config, + ); + return await fetchPercyChanges(args, config); + } catch (error) { + return handleMCPError("fetchPercyChanges", server, config, error); + } }, ); @@ -150,7 +125,21 @@ export function registerPercyTools( "Approve or reject a Percy build", ManagePercyBuildApprovalParamsShape, async (args) => { - return await approveOrDeclinePercyBuild(args, config); + try { + trackMCP( + "managePercyBuildApproval", + server.server.getClientVersion()!, + config, + ); + return await approveOrDeclinePercyBuild(args, config); + } catch (error) { + return handleMCPError( + "managePercyBuildApproval", + server, + config, + error, + ); + } }, ); diff --git a/src/tools/percy-snapshot-utils/detect-test-files.ts b/src/tools/percy-snapshot-utils/detect-test-files.ts index 876b738f..40886d1d 100644 --- a/src/tools/percy-snapshot-utils/detect-test-files.ts +++ b/src/tools/percy-snapshot-utils/detect-test-files.ts @@ -1,6 +1,5 @@ import fs from "fs"; import path from "path"; -import logger from "../../logger.js"; import { SDKSupportedLanguage, @@ -38,7 +37,7 @@ async function walkDir( } } } catch { - logger.error(`Failed to read directory: ${dir}`); + // ignore } return result; @@ -54,7 +53,6 @@ async function fileContainsRegex( const content = await fs.promises.readFile(filePath, "utf8"); return regexes.some((re) => re.test(content)); } catch { - logger.warn(`Failed to read file: ${filePath}`); return false; } } @@ -69,7 +67,6 @@ async function batchRegexCheck( regexes.length > 0 ? regexes.some((re) => re.test(content)) : false, ); } catch { - logger.warn(`Failed to read file: ${filePath}`); return regexGroups.map(() => false); } } @@ -110,7 +107,6 @@ export async function listTestFiles( const config = TEST_FILE_DETECTION[language]; if (!config) { - logger.error(`Unsupported language: ${language}`); return []; } @@ -135,7 +131,6 @@ export async function listTestFiles( if (config.namePatterns.some((pattern) => pattern.test(fileName))) { candidateFiles.set(file, score); - logger.debug(`File matched by name pattern: ${file} (score: ${score})`); } } @@ -147,7 +142,6 @@ export async function listTestFiles( const fileName = path.basename(file); const score = getFileScore(fileName, config); candidateFiles.set(file, score); - logger.debug(`File matched by content regex: ${file} (score: ${score})`); } }); @@ -158,16 +152,12 @@ export async function listTestFiles( try { const featureFiles = await walkDir(baseDir, [".feature"], 6); featureFiles.forEach((file) => candidateFiles.set(file, 2)); - logger.info(`Added ${featureFiles.length} SpecFlow .feature files`); } catch { - logger.warn( - `Failed to collect SpecFlow .feature files from baseDir: ${baseDir}`, - ); + // ignore } } if (candidateFiles.size === 0) { - logger.info("No test files found matching patterns"); return []; } @@ -185,7 +175,6 @@ export async function listTestFiles( const isUITest = await isLikelyUITest(file); if (isUITest) { - logger.debug(`File included - strong UI indicators: ${file}`); return file; } @@ -198,43 +187,29 @@ export async function listTestFiles( config.excludeRegex || [], ]); - // Skip if explicitly excluded (mocks, unit tests, etc.) if (shouldExclude) { - logger.debug(`File excluded by exclude regex: ${file}`); return null; } - // Skip backend tests in any mode if (hasBackend) { - logger.debug(`File excluded as backend test: ${file}`); return null; } - // Include if has explicit UI drivers if (hasExplicitUI) { - logger.debug(`File included - explicit UI drivers: ${file}`); return file; } - // Include if has UI indicators (for cases where drivers aren't explicitly imported) if (hasUIIndicators) { - logger.debug(`File included - UI indicators: ${file}`); return file; } - // In non-strict mode, include high-scoring test files even without explicit UI patterns if (!strictMode) { const score = candidateFiles.get(file) || 0; if (score >= 3) { - // High confidence UI test based on naming - logger.debug( - `File included - high confidence score: ${file} (score: ${score})`, - ); return file; } } - logger.debug(`File excluded - no UI patterns detected: ${file}`); return null; }); @@ -251,9 +226,6 @@ export async function listTestFiles( return scoreB - scoreA; }); - logger.info( - `Returning ${uiFiles.length} UI test files from ${candidateFiles.size} total test files`, - ); return uiFiles; } diff --git a/src/tools/rca-agent-utils/constants.ts b/src/tools/rca-agent-utils/constants.ts index 0ff64d9e..f1ca985e 100644 --- a/src/tools/rca-agent-utils/constants.ts +++ b/src/tools/rca-agent-utils/constants.ts @@ -2,36 +2,36 @@ import { z } from "zod"; import { TestStatus } from "./types.js"; export const FETCH_RCA_PARAMS = { - testId: z - .array(z.string()) - .max(3) - .describe( - "Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed." - ), + testId: z + .array(z.string()) + .max(3) + .describe( + "Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed.", + ), }; export const GET_BUILD_ID_PARAMS = { - projectName: z - .string() - .describe( - "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions" - ), - buildName: z - .string() - .describe( - "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions" - ), + projectName: z + .string() + .describe( + "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", + ), + buildName: z + .string() + .describe( + "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", + ), }; export const LIST_TEST_IDS_PARAMS = { - buildId: z - .string() - .describe( - "The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name" - ), - status: z - .nativeEnum(TestStatus) - .describe( - "Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status" - ), + buildId: z + .string() + .describe( + "The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name", + ), + status: z + .nativeEnum(TestStatus) + .describe( + "Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status", + ), }; diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 3b723b85..f0cc93d1 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -1,5 +1,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, LIST_TEST_IDS_PARAMS } from "./rca-agent-utils/constants.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import logger from "../logger.js"; import { BrowserStackConfig } from "../lib/types.js"; @@ -9,6 +8,12 @@ import { getTestIds } from "./rca-agent-utils/get-failed-test-id.js"; import { getRCAData } from "./rca-agent-utils/rca-data.js"; import { formatRCAData } from "./rca-agent-utils/format-rca.js"; import { TestStatus } from "./rca-agent-utils/types.js"; +import { handleMCPError } from "../lib/utils.js"; +import { + FETCH_RCA_PARAMS, + GET_BUILD_ID_PARAMS, + LIST_TEST_IDS_PARAMS, +} from "./rca-agent-utils/constants.js"; // Tool function to fetch build ID export async function getBuildIdTool( @@ -117,7 +122,6 @@ export async function listTestIdsTool( } } -// Registers the fetchRCA tool with the MCP server export default function addRCATools( server: McpServer, config: BrowserStackConfig, @@ -132,16 +136,7 @@ export default function addRCATools( try { return await fetchRCADataTool(args, config); } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text", - text: `Error during fetching RCA data: ${errorMessage}`, - }, - ], - }; + return handleMCPError("fetchRCA", server, config, error); } }, ); @@ -154,16 +149,7 @@ export default function addRCATools( try { return await getBuildIdTool(args, config); } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text", - text: `Error during fetching build ID: ${errorMessage}`, - }, - ], - }; + return handleMCPError("getBuildId", server, config, error); } }, ); @@ -176,16 +162,7 @@ export default function addRCATools( try { return await listTestIdsTool(args, config); } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return { - content: [ - { - type: "text", - text: `Error during listing test IDs: ${errorMessage}`, - }, - ], - }; + return handleMCPError("listTestIds", server, config, error); } }, ); diff --git a/src/tools/review-agent.ts b/src/tools/review-agent.ts index 4b8e4d2b..cf87fef5 100644 --- a/src/tools/review-agent.ts +++ b/src/tools/review-agent.ts @@ -1,4 +1,3 @@ -import logger from "../logger.js"; import { BrowserStackConfig } from "../lib/types.js"; import { getBrowserStackAuth } from "../lib/get-auth.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; @@ -57,9 +56,6 @@ export async function fetchPercyChanges( orgId, browserIds, ); - logger.info( - `Fetched ${snapshotIds.length} snapshot IDs for build: ${lastBuildId} as ${snapshotIds.join(", ")}`, - ); // Fetch all diffs concurrently and flatten results const allDiffs = await getPercySnapshotDiffs(snapshotIds, percyToken); diff --git a/src/tools/run-percy-scan.ts b/src/tools/run-percy-scan.ts index fac071a0..32f10c11 100644 --- a/src/tools/run-percy-scan.ts +++ b/src/tools/run-percy-scan.ts @@ -29,11 +29,11 @@ export async function runPercyScan( steps.push( `Attempt to infer the project's test command from context (high confidence commands first): -- Java → mvn test -- Python → pytest -- Node.js → npm test or yarn test -- Cypress → cypress run -or from package.json scripts`, + - Java → mvn test + - Python → pytest + - Node.js → npm test or yarn test + - Cypress → cypress run + or from package.json scripts`, `Wrap the inferred command with Percy:\nnpx percy exec -- `, `If the test command cannot be inferred confidently, ask the user directly for the correct test command.`, ); diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 754d30d0..4f2bd8d2 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -1,10 +1,10 @@ import { z } from "zod"; +import { PercyIntegrationTypeEnum } from "./types.js"; import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum, } from "./types.js"; -import { PercyIntegrationTypeEnum } from "./types.js"; export const SetUpPercyParamsShape = { projectName: z.string().describe("A unique name for your Percy project."), @@ -16,12 +16,12 @@ export const SetUpPercyParamsShape = { integrationType: z .nativeEnum(PercyIntegrationTypeEnum) .describe( - "Specifies whether to integrate with Percy Web or Percy Automate. If not explicitly provided, prompt the user to select the desired integration type.", + "Specify the Percy integration type: web (Percy Web) or automate (Percy Automate). If not provided, always prompt the user with: 'Please specify the Percy integration type.' Do not proceed without an explicit selection. Never use a default.", ), folderPaths: z .array(z.string()) .describe( - "An array of folder paths to include in which Percy will be integrated. If not provided, strictly inspect the code and return the folders which contain UI test cases.", + "An array of absolute folder paths containing UI test files. If not provided, analyze codebase for UI test folders by scanning for test patterns which contain UI test cases as per framework. Return empty array if none found.", ), }; diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index e744580e..01a119f0 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -1,11 +1,3 @@ -import { - SetUpPercySchema, - RunTestsOnBrowserStackSchema, -} from "./common/schema.js"; -import { - getBootstrapFailedMessage, - percyUnsupportedResult, -} from "./common/utils.js"; import { formatToolResult } from "./common/utils.js"; import { BrowserStackConfig } from "../../lib/types.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; @@ -17,6 +9,14 @@ import { runPercyAutomateOnly } from "./percy-automate/handler.js"; import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js"; import { checkPercyIntegrationSupport } from "./common/utils.js"; +import { + SetUpPercySchema, + RunTestsOnBrowserStackSchema, +} from "./common/schema.js"; +import { + getBootstrapFailedMessage, + percyUnsupportedResult, +} from "./common/utils.js"; export async function runTestsOnBrowserStackHandler( rawInput: unknown, From 58c50acadaa40fe7b25929b0e156f23318131dd3 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 22:20:16 +0530 Subject: [PATCH 40/84] chore: update version to 1.2.4 and add mcpName field --- package-lock.json | 4 ++-- package.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4120c7e1..3411e58e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.3", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@browserstack/mcp-server", - "version": "1.2.3", + "version": "1.2.4", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.4", diff --git a/package.json b/package.json index 34c9a474..339558c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.3", + "version": "1.2.4", "description": "BrowserStack's Official MCP Server", + "mcpName": "io.github.browserstack/mcp-server", "main": "dist/index.js", "repository": { "type": "git", From a23d6c3bfc95d001af17224a394a081b1d818d79 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 22:26:40 +0530 Subject: [PATCH 41/84] instrumentation ++ --- src/tools/bstack-sdk.ts | 9 ++++++++- src/tools/build-insights.ts | 2 ++ src/tools/rca-agent.ts | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index 84a84329..cff0c6a4 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -3,6 +3,8 @@ import { BrowserStackConfig } from "../lib/types.js"; import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js"; import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js"; import { RUN_ON_BROWSERSTACK_DESCRIPTION } from "./sdk-utils/common/constants.js"; +import { handleMCPError } from "../lib/utils.js"; +import { trackMCP } from "../lib/instrumentation.js"; export function registerRunBrowserStackTestsTool( server: McpServer, @@ -15,7 +17,12 @@ export function registerRunBrowserStackTestsTool( RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, async (args) => { - return runTestsOnBrowserStackHandler(args, config); + try { + trackMCP("runTestsOnBrowserStack", server.server.getClientVersion()!, config); + return await runTestsOnBrowserStackHandler(args, config); + } catch (error) { + return handleMCPError("runTestsOnBrowserStack", server, config, error); + } }, ); diff --git a/src/tools/build-insights.ts b/src/tools/build-insights.ts index 60f685fe..cbad3201 100644 --- a/src/tools/build-insights.ts +++ b/src/tools/build-insights.ts @@ -4,6 +4,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import logger from "../logger.js"; import { BrowserStackConfig } from "../lib/types.js"; import { fetchFromBrowserStackAPI, handleMCPError } from "../lib/utils.js"; +import { trackMCP } from "../lib/instrumentation.js"; // Tool function that fetches build insights from two APIs export async function fetchBuildInsightsTool( @@ -78,6 +79,7 @@ export default function addBuildInsightsTools( }, async (args) => { try { + trackMCP("fetchBuildInsights", server.server.getClientVersion()!, config); return await fetchBuildInsightsTool(args, config); } catch (error) { return handleMCPError("fetchBuildInsights", server, config, error); diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index f0cc93d1..888b414c 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -9,6 +9,7 @@ import { getRCAData } from "./rca-agent-utils/rca-data.js"; import { formatRCAData } from "./rca-agent-utils/format-rca.js"; import { TestStatus } from "./rca-agent-utils/types.js"; import { handleMCPError } from "../lib/utils.js"; +import { trackMCP } from "../index.js"; import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, @@ -134,6 +135,7 @@ export default function addRCATools( FETCH_RCA_PARAMS, async (args) => { try { + trackMCP("fetchRCA", server.server.getClientVersion()!, config); return await fetchRCADataTool(args, config); } catch (error) { return handleMCPError("fetchRCA", server, config, error); @@ -147,6 +149,7 @@ export default function addRCATools( GET_BUILD_ID_PARAMS, async (args) => { try { + trackMCP("getBuildId", server.server.getClientVersion()!, config); return await getBuildIdTool(args, config); } catch (error) { return handleMCPError("getBuildId", server, config, error); @@ -160,6 +163,7 @@ export default function addRCATools( LIST_TEST_IDS_PARAMS, async (args) => { try { + trackMCP("listTestIds", server.server.getClientVersion()!, config); return await listTestIdsTool(args, config); } catch (error) { return handleMCPError("listTestIds", server, config, error); From a1c2f2681c054324bff9cbc85fa91f45e7b967a5 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 10 Sep 2025 23:58:39 +0530 Subject: [PATCH 42/84] linting ++ --- src/tools/bstack-sdk.ts | 6 +++++- src/tools/build-insights.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index cff0c6a4..a7de7819 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -18,7 +18,11 @@ export function registerRunBrowserStackTestsTool( RunTestsOnBrowserStackParamsShape, async (args) => { try { - trackMCP("runTestsOnBrowserStack", server.server.getClientVersion()!, config); + trackMCP( + "runTestsOnBrowserStack", + server.server.getClientVersion()!, + config, + ); return await runTestsOnBrowserStackHandler(args, config); } catch (error) { return handleMCPError("runTestsOnBrowserStack", server, config, error); diff --git a/src/tools/build-insights.ts b/src/tools/build-insights.ts index cbad3201..ed6e3d74 100644 --- a/src/tools/build-insights.ts +++ b/src/tools/build-insights.ts @@ -79,7 +79,11 @@ export default function addBuildInsightsTools( }, async (args) => { try { - trackMCP("fetchBuildInsights", server.server.getClientVersion()!, config); + trackMCP( + "fetchBuildInsights", + server.server.getClientVersion()!, + config, + ); return await fetchBuildInsightsTool(args, config); } catch (error) { return handleMCPError("fetchBuildInsights", server, config, error); From b67bea943e3353ed6a7a2d76f8f1bf929cef9a5d Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 11 Sep 2025 02:27:39 +0530 Subject: [PATCH 43/84] refactor: update params and improve descriptions --- src/tools/rca-agent-utils/constants.ts | 8 ++--- .../rca-agent-utils/get-failed-test-id.ts | 30 ++++++++++++------- src/tools/rca-agent-utils/types.ts | 5 ++++ src/tools/rca-agent.ts | 15 +++++----- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/tools/rca-agent-utils/constants.ts b/src/tools/rca-agent-utils/constants.ts index f1ca985e..c7a69c0f 100644 --- a/src/tools/rca-agent-utils/constants.ts +++ b/src/tools/rca-agent-utils/constants.ts @@ -11,15 +11,15 @@ export const FETCH_RCA_PARAMS = { }; export const GET_BUILD_ID_PARAMS = { - projectName: z + browserStackProjectName: z .string() .describe( - "The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", + "The BrowserStack project name used during test run creation. Action: First, check browserstack.yml or any equivalent project configuration files. If the project name is found, extract and return it. If it is not found or if there is any uncertainty, immediately prompt the user to provide the value. Do not infer, guess, or assume a default.", ), - buildName: z + browserStackBuildName: z .string() .describe( - "The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions", + "The BrowserStack build name used during test run creation. Action: First, check browserstack.yml or any equivalent project configuration files. If the build name is found, extract and return it. If it is not found or if there is any uncertainty, immediately prompt the user to provide the value. Do not infer, guess, or assume a default.", ), }; diff --git a/src/tools/rca-agent-utils/get-failed-test-id.ts b/src/tools/rca-agent-utils/get-failed-test-id.ts index 7e8e6fb2..a474b2b6 100644 --- a/src/tools/rca-agent-utils/get-failed-test-id.ts +++ b/src/tools/rca-agent-utils/get-failed-test-id.ts @@ -36,22 +36,25 @@ export async function getTestIds( // Extract failed IDs from current page if (data.hierarchy && data.hierarchy.length > 0) { - const currentFailedTests = extractFailedTestIds(data.hierarchy); + const currentFailedTests = extractFailedTestIds(data.hierarchy, status); allFailedTests = allFailedTests.concat(currentFailedTests); } // Check for pagination termination conditions - if (!data.pagination?.has_next || !data.pagination.next_page) { + if ( + !data.pagination?.has_next || + !data.pagination.next_page || + requestNumber >= 5 + ) { break; } - // Safety limit to prevent runaway requests - if (requestNumber >= 5) { - break; - } + const params: Record = { + next_page: data.pagination.next_page, + }; + if (status) params.test_statuses = status; - // Prepare next request - url = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`; + url = `${baseUrl}?${new URLSearchParams(params).toString()}`; } // Return unique failed test IDs @@ -63,11 +66,14 @@ export async function getTestIds( } // Recursive function to extract failed test IDs from hierarchy -function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { +function extractFailedTestIds( + hierarchy: TestDetails[], + status?: TestStatus, +): FailedTestInfo[] { let failedTests: FailedTestInfo[] = []; for (const node of hierarchy) { - if (node.details?.status === "failed" && node.details?.run_count) { + if (node.details?.status === status && node.details?.run_count) { if (node.details?.observability_url) { const idMatch = node.details.observability_url.match(/details=(\d+)/); if (idMatch) { @@ -80,7 +86,9 @@ function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] { } if (node.children && node.children.length > 0) { - failedTests = failedTests.concat(extractFailedTestIds(node.children)); + failedTests = failedTests.concat( + extractFailedTestIds(node.children, status), + ); } } diff --git a/src/tools/rca-agent-utils/types.ts b/src/tools/rca-agent-utils/types.ts index c03fbdfa..b2795dd7 100644 --- a/src/tools/rca-agent-utils/types.ts +++ b/src/tools/rca-agent-utils/types.ts @@ -48,3 +48,8 @@ export interface RCATestCase { export interface RCAResponse { testCases: RCATestCase[]; } + +export interface BuildIdArgs { + browserStackProjectName: string; + browserStackBuildName: string; +} diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 888b414c..4b341e43 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -10,6 +10,7 @@ import { formatRCAData } from "./rca-agent-utils/format-rca.js"; import { TestStatus } from "./rca-agent-utils/types.js"; import { handleMCPError } from "../lib/utils.js"; import { trackMCP } from "../index.js"; +import { BuildIdArgs } from "./rca-agent-utils/types.js"; import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, @@ -18,22 +19,22 @@ import { // Tool function to fetch build ID export async function getBuildIdTool( - args: { - projectName: string; - buildName: string; - }, + args: BuildIdArgs, config: BrowserStackConfig, ): Promise { try { - const { projectName, buildName } = args; + const { browserStackProjectName, browserStackBuildName } = args; + const authString = getBrowserStackAuth(config); const [username, accessKey] = authString.split(":"); + const buildId = await getBuildId( - projectName, - buildName, + browserStackProjectName, + browserStackBuildName, username, accessKey, ); + return { content: [ { From 9f7d1a6bce62a6a95d7e385e2907c2875055edd8 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Thu, 11 Sep 2025 14:27:25 +0530 Subject: [PATCH 44/84] Updated Remote MCP instructions on Readme page --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 497b585d..bf89c550 100644 --- a/README.md +++ b/README.md @@ -470,9 +470,24 @@ As of now we support 20 tools. ```text Upload PRD from /Users/xyz/Desktop/login-flow.pdf and use BrowserStack AI to generate test cases ``` -## Remote MCP Setup +## 🚀 Remote MCP Server - - VSCode (Copilot - Agent Mode): `.vscode/mcp.json`: +Remote MCP comes with all the functionalities of an MCP server without the hassles of complex setup or local installation. + +### Key benefits: + +- ✅ Works seamlessly in enterprise networks without worrying about firewalls or binaries. + +- ✅ Secure OAuth integration – no password sharing or manual credential handling. + +### Limitations: + +- ❌ No Local Testing support (cannot test apps behind VPNs, firewalls, or localhost). If you have to do Local Testing, you would have to use a BrowserStack Local MCP server. +- ❌ Latency can be slightly higher, but nothing considerable — you generally won’t notice it in normal use. + +### Installation Steps: + + - On VSCode (Copilot - Agent Mode): `.vscode/mcp.json`: - Locate or Create the Configuration File: - In the root directory of your project, look for a folder named .vscode. This folder is usually hidden so you will need to find it as mentioned in the expand. From 31ca77f521d751de7360fcd03877f0ab3b0f8435 Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Thu, 11 Sep 2025 14:35:06 +0530 Subject: [PATCH 45/84] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf89c550..68463349 100644 --- a/README.md +++ b/README.md @@ -476,7 +476,7 @@ Remote MCP comes with all the functionalities of an MCP server without the hassl ### Key benefits: -- ✅ Works seamlessly in enterprise networks without worrying about firewalls or binaries. +- ✅ Works seamlessly in enterprise networks without worrying about firewalls or binaries or where local installation is not allowed. - ✅ Secure OAuth integration – no password sharing or manual credential handling. From e8a653f49e797964fdbdabc5450878c856502c42 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 11 Sep 2025 21:28:58 +0530 Subject: [PATCH 46/84] refactor: enhance error handling in validateSupportforAppAutomate --- src/tools/appautomate-utils/appium-sdk/types.ts | 11 ++--------- src/tools/appautomate-utils/appium-sdk/utils.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/types.ts b/src/tools/appautomate-utils/appium-sdk/types.ts index e17f0da3..d231a2a8 100644 --- a/src/tools/appautomate-utils/appium-sdk/types.ts +++ b/src/tools/appautomate-utils/appium-sdk/types.ts @@ -60,15 +60,8 @@ export interface AppSDKInstruction { export const SUPPORTED_CONFIGURATIONS = { appium: { ruby: ["cucumberRuby"], - java: [ - "junit5", - "junit4", - "testng", - "cucumberTestng", - "selenide", - "jbehave", - ], - csharp: ["nunit", "xunit", "mstest", "specflow", "reqnroll"], + java: [], + csharp: [], 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 index 9c042ba5..3c532da8 100644 --- a/src/tools/appautomate-utils/appium-sdk/utils.ts +++ b/src/tools/appautomate-utils/appium-sdk/utils.ts @@ -96,7 +96,15 @@ export function validateSupportforAppAutomate( ); } - const testingFrameworks = SUPPORTED_CONFIGURATIONS[framework][language]; + const testingFrameworks = SUPPORTED_CONFIGURATIONS[framework][ + language + ] as string[]; + + if (testingFrameworks.length === 0) { + throw new Error( + `No testing frameworks are supported for language '${language}' and framework '${framework}'.`, + ); + } if (!testingFrameworks.includes(testingFramework)) { throw new Error( `Unsupported testing framework '${testingFramework}' for language '${language}' and framework '${framework}'. Supported testing frameworks: ${testingFrameworks.join(", ")}`, From 6532db2f62423eb11bca4a5d42cef5d92734a2d3 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 13:02:49 +0530 Subject: [PATCH 47/84] refactor: add logging for directory read failures in walkDir function --- src/tools/percy-snapshot-utils/detect-test-files.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/percy-snapshot-utils/detect-test-files.ts b/src/tools/percy-snapshot-utils/detect-test-files.ts index 40886d1d..999790f1 100644 --- a/src/tools/percy-snapshot-utils/detect-test-files.ts +++ b/src/tools/percy-snapshot-utils/detect-test-files.ts @@ -14,6 +14,7 @@ import { } from "../percy-snapshot-utils/constants.js"; import { DetectionConfig } from "../percy-snapshot-utils/types.js"; +import logger from "../../logger.js"; async function walkDir( dir: string, @@ -37,7 +38,7 @@ async function walkDir( } } } catch { - // ignore + logger.info("Failed to read user directory"); } return result; From 9df073217101116774a4e9104e3b6eae199e7501 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 07:35:57 +0000 Subject: [PATCH 48/84] chore(deps): bump axios from 1.8.4 to 1.12.0 Bumps [axios](https://github.com/axios/axios) from 1.8.4 to 1.12.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.8.4...v1.12.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.12.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3411e58e..85bb3a79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2576,12 +2576,13 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, From 982c4c967cb460465d005ded8f879ba6e71ef722 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 16:48:45 +0530 Subject: [PATCH 49/84] Enhance One Click MCP Setup section in README --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 68463349..5420dbde 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,18 @@ Manage, execute, debug tests, and even fix code using plain English prompts. #### Reduced context switching: Stay in flow—keep all project context in one place and trigger actions directly from your IDE or LLM. -## ⚡️ One Click MCP Setup +## ⚡️ One Click MCP Setup + +Click on the buttons below to install MCP in your respective IDE: + + + Install in VS Code + +  + + Install in VS Code + -[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor) #### Note : Ensure you are using Node version >= `18.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) - To Upgrade Node : From 82cd9a6fa882282b23c9bb715c36d0e3f438eaac Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 16:49:52 +0530 Subject: [PATCH 50/84] Add files via upload --- assets/one-click-cursor.png | Bin 0 -> 5282 bytes assets/one-click-vs-code.png | Bin 0 -> 8069 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/one-click-cursor.png create mode 100644 assets/one-click-vs-code.png diff --git a/assets/one-click-cursor.png b/assets/one-click-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..324bdc6b06c6fd4d869bc387f65ccdc13cca6a17 GIT binary patch literal 5282 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 SNZ`~QL0Mj1u13Z({Qm%Ij(CIs literal 0 HcmV?d00001 From 78970a38147bf3200879e29bd8098e63dcef644f Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 16:50:17 +0530 Subject: [PATCH 51/84] Fix installation link for MCP in README Updated installation instructions for MCP in README. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5420dbde..ba7a6128 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,13 @@ Stay in flow—keep all project context in one place and trigger actions directl ## ⚡️ One Click MCP Setup -Click on the buttons below to install MCP in your respective IDE: +Click on the buttons below to install MCP in your respective IDE: Install in VS Code - Install in VS Code + Install in Cursor #### Note : Ensure you are using Node version >= `18.0` From 2aff478e9a590ff45ddc0a1354a2f22de344f405 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 16:52:18 +0530 Subject: [PATCH 52/84] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ba7a6128..f76772ac 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Click on the buttons below to install MCP in your respective IDE: Install in VS Code +    Install in Cursor From 742a9fd7bd5c4539c924b250f96926e730510a67 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 17:18:43 +0530 Subject: [PATCH 53/84] Revise One Click MCP Setup section in README Updated installation instructions and links for MCP setup. --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f76772ac..ce357566 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,7 @@ Stay in flow—keep all project context in one place and trigger actions directl Click on the buttons below to install MCP in your respective IDE: - - Install in VS Code - -    - - Install in Cursor - +Install in VS Code   Install in Cursor #### Note : Ensure you are using Node version >= `18.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) @@ -165,8 +159,9 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and ### **One Click MCP Setup** -[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor) +Click on the buttons below to install MCP in your respective IDE: +Install in VS Code   Install in Cursor ### **Alternate ways to Setup MCP server** From 6262eabd7751ef21af05b00100ae5dd0ae330740 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 12 Sep 2025 17:30:14 +0530 Subject: [PATCH 54/84] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce357566..9bea0ff1 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Stay in flow—keep all project context in one place and trigger actions directl Click on the buttons below to install MCP in your respective IDE: -Install in VS Code   Install in Cursor +Install in VS Code   Install in Cursor #### Note : Ensure you are using Node version >= `18.0` - Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS) @@ -161,7 +161,7 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and Click on the buttons below to install MCP in your respective IDE: -Install in VS Code   Install in Cursor +Install in VS Code   Install in Cursor ### **Alternate ways to Setup MCP server** From 4e943e595edd7b9efbc49118d1ebcbc129d0abc8 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 15 Sep 2025 23:50:19 +0530 Subject: [PATCH 55/84] fix: Enhance ApiClient with URL validation --- src/lib/apiClient.ts | 104 +++++++++++++++++++++++++++++++++---- src/lib/instrumentation.ts | 9 ++-- src/lib/utils.ts | 24 +++++++++ 3 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 23cdeea9..97287b0c 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -4,12 +4,14 @@ const { HttpsProxyAgent } = httpsProxyAgentPkg; import * as https from "https"; import * as fs from "fs"; import config from "../config.js"; +import { isDataUrlPayloadTooLarge } from "../lib/utils.js"; type RequestOptions = { url: string; headers?: Record; params?: Record; body?: any; + timeout?: number; raise_error?: boolean; // default: true }; @@ -99,11 +101,53 @@ class ApiClient { return getAxiosAgent(); } + private validateUrl(url: string, options?: AxiosRequestConfig) { + try { + const parsedUrl = new URL(url); + + // Default safe limits + const maxContentLength = options?.maxContentLength ?? 20 * 1024 * 1024; // 20MB + const maxBodyLength = options?.maxBodyLength ?? 20 * 1024 * 1024; // 20MB + const maxUrlLength = 8000; // cutoff for URLs + + // Check overall URL length + if (url.length > maxUrlLength) { + throw new Error( + `URL length exceeds maxUrlLength (${maxUrlLength} chars)`, + ); + } + + if (parsedUrl.protocol === "data:") { + // Either reject completely OR check payload size + if (isDataUrlPayloadTooLarge(url, maxContentLength)) { + throw new Error("data: URI payload too large or invalid"); + } + } else if (!["http:", "https:"].includes(parsedUrl.protocol)) { + throw new Error(`Unsupported URL scheme: ${parsedUrl.protocol}`); + } + + if ( + options?.data && + Buffer.byteLength(JSON.stringify(options.data), "utf8") > maxBodyLength + ) { + throw new Error( + `Request body exceeds maxBodyLength (${maxBodyLength} bytes)`, + ); + } + } catch (error: any) { + throw new Error(`Invalid URL: ${error.message}`); + } + } + private async requestWrapper( fn: (agent: AxiosRequestConfig["httpsAgent"]) => Promise>, + url: string, + config?: AxiosRequestConfig, raise_error: boolean = true, ): Promise> { try { + this.validateUrl(url, config); + const res = await fn(this.axiosAgent); return new ApiResponse(res); } catch (error: any) { @@ -118,11 +162,19 @@ class ApiClient { url, headers, params, + timeout, raise_error = true, }: RequestOptions): Promise> { + const config: AxiosRequestConfig = { + headers, + params, + timeout, + httpsAgent: this.axiosAgent, + }; return this.requestWrapper( - (agent) => - this.instance.get(url, { headers, params, httpsAgent: agent }), + () => this.instance.get(url, config), + url, + config, raise_error, ); } @@ -131,11 +183,19 @@ class ApiClient { url, headers, body, + timeout, raise_error = true, }: RequestOptions): Promise> { + const config: AxiosRequestConfig = { + headers, + timeout, + httpsAgent: this.axiosAgent, + data: body, + }; return this.requestWrapper( - (agent) => - this.instance.post(url, body, { headers, httpsAgent: agent }), + () => this.instance.post(url, config.data, config), + url, + config, raise_error, ); } @@ -144,11 +204,19 @@ class ApiClient { url, headers, body, + timeout, raise_error = true, }: RequestOptions): Promise> { + const config: AxiosRequestConfig = { + headers, + timeout, + httpsAgent: this.axiosAgent, + data: body, + }; return this.requestWrapper( - (agent) => - this.instance.put(url, body, { headers, httpsAgent: agent }), + () => this.instance.put(url, config.data, config), + url, + config, raise_error, ); } @@ -157,11 +225,19 @@ class ApiClient { url, headers, body, + timeout, raise_error = true, }: RequestOptions): Promise> { + const config: AxiosRequestConfig = { + headers, + timeout, + httpsAgent: this.axiosAgent, + data: body, + }; return this.requestWrapper( - (agent) => - this.instance.patch(url, body, { headers, httpsAgent: agent }), + () => this.instance.patch(url, config.data, config), + url, + config, raise_error, ); } @@ -170,11 +246,19 @@ class ApiClient { url, headers, params, + timeout, raise_error = true, }: RequestOptions): Promise> { + const config: AxiosRequestConfig = { + headers, + params, + timeout, + httpsAgent: this.axiosAgent, + }; return this.requestWrapper( - (agent) => - this.instance.delete(url, { headers, params, httpsAgent: agent }), + () => this.instance.delete(url, config), + url, + config, raise_error, ); } diff --git a/src/lib/instrumentation.ts b/src/lib/instrumentation.ts index 080d0dc3..c9dd1a1c 100644 --- a/src/lib/instrumentation.ts +++ b/src/lib/instrumentation.ts @@ -3,7 +3,7 @@ import { getBrowserStackAuth } from "./get-auth.js"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const packageJson = require("../../package.json"); -import axios from "axios"; +import { apiClient } from "./apiClient.js"; import globalConfig from "../config.js"; interface MCPEventPayload { @@ -63,13 +63,16 @@ export function trackMCP( authHeader = `Basic ${Buffer.from(authString).toString("base64")}`; } - axios - .post(instrumentationEndpoint, event, { + apiClient + .post({ + url: instrumentationEndpoint, + body: event, headers: { "Content-Type": "application/json", ...(authHeader ? { Authorization: authHeader } : {}), }, timeout: 2000, + raise_error: false, }) .catch(() => {}); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bed98a73..5b8d5af6 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -89,3 +89,27 @@ export function handleMCPError( `Failed to ${readableToolName}: ${errorMessage}. Please open an issue on GitHub if the problem persists`, ); } + +export function isDataUrlPayloadTooLarge( + dataUrl: string, + maxBytes: number, +): boolean { + const commaIndex = dataUrl.indexOf(","); + if (commaIndex === -1) return true; // malformed + const meta = dataUrl.slice(0, commaIndex); + const payload = dataUrl.slice(commaIndex + 1); + + const isBase64 = /;base64$/i.test(meta); + if (!isBase64) { + try { + const decoded = decodeURIComponent(payload); + return Buffer.byteLength(decoded, "utf8") > maxBytes; + } catch { + return true; + } + } + + const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0; + const decodedBytes = Math.floor((payload.length * 3) / 4) - padding; + return decodedBytes > maxBytes; +} From 077c9e56bfac690f4e5953c492ca87ae2aeea26f Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Tue, 16 Sep 2025 13:16:16 +0530 Subject: [PATCH 56/84] Updated prompt --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68463349..8dadb894 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ As of now we support 20 tools. **Prompt example** ```text - Take a screenshot of my app on Google Pixel 6 with Android 14 while testing on App Automate. App file path: /Users/xyz/app-debug.apk + Take a screenshot of my app on Google Pixel 6 with Android 12 while testing on App Automate. App file path: /Users/xyz/app-debug.apk ``` 15. `runAppTestsOnBrowserStack` — Run automated mobile tests (Espresso/XCUITest, etc.) on real devices. From 84c7f9ee8099bbb9e67fff1c2c3ddd422b66a87c Mon Sep 17 00:00:00 2001 From: pushkarbw Date: Tue, 16 Sep 2025 13:17:49 +0530 Subject: [PATCH 57/84] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05e9003b..e84d6b2a 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,7 @@ As of now we support 20 tools. **Prompt example** ```text - Run Espresso tests from /tests/checkout.zip on Galaxy S21 and Pixel 6 with Android 14. App path is /apps/beta-release.apk under project 'Checkout Flow' + Run Espresso tests from /tests/checkout.zip on Galaxy S21 and Pixel 6 with Android 12. App path is /apps/beta-release.apk under project 'Checkout Flow' ``` --- From bca14ea953304a29d7677f8ebf907c8927e2a3e7 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 00:20:57 +0530 Subject: [PATCH 58/84] Adding initial frameworks --- src/tools/appautomate-utils/appium-sdk/types.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tools/appautomate-utils/appium-sdk/types.ts b/src/tools/appautomate-utils/appium-sdk/types.ts index d231a2a8..eecd469e 100644 --- a/src/tools/appautomate-utils/appium-sdk/types.ts +++ b/src/tools/appautomate-utils/appium-sdk/types.ts @@ -21,6 +21,7 @@ export enum AppSDKSupportedTestingFrameworkEnum { junit4 = "junit4", selenide = "selenide", jbehave = "jbehave", + serenity = "serenity", cucumberTestng = "cucumberTestng", cucumberJunit4 = "cucumberJunit4", cucumberJunit5 = "cucumberJunit5", @@ -60,7 +61,15 @@ export interface AppSDKInstruction { export const SUPPORTED_CONFIGURATIONS = { appium: { ruby: ["cucumberRuby"], - java: [], + java: [ + "testng", + "cucumber", + "junit4", + "junit5", + "jbehave", + "selenide", + "serenity", + ], csharp: [], python: ["pytest", "robot", "behave", "lettuce"], nodejs: ["jest", "mocha", "cucumberJs", "webdriverio", "nightwatch"], From 7566b37dccc5a367669b5013380f80ae9ec3656a Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 00:50:45 +0530 Subject: [PATCH 59/84] Java Adjustments --- .../appium-sdk/languages/java.ts | 84 ++++++++++++++----- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/languages/java.ts b/src/tools/appautomate-utils/appium-sdk/languages/java.ts index ea5ad958..965f886f 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/java.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/java.ts @@ -11,15 +11,32 @@ export const MAVEN_ARCHETYPE_GROUP_ID = "com.browserstack"; export const MAVEN_ARCHETYPE_ARTIFACT_ID = "junit-archetype-integrate"; export const MAVEN_ARCHETYPE_VERSION = "1.0"; +// Version mapping for different frameworks +export const JAVA_APP_FRAMEWORK_VERSION_MAP: Record = { + testng: "1.4", + selenide: "1.4", + junit5: "1.0", + junit4: "1.0", + jbehave: "1.0", + cucumberTestng: "1.0", + cucumberJunit4: "1.0", + cucumberJunit5: "1.0", + cucumber: "1.0", + serenity: "1.0", +}; + // Framework mapping for Java Maven archetype generation for App Automate export const JAVA_APP_FRAMEWORK_MAP: Record = { - testng: "browserstack-sdk-archetype-integrate", + testng: "testng-archetype-integrate", junit5: "browserstack-sdk-archetype-integrate", selenide: "selenide-archetype-integrate", jbehave: "browserstack-sdk-archetype-integrate", + junit4: "browserstack-sdk-archetype-integrate", cucumberTestng: "browserstack-sdk-archetype-integrate", cucumberJunit4: "browserstack-sdk-archetype-integrate", cucumberJunit5: "browserstack-sdk-archetype-integrate", + cucumber: "browserstack-sdk-archetype-integrate", + serenity: "browserstack-sdk-archetype-integrate", }; // Common Gradle setup instructions for App Automate (platform-independent) @@ -49,44 +66,74 @@ export function getJavaAppFrameworkForMaven(framework: string): string { return JAVA_APP_FRAMEWORK_MAP[framework] || framework; } +export function getJavaAppFrameworkVersion(framework: string): string { + return JAVA_APP_FRAMEWORK_VERSION_MAP[framework] || MAVEN_ARCHETYPE_VERSION; +} + function getMavenCommandForWindows( framework: string, mavenFramework: string, + version: string, username: string, accessKey: string, + appPath?: string, ): string { - return ( + let command = ( `mvn archetype:generate -B ` + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + `-DarchetypeArtifactId="${mavenFramework}" ` + - `-DarchetypeVersion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DarchetypeVersion="${version}" ` + `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + - `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + - `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DartifactId="${mavenFramework}" ` + + `-Dversion="${version}" ` + `-DBROWSERSTACK_USERNAME="${username}" ` + - `-DBROWSERSTACK_ACCESS_KEY="${accessKey}" ` + - `-DBROWSERSTACK_FRAMEWORK="${framework}"` + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}"` ); + + // Add framework parameter for browserstack-sdk-archetype-integrate + if (mavenFramework === "browserstack-sdk-archetype-integrate") { + command += ` -DBROWSERSTACK_FRAMEWORK="${framework}"`; + } + + // Add app path if provided + if (appPath) { + command += ` -DBROWSERSTACK_APP="${appPath}"`; + } + + return command; } function getMavenCommandForUnix( framework: string, mavenFramework: string, + version: string, username: string, accessKey: string, + appPath?: string, ): string { - return ( + let command = ( `mvn archetype:generate -B ` + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + `-DarchetypeArtifactId="${mavenFramework}" ` + - `-DarchetypeVersion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DarchetypeVersion="${version}" ` + `-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + - `-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` + - `-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` + + `-DartifactId="${mavenFramework}" ` + + `-Dversion="${version}" ` + `-DBROWSERSTACK_USERNAME="${username}" ` + - `-DBROWSERSTACK_ACCESS_KEY="${accessKey}" ` + - `-DBROWSERSTACK_FRAMEWORK="${framework}"` + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}"` ); + + // Add framework parameter for browserstack-sdk-archetype-integrate + if (mavenFramework === "browserstack-sdk-archetype-integrate") { + command += ` -DBROWSERSTACK_FRAMEWORK="${framework}"`; + } + + // Add app path if provided + if (appPath) { + command += ` -DBROWSERSTACK_APP="${appPath}"`; + } + + return command; } export function getJavaSDKCommand( @@ -98,6 +145,7 @@ export function getJavaSDKCommand( const { isWindows = false, getPlatformLabel } = PLATFORM_UTILS || {}; const mavenFramework = getJavaAppFrameworkForMaven(framework); + const version = getJavaAppFrameworkVersion(framework); let mavenCommand: string; @@ -105,22 +153,20 @@ export function getJavaSDKCommand( mavenCommand = getMavenCommandForWindows( framework, mavenFramework, + version, username, accessKey, + appPath, ); - if (appPath) { - mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; - } } else { mavenCommand = getMavenCommandForUnix( framework, mavenFramework, + version, username, accessKey, + appPath, ); - if (appPath) { - mavenCommand += ` -DBROWSERSTACK_APP="${appPath}"`; - } } const envStep = createEnvStep( From 3a61131953ebf821cdf5edb8707a128630ac9976 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 13:48:44 +0530 Subject: [PATCH 60/84] Initial device validations --- src/lib/device-cache.ts | 6 + src/tools/appautomate.ts | 5 + src/tools/sdk-utils/bstack/sdkHandler.ts | 17 +- .../sdk-utils/common/device-validator.ts | 466 ++++++++++++++++++ src/tools/sdk-utils/common/schema.ts | 71 ++- src/tools/sdk-utils/common/utils.ts | 4 +- src/tools/sdk-utils/handler.ts | 18 +- src/tools/sdk-utils/percy-bstack/handler.ts | 5 +- 8 files changed, 572 insertions(+), 20 deletions(-) create mode 100644 src/tools/sdk-utils/common/device-validator.ts diff --git a/src/lib/device-cache.ts b/src/lib/device-cache.ts index 65ce7d96..5e46f21e 100644 --- a/src/lib/device-cache.ts +++ b/src/lib/device-cache.ts @@ -13,6 +13,8 @@ export enum BrowserStackProducts { LIVE = "live", APP_LIVE = "app_live", APP_AUTOMATE = "app_automate", + SELENIUM_APP_AUTOMATE = "selenium_app_automate", + PLAYWRIGHT_APP_AUTOMATE = "playwright_app_automate", } const URLS: Record = { @@ -22,6 +24,10 @@ const URLS: Record = { "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_APP_AUTOMATE]: + "https://www.browserstack.com/list-of-browsers-and-platforms/selenium_app_automate.json", + [BrowserStackProducts.PLAYWRIGHT_APP_AUTOMATE]: + "https://www.browserstack.com/list-of-browsers-and-platforms/playwright.json", }; /** diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index d13f8a46..47898175 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -25,6 +25,8 @@ import { BrowserStackProducts, } from "../lib/device-cache.js"; +import { validateAppAutomateDevices } from "./sdk-utils/common/device-validator.js"; + import { findMatchingDevice, getDeviceVersions, @@ -193,6 +195,9 @@ async function runAppTestsOnBrowserStack( ); } + // Validate devices against real BrowserStack device data + await validateAppAutomateDevices(args.devices); + switch (args.detectedAutomationFramework) { case AppTestPlatform.ESPRESSO: { try { diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 23957d7b..500841e1 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -6,21 +6,32 @@ import { getSDKPrefixCommand } from "./commands.js"; import { generateBrowserStackYMLInstructions } from "./configUtils.js"; import { getInstructionsForProjectConfiguration } from "../common/instructionUtils.js"; import { BrowserStackConfig } from "../../../lib/types.js"; +import { validateDevices } from "../common/device-validator.js"; import { SDKSupportedBrowserAutomationFramework, SDKSupportedTestingFramework, SDKSupportedLanguage, } from "../common/types.js"; -export function runBstackSDKOnly( +export async function runBstackSDKOnly( input: RunTestsOnBrowserStackInput, config: BrowserStackConfig, isPercyAutomate = false, -): RunTestsInstructionResult { +): Promise { const steps: RunTestsStep[] = []; 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; + + await validateDevices( + tupleTargets || [], + input.detectedBrowserAutomationFramework, + ); + // Handle frameworks with unique setup instructions that don't use browserstack.yml if ( input.detectedBrowserAutomationFramework === "cypress" || @@ -76,7 +87,7 @@ export function runBstackSDKOnly( } const ymlInstructions = generateBrowserStackYMLInstructions( - input.desiredPlatforms as string[], + (tupleTargets || []).map((tuple) => tuple.join(" ")), false, input.projectName, ); diff --git a/src/tools/sdk-utils/common/device-validator.ts b/src/tools/sdk-utils/common/device-validator.ts new file mode 100644 index 00000000..a5155fd4 --- /dev/null +++ b/src/tools/sdk-utils/common/device-validator.ts @@ -0,0 +1,466 @@ +import { + getDevicesAndBrowsers, + BrowserStackProducts, +} from "../../../lib/device-cache.js"; +import { resolveVersion } from "../../../lib/version-resolver.js"; +import { customFuzzySearch } from "../../../lib/fuzzy.js"; +import { SDKSupportedBrowserAutomationFrameworkEnum } from "./types.js"; + +export interface ValidatedEnvironment { + platform: string; + osVersion: string; + browser?: string; + browserVersion?: string; + deviceName?: string; + notes?: string; +} + +// Centralized defaults +const DEFAULTS = { + windows: { browser: "chrome" }, + macos: { browser: "safari" }, + android: { device: "Samsung Galaxy S24", browser: "chrome" }, + ios: { device: "iPhone 15", browser: "safari" }, +} as const; + +/** + * Validates device tuples against real BrowserStack device data + * This prevents hallucination by checking against actual available devices + * Throws errors directly if validation fails + */ +export async function validateDevices( + devices: Array>, + framework?: string, +): Promise { + const validatedEnvironments: ValidatedEnvironment[] = []; + + if (!devices || devices.length === 0) { + // Default fallback - no validation needed + return [ + { + platform: "windows", + osVersion: "latest", + browser: "chrome", + browserVersion: "latest", + }, + ]; + } + + try { + // Determine what data we need to fetch + const needsDesktop = devices.some((env) => + ["windows", "mac", "macos"].includes((env[0] || "").toLowerCase()), + ); + const needsMobile = devices.some((env) => + ["android", "ios"].includes((env[0] || "").toLowerCase()), + ); + + // Fetch only needed data + let liveData: any = null; + let appAutomateData: any = null; + + if (needsDesktop) { + liveData = await getDevicesAndBrowsers(BrowserStackProducts.LIVE); + } + + if (needsMobile) { + // Use framework-specific endpoint for app automate data + if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) { + appAutomateData = await getDevicesAndBrowsers( + BrowserStackProducts.PLAYWRIGHT_APP_AUTOMATE, + ); + } else if ( + framework === SDKSupportedBrowserAutomationFrameworkEnum.selenium + ) { + appAutomateData = await getDevicesAndBrowsers( + BrowserStackProducts.SELENIUM_APP_AUTOMATE, + ); + } else { + appAutomateData = await getDevicesAndBrowsers( + BrowserStackProducts.APP_AUTOMATE, + ); + } + } + + for (const env of devices) { + const discriminator = (env[0] || "").toLowerCase(); + let validatedEnv: ValidatedEnvironment; + + if (discriminator === "windows") { + const allEntries = liveData.desktop.flatMap((plat: any) => + plat.browsers.map((b: any) => ({ + os: plat.os, + os_version: plat.os_version, + browser: b.browser, + browser_version: b.browser_version, + })), + ); + validatedEnv = await validateDesktopEnvironment( + env, + allEntries, + "windows", + DEFAULTS.windows.browser, + ); + } else if (discriminator === "mac" || discriminator === "macos") { + const allEntries = liveData.desktop.flatMap((plat: any) => + plat.browsers.map((b: any) => ({ + os: plat.os, + os_version: plat.os_version, + browser: b.browser, + browser_version: b.browser_version, + })), + ); + validatedEnv = await validateDesktopEnvironment( + env, + allEntries, + "macos", + DEFAULTS.macos.browser, + ); + } else if (discriminator === "android") { + const allEntries = appAutomateData.mobile.flatMap((grp: any) => + grp.devices.map((d: any) => ({ + os: grp.os, + os_version: d.os_version, + display_name: d.display_name, + browsers: d.browsers || [ + { browser: d.browser, display_name: d.browser }, + ], + })), + ); + validatedEnv = await validateMobileEnvironment( + env, + allEntries, + "android", + DEFAULTS.android.device, + DEFAULTS.android.browser, + ); + } else if (discriminator === "ios") { + const allEntries = appAutomateData.mobile.flatMap((grp: any) => + grp.devices.map((d: any) => ({ + os: grp.os, + os_version: d.os_version, + display_name: d.display_name, + browsers: d.browsers || [ + { browser: d.browser, display_name: d.browser }, + ], + })), + ); + validatedEnv = await validateMobileEnvironment( + env, + allEntries, + "ios", + DEFAULTS.ios.device, + DEFAULTS.ios.browser, + ); + } else { + throw new Error(`Unsupported platform: ${discriminator}`); + } + + validatedEnvironments.push(validatedEnv); + } + } catch (error) { + throw new Error( + `Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + return validatedEnvironments; +} + +/** + * Validates mobile device strings for App Automate against real BrowserStack device data + * Device strings should be in format: "Device Name-OS Version" (e.g., "Samsung Galaxy S20-10.0") + * This prevents hallucination by checking against actual available devices + * Throws errors directly if validation fails + */ +export async function validateAppAutomateDevices( + deviceStrings: string[], +): Promise { + const validatedDevices: ValidatedEnvironment[] = []; + + if (!deviceStrings || deviceStrings.length === 0) { + // Default fallback - no validation needed + return [ + { + platform: "android", + osVersion: "latest", + deviceName: "Samsung Galaxy S24", + }, + ]; + } + + try { + // Fetch app automate device data + const appAutomateData = await getDevicesAndBrowsers( + BrowserStackProducts.APP_AUTOMATE, + ); + + for (const deviceString of deviceStrings) { + // Parse device string in format "Device Name-OS Version" + const parts = deviceString.split("-"); + if (parts.length < 2) { + throw new Error( + `Invalid device format: "${deviceString}". Expected format: "Device Name-OS Version" (e.g., "Samsung Galaxy S20-10.0")`, + ); + } + + const deviceName = parts.slice(0, -1).join("-"); // Handle device names with hyphens + const osVersion = parts[parts.length - 1]; + + // Find matching device in the data + let validatedDevice: ValidatedEnvironment | null = null; + + for (const platformGroup of appAutomateData.mobile) { + const platformDevices = platformGroup.devices; + + // Find exact device name match (case-insensitive) + const exactMatch = platformDevices.find( + (d: any) => d.display_name.toLowerCase() === deviceName.toLowerCase(), + ); + + if (exactMatch) { + // Check if the OS version is available for this device + const deviceVersions = platformDevices + .filter((d: any) => d.display_name === exactMatch.display_name) + .map((d: any) => d.os_version); + + const validatedOSVersion = resolveVersion(osVersion, deviceVersions); + + if (!deviceVersions.includes(validatedOSVersion)) { + throw new Error( + `OS version "${osVersion}" not available for device "${deviceName}". Available versions: ${deviceVersions.join(", ")}`, + ); + } + + validatedDevice = { + platform: platformGroup.os, + osVersion: validatedOSVersion, + deviceName: exactMatch.display_name, + }; + break; + } + } + + if (!validatedDevice) { + // If no exact match found, suggest similar devices + const allDevices = appAutomateData.mobile.flatMap((grp: any) => + grp.devices.map((d: any) => ({ + ...d, + platform: grp.os, + })), + ); + + const deviceMatches = customFuzzySearch( + allDevices, + ["display_name"], + deviceName, + 5, + ); + + const suggestions = deviceMatches + .map((m) => `${m.display_name}`) + .join(", "); + + throw new Error( + `Device "${deviceName}" not found.\nAvailable similar devices: ${suggestions}\nPlease use the exact device name with format: "Device Name-OS Version"`, + ); + } + + validatedDevices.push(validatedDevice); + } + } catch (error) { + throw new Error( + `Failed to validate devices: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + return validatedDevices; +} + +// Unified desktop validation helper +async function validateDesktopEnvironment( + env: string[], + entries: any[], + platform: "windows" | "macos", + defaultBrowser: string, +): Promise { + const [, osVersion, browser, browserVersion] = env; + + const platformEntries = entries.filter((e) => + platform === "windows" ? e.os === "Windows" : e.os === "OS X", + ); + + if (platformEntries.length === 0) { + throw new Error(`No ${platform} devices available`); + } + + const availableOSVersions = [ + ...new Set(platformEntries.map((e) => e.os_version)), + ] as string[]; + + const validatedOSVersion = + platform === "macos" + ? validateMacOSVersion(osVersion || "latest", availableOSVersions) + : resolveVersion(osVersion || "latest", availableOSVersions); + + const osFiltered = platformEntries.filter( + (e) => e.os_version === validatedOSVersion, + ); + + const availableBrowsers = [ + ...new Set(osFiltered.map((e) => e.browser)), + ] as string[]; + const validatedBrowser = validateBrowser( + browser || defaultBrowser, + availableBrowsers, + ); + + const browserFiltered = osFiltered.filter( + (e) => e.browser === validatedBrowser, + ); + + const availableBrowserVersions = [ + ...new Set(browserFiltered.map((e) => e.browser_version)), + ] as string[]; + const validatedBrowserVersion = resolveVersion( + browserVersion || "latest", + availableBrowserVersions, + ); + + return { + platform, + osVersion: validatedOSVersion, + browser: validatedBrowser, + browserVersion: validatedBrowserVersion, + }; +} + +// Unified mobile validation helper +async function validateMobileEnvironment( + env: string[], + entries: any[], + platform: "android" | "ios", + defaultDevice: string, + defaultBrowser: string, +): Promise { + const [, deviceName, osVersion, browser] = env; + + const platformEntries = entries.filter((e) => e.os === platform); + if (platformEntries.length === 0) { + throw new Error(`No ${platform} devices available`); + } + + const deviceMatches = customFuzzySearch( + platformEntries, + ["display_name"], + deviceName || defaultDevice, + 5, + ); + if (deviceMatches.length === 0) { + throw new Error(`No ${platform} devices matching "${deviceName}"`); + } + + const exactMatch = deviceMatches.find( + (m) => m.display_name.toLowerCase() === (deviceName || "").toLowerCase(), + ); + if (!exactMatch) { + const suggestions = deviceMatches.map((m) => m.display_name).join(", "); + throw new Error( + `Error Device "${deviceName}" not found for ${platform}.\nAvailable options: ${suggestions}\nPlease correct these issues and try again.`, + ); + } + + const deviceFiltered = platformEntries.filter( + (d) => d.display_name === exactMatch.display_name, + ); + + const availableOSVersions = [ + ...new Set(deviceFiltered.map((d) => d.os_version)), + ] as string[]; + const validatedOSVersion = resolveVersion( + osVersion || "latest", + availableOSVersions, + ); + + // Filter by OS version + const osFiltered = deviceFiltered.filter( + (d) => d.os_version === validatedOSVersion, + ); + + // Validate browser if provided + let validatedBrowser = browser || defaultBrowser; + if (browser && osFiltered.length > 0) { + const availableBrowsers = [ + ...new Set( + osFiltered.flatMap((d) => d.browsers?.map((b: any) => b.browser) || []), + ), + ] as string[]; + + if (availableBrowsers.length > 0) { + validatedBrowser = validateBrowser(browser, availableBrowsers); + } else { + // If no browsers available for this device/OS combination, throw error + throw new Error( + `Browser "${browser}" not available for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". No browsers found for this configuration.`, + ); + } + } + + return { + platform, + osVersion: validatedOSVersion, + deviceName: exactMatch.display_name, + browser: validatedBrowser, + }; +} + +function validateBrowser( + requestedBrowser: string, + availableBrowsers: string[], +): string { + const exactMatch = availableBrowsers.find( + (b) => b.toLowerCase() === requestedBrowser.toLowerCase(), + ); + if (exactMatch) { + return exactMatch; + } + + const fuzzyMatches = customFuzzySearch( + availableBrowsers.map((b) => ({ browser: b })), + ["browser"], + requestedBrowser, + 1, + ); + + if (fuzzyMatches.length > 0) { + return fuzzyMatches[0].browser; + } + + throw new Error( + `Browser "${requestedBrowser}" not found. Available options: ${availableBrowsers.join(", ")}`, + ); +} + +function validateMacOSVersion(requested: string, available: string[]): string { + if (requested === "latest") { + return available[available.length - 1]; + } else if (requested === "oldest") { + return available[0]; + } else { + const fuzzy = customFuzzySearch( + available.map((v) => ({ os_version: v })), + ["os_version"], + requested, + 1, + ); + const matched = fuzzy.length ? fuzzy[0].os_version : requested; + + if (available.includes(matched)) { + return matched; + } else { + throw new Error( + `macOS version "${requested}" not found. Available options: ${available.join(", ")}`, + ); + } + } +} diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 4f2bd8d2..c170cd95 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -6,6 +6,23 @@ import { SDKSupportedLanguageEnum, } from "./types.js"; +// Platform enums for better validation +export const PlatformEnum = { + WINDOWS: "windows", + MACOS: "macos", + ANDROID: "android", + IOS: "ios", +} as const; + +export const WindowsPlatformEnum = { + WINDOWS: "windows", +} as const; + +export const MacOSPlatformEnum = { + MAC: "mac", + MACOS: "macos", +} as const; + export const SetUpPercyParamsShape = { projectName: z.string().describe("A unique name for your Percy project."), detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), @@ -34,12 +51,60 @@ export const RunTestsOnBrowserStackParamsShape = { SDKSupportedBrowserAutomationFrameworkEnum, ), detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), - desiredPlatforms: z - .array(z.enum(["windows", "macos", "android", "ios"])) - .describe("An array of platforms to run tests on."), + 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'"), + ]), + ]), + ) + .max(3) + .default([]) + .describe( + "Preferred input: 1-3 tuples describing target devices.Example: [['windows', '11', 'chrome', 'latest'], ['android', 'Samsung Galaxy S24', '14', 'chrome'], ['ios', 'iPhone 15', '17', 'safari']]", + ), }; export const SetUpPercySchema = z.object(SetUpPercyParamsShape); + export const RunTestsOnBrowserStackSchema = z.object( RunTestsOnBrowserStackParamsShape, ); diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts index d21cbc9e..0bb433af 100644 --- a/src/tools/sdk-utils/common/utils.ts +++ b/src/tools/sdk-utils/common/utils.ts @@ -110,8 +110,10 @@ export function getBootstrapFailedMessage( error: unknown, context: { config: unknown; percyMode?: string; sdkVersion?: string }, ): string { + const error_message = + error instanceof Error ? error.message : "unknown error"; return `Failed to bootstrap project with BrowserStack SDK. -Error: ${error} +Error: ${error_message} Percy Mode: ${context.percyMode ?? "automate"} SDK Version: ${context.sdkVersion ?? "N/A"} Please open an issue on GitHub if the problem persists.`; diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 01a119f0..ec33e3d7 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -22,15 +22,9 @@ export async function runTestsOnBrowserStackHandler( rawInput: unknown, config: BrowserStackConfig, ): Promise { - try { - const input = RunTestsOnBrowserStackSchema.parse(rawInput); - - // Only handle BrowserStack SDK setup for functional/integration tests. - const result = runBstackSDKOnly(input, config); - return await formatToolResult(result); - } catch (error) { - throw new Error(getBootstrapFailedMessage(error, { config })); - } + const input = RunTestsOnBrowserStackSchema.parse(rawInput); + const result = await runBstackSDKOnly(input, config); + return await formatToolResult(result); } export async function setUpPercyHandler( @@ -75,7 +69,7 @@ export async function setUpPercyHandler( const percyWithBrowserstackSDKResult = runPercyWithBrowserstackSDK( { ...percyInput, - desiredPlatforms: [], + devices: [], }, config, ); @@ -113,9 +107,9 @@ export async function setUpPercyHandler( detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, detectedTestingFramework: input.detectedTestingFramework, - desiredPlatforms: [], + devices: [], }; - const sdkResult = runBstackSDKOnly(sdkInput, config, true); + const sdkResult = await runBstackSDKOnly(sdkInput, config, true); // Percy Automate instructions const percyToken = await fetchPercyToken( input.projectName, diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index e0711779..11f95956 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -107,7 +107,10 @@ export function runPercyWithBrowserstackSDK( } const ymlInstructions = generateBrowserStackYMLInstructions( - input.desiredPlatforms as string[], + // For now, feed a normalized summary string from devices for the comment + ((input as any).devices as string[][] | undefined)?.map((t) => + t.join(" "), + ) || [], true, input.projectName, ); From a68e754f0258e0d444bcfe65e726cbb1d8154d43 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 17:18:05 +0530 Subject: [PATCH 61/84] feat : Adding setup tool for pery --- src/tools/percy-sdk.ts | 55 ++++++++++++++++++++++-- src/tools/run-percy-scan.ts | 2 +- src/tools/sdk-utils/common/constants.ts | 16 ++++--- src/tools/sdk-utils/handler.ts | 46 ++++++++++++++++++++ src/tools/sdk-utils/percy-web/handler.ts | 4 +- 5 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 37d436ff..2c12e0a8 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,12 +7,16 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js"; import { updateTestsWithPercyCommands } from "./add-percy-snapshots.js"; import { approveOrDeclinePercyBuild } from "./review-agent-utils/percy-approve-reject.js"; -import { setUpPercyHandler } from "./sdk-utils/handler.js"; - +import { + setUpPercyHandler, + simulatePercyChangeHandler, +} from "./sdk-utils/handler.js"; +import { z } from "zod"; import { SETUP_PERCY_DESCRIPTION, LIST_TEST_FILES_DESCRIPTION, PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, + SIMULATE_PERCY_CHANGE_DESCRIPTION, } from "./sdk-utils/common/constants.js"; import { @@ -33,8 +37,53 @@ export function registerPercyTools( ) { const tools: Record = {}; + server.prompt( + "integrate-percy", + { + project_name: z + .string() + .describe("The name of the project to integrate with Percy"), + }, + async ({ project_name }) => { + return { + messages: [ + { + role: "assistant", + content: { + type: "text", + text: `Integrate percy in this project ${project_name} using tool percyVisualTestIntegrationAgent.`, + }, + }, + ], + }; + }, + ); + + tools.percyVisualTestIntegrationAgent = server.tool( + "percyVisualTestIntegrationAgent", + SIMULATE_PERCY_CHANGE_DESCRIPTION, + SetUpPercyParamsShape, + async (args) => { + try { + trackMCP( + "VisualTestIntegrationAgent", + server.server.getClientVersion()!, + config, + ); + return simulatePercyChangeHandler(args, config); + } catch (error) { + return handleMCPError( + "VisualTestIntegrationAgent", + server, + config, + error, + ); + } + }, + ); + tools.setupPercyVisualTesting = server.tool( - "setupPercyVisualTesting", + "expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, async (args) => { diff --git a/src/tools/run-percy-scan.ts b/src/tools/run-percy-scan.ts index 32f10c11..3db5a6fb 100644 --- a/src/tools/run-percy-scan.ts +++ b/src/tools/run-percy-scan.ts @@ -34,7 +34,7 @@ export async function runPercyScan( - Node.js → npm test or yarn test - Cypress → cypress run or from package.json scripts`, - `Wrap the inferred command with Percy:\nnpx percy exec -- `, + `Wrap the inferred command with Percy along with label: \nnpx percy exec --labels=mcp -- `, `If the test command cannot be inferred confidently, ask the user directly for the correct test command.`, ); diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index 36bfee70..03f18327 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -2,7 +2,7 @@ export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup."; export const SETUP_PERCY_DESCRIPTION = - "Set up Percy visual testing for your project. This supports both Percy Web Standalone and Percy Automate."; + "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate."; export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories."; @@ -13,8 +13,7 @@ export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = export const RUN_ON_BROWSERSTACK_DESCRIPTION = "Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use this tool for functional or integration test setup on BrowserStack only. For any visual testing or Percy integration, use the dedicated Percy setup tool. Example prompts: run this test on browserstack; set up this project for browserstack."; -export const SIMULATE_PERCY_CHANGE_DESCRIPTION = - "This tool helps new users simulate a Percy visual change by guiding them to make a visible UI modification, rerun Percy, and verify that the change is detected. Only Percy web is supported"; +export const SIMULATE_PERCY_CHANGE_DESCRIPTION = `Integrate Percy visual testing into new projects and demonstrate visual change detection through a step-by-step simulation. This tool handles initial Percy integration, setup, and creates a working demonstration for new users. Primary tool for prompts like: "Integrate percy for this project", "Integrate percy in this project {project_name}"`; export const PERCY_REPLACE_REGEX = /Invoke listTestFiles\(\) with the provided directories[\s\S]*?- DO NOT STOP until you add commands in all the files or you reach end of the files\./; @@ -35,7 +34,7 @@ Process files in STRICT sequential order using tool addPercySnapshotCommands() w export const PERCY_SIMULATE_INSTRUCTION = ` MANDATORY: Aggressive, Accurate Percy Snapshots Integration -Prerequisite: Before making any code changes in the user's repo, switch to a branch named temp-percy (run git init first if the repo is not yet initialized). +Prerequisite: Before making any code changes in the user's repository, create and switch to a new branch named browserstack-simulation. Stage all changes but do not commit them. If the repository has not been initialized yet, run git init first. You MUST follow these steps with NO EXCEPTIONS: 1. Identify 4-5 key test files in the project that are critical for visual testing. @@ -53,7 +52,7 @@ STEP 1: At this case we have added Percy snapshot commands to the test files at - Do NOT add any popup injection or visual changes yet. - Do NOT run any Percy builds at this stage. -STEP 2: Run ONE comprehensive baseline Percy build that executes ALL tests containing Percy snapshots in a SINGLE build. This creates one baseline build with all snapshots for comparison. Use a command like: npx percy exec -- python -m pytest tests/ -k 'test_name1 or test_name2 or test_name3' -v to run multiple specific tests in one build." +STEP 2: Run ONE comprehensive baseline Percy build that executes ALL tests containing Percy snapshots in a SINGLE build. This creates one baseline build with all snapshots for comparison. Use a command like: npx percy exec --label=mcp -- python -m pytest tests/ -k 'test_name1 or test_name2 or test_name3' -v to run multiple specific tests in one build." STEP 3: Modify your test to inject a visible UI change (such as a popup) IMMEDIATELY BEFORE an EXISTING snapshot command (e.g., before percy_snapshot(self.driver, "screenshot name")). - Do NOT add a new snapshot name for the popup. @@ -77,14 +76,14 @@ driver.execute_script(popup_script) percy_snapshot(self.driver, "Before Adding to Cart") # (Do NOT change the snapshot name, keep existing one) \`\`\` -STEP 4: Run a second Percy build. +STEP 4: Run a second Percy build with same label and same test command as the baseline. - The snapshot names must remain the same as in the baseline. - The visual change should now appear in the same snapshot as before. - Use the same build command you ran for the baseline. STEP 5: Compare the two Percy builds to see the detected visual difference. -STEP 6: Now ask user if they want to setup percy for full project coverage? If yes, call the "setupPercyVisualTesting" tool to enable complete coverage for the entire project. +STEP 6: Now ask user if they want to expand percy for other testcases? If yes, call the "expandPercyVisualTesting" tool to enable complete coverage for the entire project. CONSTRAINTS: - Do NOT run any builds until explicitly instructed in the steps. @@ -100,3 +99,6 @@ VALIDATION CHECKPOINTS (before proceeding to the next step): CRITICAL: Do NOT run tests separately or create multiple builds during baseline establishment. The goal is to have exactly TWO builds total: (1) baseline build with all original snapshots, (2) modified build with the same tests but visual changes injected. `; + +export const PERCY_VERIFICATION_REGEX = + /\*\*✅ Verification:\*\*\nPlease verify that you have completed all[\s\S]*?double-check each step and ensure all commands executed successfully\./s; diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 01a119f0..4ec4eb14 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -17,6 +17,12 @@ import { getBootstrapFailedMessage, percyUnsupportedResult, } from "./common/utils.js"; +import { + PERCY_SIMULATE_INSTRUCTION, + PERCY_REPLACE_REGEX, + PERCY_SIMULATION_DRIVER_INSTRUCTION, + PERCY_VERIFICATION_REGEX, +} from "./common/constants.js"; export async function runTestsOnBrowserStackHandler( rawInput: unknown, @@ -161,3 +167,43 @@ export async function setUpPercyHandler( throw new Error(getBootstrapFailedMessage(error, { config })); } } + +export async function simulatePercyChangeHandler( + rawInput: unknown, + config: BrowserStackConfig, +): Promise { + try { + let percyInstruction; + + try { + percyInstruction = await setUpPercyHandler(rawInput, config); + } catch { + throw new Error("Failed to set up Percy"); + } + + if (percyInstruction.isError) { + return percyInstruction; + } + + if (Array.isArray(percyInstruction.content)) { + percyInstruction.content = percyInstruction.content.map((item) => { + if (typeof item.text === "string") { + const updatedText = item.text + .replace(PERCY_REPLACE_REGEX, PERCY_SIMULATE_INSTRUCTION) + .replace(PERCY_VERIFICATION_REGEX, ""); + return { ...item, text: updatedText }; + } + return item; + }); + } + + percyInstruction.content?.push({ + type: "text" as const, + text: PERCY_SIMULATION_DRIVER_INSTRUCTION, + }); + + return percyInstruction; + } catch (error) { + throw new Error(getBootstrapFailedMessage(error, { config })); + } +} diff --git a/src/tools/sdk-utils/percy-web/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts index bca09687..681fa614 100644 --- a/src/tools/sdk-utils/percy-web/handler.ts +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -32,7 +32,9 @@ export function runPercyWeb( steps.push({ type: "instruction", title: "Set Percy Token in Environment", - content: `Here is percy token if required {${percyToken}}`, + content: `Set the environment variable for your project: + export PERCY_TOKEN="${percyToken}" + (For Windows: use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)`, }); steps.push({ From 167ea4323a7b3a788fa25ca1defd588188815bab Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 17:44:49 +0530 Subject: [PATCH 62/84] fix : linting ++ --- src/tools/sdk-utils/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index 03f18327..bd29767d 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -2,7 +2,7 @@ export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup."; export const SETUP_PERCY_DESCRIPTION = - "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate."; + "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}"; export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories."; From 61847daac54cfc2e43a5026dd0f9fcf1a8407b39 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 18:44:32 +0530 Subject: [PATCH 63/84] feat: introduce validations for BrowserStack devices --- .../appium-sdk/config-generator.ts | 54 +++++++++++ .../appautomate-utils/appium-sdk/handler.ts | 33 +++++-- src/tools/sdk-utils/bstack/configUtils.ts | 96 +++++++++++++++++++ src/tools/sdk-utils/bstack/sdkHandler.ts | 8 +- .../sdk-utils/common/device-validator.ts | 43 +++++++-- 5 files changed, 215 insertions(+), 19 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/config-generator.ts b/src/tools/appautomate-utils/appium-sdk/config-generator.ts index bec4f667..a4a62f4a 100644 --- a/src/tools/appautomate-utils/appium-sdk/config-generator.ts +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -5,6 +5,7 @@ import { DEFAULT_APP_PATH, createStep, } from "./index.js"; +import { ValidatedEnvironment } from "../../sdk-utils/common/device-validator.js"; export function generateAppBrowserStackYMLInstructions( platforms: string[], @@ -71,3 +72,56 @@ accessibility: false ${configContent}`, ); } + +/** + * Generate App Automate browserstack.yml from validated device configurations + */ +export function generateAppAutomateYML( + validatedEnvironments: ValidatedEnvironment[], + username: string, + accessKey: string, + appPath: string = DEFAULT_APP_PATH, + projectName: string, +): string { + // Generate platform configurations from validated environments + const platformConfigs = validatedEnvironments + .filter((env) => env.platform === "android" || env.platform === "ios") + .map((env) => { + return ` - platformName: ${env.platform} + deviceName: "${env.deviceName}" + platformVersion: "${env.osVersion}"`; + }) + .join("\n"); + + // Construct YAML content with validated data + const configContent = `\`\`\`yaml +userName: ${username} +accessKey: ${accessKey} +app: ${appPath} +platforms: +${platformConfigs} +parallelsPerPlatform: 1 +browserstackLocal: true +buildName: ${projectName}-AppAutomate-Build +projectName: ${projectName} +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 validated App Automate configuration:", + `Create or update the browserstack.yml file in your project root with your validated device configurations: + +${configContent}`, + ); +} diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index 405b203d..37705bd5 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -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 { generateAppAutomateYML } from "./config-generator.js"; + import { getAppUploadInstruction, validateSupportforAppAutomate, @@ -43,6 +45,10 @@ export async function setupAppAutomateHandler( //Validating if supported framework or not validateSupportforAppAutomate(framework, language, testingFramework); + // For app automate, we don't have individual device validation like in automate + // The platforms array already contains the desired platforms + const validatedEnvironments: any[] = []; + // Step 1: Generate SDK setup command const sdkCommand = getAppSDKPrefixCommand( language, @@ -57,13 +63,26 @@ export async function setupAppAutomateHandler( } // Step 2: Generate browserstack.yml configuration - const configInstructions = generateAppBrowserStackYMLInstructions( - platforms, - username, - accessKey, - appPath, - testingFramework, - ); + let configInstructions; + if (validatedEnvironments.length > 0) { + // Use validated environments for YML generation + configInstructions = generateAppAutomateYML( + validatedEnvironments, + username, + accessKey, + appPath, + input.project as string, + ); + } else { + // Fallback to original method + configInstructions = generateAppBrowserStackYMLInstructions( + platforms, + username, + accessKey, + appPath, + testingFramework, + ); + } if (configInstructions) { instructions.push({ content: configInstructions, type: "config" }); diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index 587abd33..2a239e19 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -1,6 +1,7 @@ /** * Utilities for generating BrowserStack configuration files. */ +import { ValidatedEnvironment } from "../common/device-validator.js"; export function generateBrowserStackYMLInstructions( desiredPlatforms: string[], @@ -70,3 +71,98 @@ Create a browserstack.yml file in the project root. The file should be in the fo \`\`\` \n`; } + +/** + * Generate browserstack.yml content from validated device configurations + */ +export function generateBrowserStackYMLFromValidatedEnvironments( + validatedEnvironments: ValidatedEnvironment[], + enablePercy: boolean = false, + projectName: string, +) { + // Generate platforms array from validated environments + const platforms = validatedEnvironments.map((env) => { + if (env.platform === "windows" || env.platform === "macos") { + // Desktop configuration + return { + os: env.platform === "windows" ? "Windows" : "OS X", + osVersion: env.osVersion, + browserName: env.browser, + browserVersion: env.browserVersion || "latest", + }; + } else { + // Mobile configuration (android/ios) + return { + deviceName: env.deviceName, + osVersion: env.osVersion, + browserName: env.browser, + }; + } + }); + + // Convert platforms to YAML format + const platformsYAML = platforms + .map((platform) => { + if (platform.deviceName) { + // Mobile platform + return ` - deviceName: "${platform.deviceName}" + osVersion: "${platform.osVersion}" + browserName: ${platform.browserName}`; + } else { + // Desktop platform + return ` - os: ${platform.os} + osVersion: "${platform.osVersion}" + browserName: ${platform.browserName} + browserVersion: ${platform.browserVersion}`; + } + }) + .join("\n"); + + let ymlContent = ` +# ====================== +# BrowserStack Reporting +# ====================== +projectName: ${projectName} +buildName: ${projectName}-Build + +# ======================================= +# Platforms (Browsers / Devices to test) +# ======================================= +# Auto-generated from validated device configurations +platforms: +${platformsYAML} + +# ======================= +# Parallels per Platform +# ======================= +parallelsPerPlatform: 1 + +# ================= +# Local Testing +# ================= +browserstackLocal: true + +# =================== +# Debugging features +# =================== +debug: true +testObservability: true`; + + if (enablePercy) { + ymlContent += ` + +# ===================== +# Percy Visual Testing +# ===================== +percy: true +percyCaptureMode: manual`; + } + + return ` +---STEP--- +Create a browserstack.yml file in the project root with your validated device configurations: + +\`\`\`yaml${ymlContent} +\`\`\` +\n`; +} diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 500841e1..8c13bf64 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -3,7 +3,7 @@ import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; import { RunTestsOnBrowserStackInput } from "../common/schema.js"; import { getBrowserStackAuth } from "../../../lib/get-auth.js"; import { getSDKPrefixCommand } from "./commands.js"; -import { generateBrowserStackYMLInstructions } from "./configUtils.js"; +import { generateBrowserStackYMLFromValidatedEnvironments } from "./configUtils.js"; import { getInstructionsForProjectConfiguration } from "../common/instructionUtils.js"; import { BrowserStackConfig } from "../../../lib/types.js"; import { validateDevices } from "../common/device-validator.js"; @@ -27,7 +27,7 @@ export async function runBstackSDKOnly( | Array> | undefined; - await validateDevices( + const validatedEnvironments = await validateDevices( tupleTargets || [], input.detectedBrowserAutomationFramework, ); @@ -86,8 +86,8 @@ export async function runBstackSDKOnly( }); } - const ymlInstructions = generateBrowserStackYMLInstructions( - (tupleTargets || []).map((tuple) => tuple.join(" ")), + const ymlInstructions = generateBrowserStackYMLFromValidatedEnvironments( + validatedEnvironments, false, input.projectName, ); diff --git a/src/tools/sdk-utils/common/device-validator.ts b/src/tools/sdk-utils/common/device-validator.ts index a5155fd4..513c523e 100644 --- a/src/tools/sdk-utils/common/device-validator.ts +++ b/src/tools/sdk-utils/common/device-validator.ts @@ -357,7 +357,12 @@ async function validateMobileEnvironment( 5, ); if (deviceMatches.length === 0) { - throw new Error(`No ${platform} devices matching "${deviceName}"`); + throw new Error( + `No ${platform} devices matching "${deviceName}". Available devices: ${platformEntries + .map((d) => d.display_name || d.device || "unknown") + .slice(0, 5) + .join(", ")}`, + ); } const exactMatch = deviceMatches.find( @@ -390,19 +395,41 @@ async function validateMobileEnvironment( // Validate browser if provided let validatedBrowser = browser || defaultBrowser; if (browser && osFiltered.length > 0) { + // Extract browsers more carefully - handle different possible structures const availableBrowsers = [ ...new Set( - osFiltered.flatMap((d) => d.browsers?.map((b: any) => b.browser) || []), + osFiltered.flatMap((d) => { + if (d.browsers && Array.isArray(d.browsers)) { + // If browsers is an array of objects with browser property + return d.browsers + .map((b: any) => { + // Use display_name for user-friendly browser names, fallback to browser field + return b.display_name || b.browser || b.browserName || b.name; + }) + .filter(Boolean); + } else if (d.browser) { + // If there's just a browser property directly + return [d.browser]; + } + // For mobile devices, provide default browsers if none found + return platform === "android" ? ["chrome"] : ["safari"]; + }), ), - ] as string[]; + ].filter(Boolean) as string[]; if (availableBrowsers.length > 0) { - validatedBrowser = validateBrowser(browser, availableBrowsers); + try { + validatedBrowser = validateBrowser(browser, availableBrowsers); + } catch (error) { + // Add more context to browser validation errors + throw new Error( + `Failed to validate browser "${browser}" for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". ${error instanceof Error ? error.message : String(error)}`, + ); + } } else { - // If no browsers available for this device/OS combination, throw error - throw new Error( - `Browser "${browser}" not available for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". No browsers found for this configuration.`, - ); + // For mobile, if no specific browsers found, just use the requested browser + // as most mobile devices support standard browsers + validatedBrowser = browser || defaultBrowser; } } From 58a2b24b09d6c30c3d8724066edd67944c34942f Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 18 Sep 2025 20:32:34 +0530 Subject: [PATCH 64/84] refactor: update device validation and configuration generation for BrowserStack --- src/lib/device-cache.ts | 8 +- .../appium-sdk/config-generator.ts | 131 +++++------- .../appautomate-utils/appium-sdk/constants.ts | 63 ++++++ .../appautomate-utils/appium-sdk/handler.ts | 44 ++-- src/tools/appautomate.ts | 3 +- src/tools/sdk-utils/bstack/configUtils.ts | 194 ++++++++---------- src/tools/sdk-utils/bstack/sdkHandler.ts | 10 +- .../sdk-utils/common/device-validator.ts | 22 +- src/tools/sdk-utils/common/schema.ts | 2 +- src/tools/sdk-utils/percy-bstack/handler.ts | 10 +- 10 files changed, 240 insertions(+), 247 deletions(-) diff --git a/src/lib/device-cache.ts b/src/lib/device-cache.ts index 5e46f21e..fc832ee0 100644 --- a/src/lib/device-cache.ts +++ b/src/lib/device-cache.ts @@ -13,8 +13,8 @@ export enum BrowserStackProducts { LIVE = "live", APP_LIVE = "app_live", APP_AUTOMATE = "app_automate", - SELENIUM_APP_AUTOMATE = "selenium_app_automate", - PLAYWRIGHT_APP_AUTOMATE = "playwright_app_automate", + SELENIUM_AUTOMATE = "selenium_automate", + PLAYWRIGHT_AUTOMATE = "playwright_automate", } const URLS: Record = { @@ -24,9 +24,9 @@ const URLS: Record = { "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_APP_AUTOMATE]: + [BrowserStackProducts.SELENIUM_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/selenium_app_automate.json", - [BrowserStackProducts.PLAYWRIGHT_APP_AUTOMATE]: + [BrowserStackProducts.PLAYWRIGHT_AUTOMATE]: "https://www.browserstack.com/list-of-browsers-and-platforms/playwright.json", }; diff --git a/src/tools/appautomate-utils/appium-sdk/config-generator.ts b/src/tools/appautomate-utils/appium-sdk/config-generator.ts index a4a62f4a..99bafe3e 100644 --- a/src/tools/appautomate-utils/appium-sdk/config-generator.ts +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -1,4 +1,3 @@ -// Configuration utilities for BrowserStack App SDK import { APP_DEVICE_CONFIGS, AppSDKSupportedTestingFrameworkEnum, @@ -8,39 +7,33 @@ import { 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} @@ -49,8 +42,8 @@ platforms: ${platformConfigs} parallelsPerPlatform: 1 browserstackLocal: true -buildName: bstack-demo -projectName: BrowserStack Sample +buildName: ${buildName} +projectName: ${projectName} debug: true networkLogs: true percy: false @@ -64,64 +57,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:"; -${configContent}`, - ); + const stepDescription = `Create or update the browserstack.yml file in your project root with the following content: + ${configContent}`; + + return createStep(stepTitle, stepDescription); } -/** - * Generate App Automate browserstack.yml from validated device configurations - */ -export function generateAppAutomateYML( - validatedEnvironments: ValidatedEnvironment[], - username: string, - accessKey: string, - appPath: string = DEFAULT_APP_PATH, - projectName: string, -): string { - // Generate platform configurations from validated environments - const platformConfigs = validatedEnvironments - .filter((env) => env.platform === "android" || env.platform === "ios") - .map((env) => { - return ` - platformName: ${env.platform} +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"); + }) + .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 ""; - // Construct YAML content with validated data - const configContent = `\`\`\`yaml -userName: ${username} -accessKey: ${accessKey} -app: ${appPath} -platforms: -${platformConfigs} -parallelsPerPlatform: 1 -browserstackLocal: true -buildName: ${projectName}-AppAutomate-Build -projectName: ${projectName} -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 validated App Automate configuration:", - `Create or update the browserstack.yml file in your project root with your validated device configurations: + return devices + .map( + (device) => ` - platformName: ${platform} + deviceName: ${device.deviceName} + platformVersion: "${device.platformVersion}"`, + ) + .join("\n"); + }) + .filter(Boolean) + .join("\n"); + } -${configContent}`, - ); + return ""; } diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index c75872dd..0d909b3b 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -49,6 +49,69 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { "The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'", ), + 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([[AppSDKSupportedPlatformEnum.android, 'Samsung Galaxy S24', 'latest']]) + .describe( + "Preferred input: 1-3 tuples describing target mobile devices. Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]", + ), + + 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."), +}; + +// Legacy schema for backward compatibility +export const SETUP_APP_AUTOMATE_SCHEMA_LEGACY = { + 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( diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index 37705bd5..bc680a5f 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -2,8 +2,6 @@ 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 { generateAppAutomateYML } from "./config-generator.js"; - import { getAppUploadInstruction, validateSupportforAppAutomate, @@ -15,6 +13,8 @@ import { generateAppBrowserStackYMLInstructions, } from "./index.js"; +import { validateDevices } from "../../sdk-utils/common/device-validator.js"; + import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework, @@ -38,16 +38,23 @@ 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>) ?? []; const appPath = input.appPath as string; const framework = input.detectedFramework as SupportedFramework; //Validating if supported framework or not validateSupportforAppAutomate(framework, language, testingFramework); - // For app automate, we don't have individual device validation like in automate - // The platforms array already contains the desired platforms - const validatedEnvironments: any[] = []; + // Use default mobile devices when array is empty + const devices = inputDevices.length === 0 + ? [['android', 'Samsung Galaxy S24', 'latest']] // Default mobile device for App Automate + : inputDevices; + + // Validate devices against real BrowserStack device data + const validatedEnvironments = await validateDevices(devices, framework); + + // Extract platforms for backward compatibility (if needed) + const platforms = validatedEnvironments.map((env) => env.platform); // Step 1: Generate SDK setup command const sdkCommand = getAppSDKPrefixCommand( @@ -63,26 +70,17 @@ export async function setupAppAutomateHandler( } // Step 2: Generate browserstack.yml configuration - let configInstructions; - if (validatedEnvironments.length > 0) { - // Use validated environments for YML generation - configInstructions = generateAppAutomateYML( + const configInstructions = generateAppBrowserStackYMLInstructions( + { validatedEnvironments, - username, - accessKey, - appPath, - input.project as string, - ); - } else { - // Fallback to original method - configInstructions = generateAppBrowserStackYMLInstructions( platforms, - username, - accessKey, - appPath, testingFramework, - ); - } + projectName: input.project as string, + }, + username, + accessKey, + appPath, + ); if (configInstructions) { instructions.push({ content: configInstructions, type: "config" }); diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 47898175..57776849 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -9,6 +9,7 @@ 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 { SETUP_APP_AUTOMATE_DESCRIPTION, @@ -25,8 +26,6 @@ import { BrowserStackProducts, } from "../lib/device-cache.js"; -import { validateAppAutomateDevices } from "./sdk-utils/common/device-validator.js"; - import { findMatchingDevice, getDeviceVersions, diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index 2a239e19..dedcadbb 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -1,43 +1,60 @@ -/** - * Utilities for generating BrowserStack configuration files. - */ import { ValidatedEnvironment } from "../common/device-validator.js"; export function generateBrowserStackYMLInstructions( - desiredPlatforms: string[], - enablePercy: boolean = false, - projectName: string, -) { + config: { + validatedEnvironments?: ValidatedEnvironment[]; + platforms?: string[]; + enablePercy?: boolean; + projectName: string; + } +): string { + const enablePercy = config.enablePercy || false; + const projectName = config.projectName; + + // Generate platform configurations using the utility function + const platformConfigs = generatePlatformConfigs(config); + + // Determine build name and step title + const buildName = config.validatedEnvironments && config.validatedEnvironments.length > 0 + ? `${projectName}-Build` + : "Sample-Build"; + + const stepTitle = config.validatedEnvironments && config.validatedEnvironments.length > 0 + ? "Create a browserstack.yml file in the project root with your validated device configurations:" + : "Create a browserstack.yml file in the project root. The file should be in the following format:"; + let ymlContent = ` # ====================== # BrowserStack Reporting # ====================== -# A single name for your project to organize all your tests. This is required for Percy. projectName: ${projectName} -# TODO: Replace these sample values with your actual project details -buildName: Sample-Build +buildName: ${buildName} # ======================================= # Platforms (Browsers / Devices to test) -# ======================================= +# =======================================`; + + if (config.validatedEnvironments && config.validatedEnvironments.length > 0) { + ymlContent += ` +# Auto-generated from validated device configurations +platforms: +${platformConfigs}`; + } else { + ymlContent += ` # Platforms object contains all the browser / device combinations you want to test on. # Generate this on the basis of the following platforms requested by the user: -# Requested platforms: ${desiredPlatforms} +# Requested platforms: ${config.platforms || []} platforms: - - os: Windows - osVersion: 11 - browserName: chrome - browserVersion: latest - +${platformConfigs}`; + } + + ymlContent += ` + # ======================= # Parallels per Platform # ======================= # The number of parallel threads to be used for each platform set. # BrowserStack's SDK runner will select the best strategy based on the configured value -# -# Example 1 - If you have configured 3 platforms and set \`parallelsPerPlatform\` as 2, a total of 6 (2 * 3) parallel threads will be used on BrowserStack -# -# Example 2 - If you have configured 1 platform and set \`parallelsPerPlatform\` as 5, a total of 5 (1 * 5) parallel threads will be used on BrowserStack parallelsPerPlatform: 1 # ================= @@ -63,106 +80,65 @@ testObservability: true # For Test Observability`; percy: true percyCaptureMode: manual`; } + return ` ---STEP--- -Create a browserstack.yml file in the project root. The file should be in the following format: +${stepTitle} \`\`\`yaml${ymlContent} \`\`\` \n`; } -/** - * Generate browserstack.yml content from validated device configurations - */ -export function generateBrowserStackYMLFromValidatedEnvironments( - validatedEnvironments: ValidatedEnvironment[], - enablePercy: boolean = false, - projectName: string, -) { - // Generate platforms array from validated environments - const platforms = validatedEnvironments.map((env) => { - if (env.platform === "windows" || env.platform === "macos") { - // Desktop configuration - return { - os: env.platform === "windows" ? "Windows" : "OS X", - osVersion: env.osVersion, - browserName: env.browser, - browserVersion: env.browserVersion || "latest", - }; - } else { - // Mobile configuration (android/ios) - return { - deviceName: env.deviceName, - osVersion: env.osVersion, - browserName: env.browser, - }; - } - }); - - // Convert platforms to YAML format - const platformsYAML = platforms - .map((platform) => { - if (platform.deviceName) { - // Mobile platform - return ` - deviceName: "${platform.deviceName}" +function generatePlatformConfigs(config: { + validatedEnvironments?: ValidatedEnvironment[]; + platforms?: string[]; +}): string { + if (config.validatedEnvironments && config.validatedEnvironments.length > 0) { + // Generate platforms array from validated environments + const platforms = config.validatedEnvironments.map((env) => { + if (env.platform === "windows" || env.platform === "macos") { + // Desktop configuration + return { + os: env.platform === "windows" ? "Windows" : "OS X", + osVersion: env.osVersion, + browserName: env.browser, + browserVersion: env.browserVersion || "latest", + }; + } else { + // Mobile configuration (android/ios) + return { + deviceName: env.deviceName, + osVersion: env.osVersion, + browserName: env.browser, + }; + } + }); + + // Convert platforms to YAML format + return platforms + .map((platform) => { + if (platform.deviceName) { + // Mobile platform + return ` - deviceName: "${platform.deviceName}" osVersion: "${platform.osVersion}" browserName: ${platform.browserName}`; - } else { - // Desktop platform - return ` - os: ${platform.os} + } else { + // Desktop platform + return ` - os: ${platform.os} osVersion: "${platform.osVersion}" browserName: ${platform.browserName} browserVersion: ${platform.browserVersion}`; - } - }) - .join("\n"); - - let ymlContent = ` -# ====================== -# BrowserStack Reporting -# ====================== -projectName: ${projectName} -buildName: ${projectName}-Build - -# ======================================= -# Platforms (Browsers / Devices to test) -# ======================================= -# Auto-generated from validated device configurations -platforms: -${platformsYAML} - -# ======================= -# Parallels per Platform -# ======================= -parallelsPerPlatform: 1 - -# ================= -# Local Testing -# ================= -browserstackLocal: true - -# =================== -# Debugging features -# =================== -debug: true -testObservability: true`; - - if (enablePercy) { - ymlContent += ` - -# ===================== -# Percy Visual Testing -# ===================== -percy: true -percyCaptureMode: manual`; + } + }) + .join("\n"); + } else if (config.platforms && config.platforms.length > 0) { + // Fallback to default platforms configuration + return ` - os: Windows + osVersion: 11 + browserName: chrome + browserVersion: latest`; } - - return ` ----STEP--- -Create a browserstack.yml file in the project root with your validated device configurations: - -\`\`\`yaml${ymlContent} -\`\`\` -\n`; + + return ""; } diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 8c13bf64..22f00488 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -3,7 +3,7 @@ import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; import { RunTestsOnBrowserStackInput } from "../common/schema.js"; import { getBrowserStackAuth } from "../../../lib/get-auth.js"; import { getSDKPrefixCommand } from "./commands.js"; -import { generateBrowserStackYMLFromValidatedEnvironments } from "./configUtils.js"; +import { generateBrowserStackYMLInstructions } from "./configUtils.js"; import { getInstructionsForProjectConfiguration } from "../common/instructionUtils.js"; import { BrowserStackConfig } from "../../../lib/types.js"; import { validateDevices } from "../common/device-validator.js"; @@ -86,11 +86,11 @@ export async function runBstackSDKOnly( }); } - const ymlInstructions = generateBrowserStackYMLFromValidatedEnvironments( + const ymlInstructions = generateBrowserStackYMLInstructions({ validatedEnvironments, - false, - input.projectName, - ); + enablePercy: false, + projectName: input.projectName, + }); if (ymlInstructions) { steps.push({ diff --git a/src/tools/sdk-utils/common/device-validator.ts b/src/tools/sdk-utils/common/device-validator.ts index 513c523e..b5a2572e 100644 --- a/src/tools/sdk-utils/common/device-validator.ts +++ b/src/tools/sdk-utils/common/device-validator.ts @@ -15,7 +15,6 @@ export interface ValidatedEnvironment { notes?: string; } -// Centralized defaults const DEFAULTS = { windows: { browser: "chrome" }, macos: { browser: "safari" }, @@ -23,11 +22,6 @@ const DEFAULTS = { ios: { device: "iPhone 15", browser: "safari" }, } as const; -/** - * Validates device tuples against real BrowserStack device data - * This prevents hallucination by checking against actual available devices - * Throws errors directly if validation fails - */ export async function validateDevices( devices: Array>, framework?: string, @@ -67,17 +61,11 @@ export async function validateDevices( // Use framework-specific endpoint for app automate data if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) { appAutomateData = await getDevicesAndBrowsers( - BrowserStackProducts.PLAYWRIGHT_APP_AUTOMATE, - ); - } else if ( - framework === SDKSupportedBrowserAutomationFrameworkEnum.selenium - ) { - appAutomateData = await getDevicesAndBrowsers( - BrowserStackProducts.SELENIUM_APP_AUTOMATE, + BrowserStackProducts.PLAYWRIGHT_AUTOMATE, ); } else { appAutomateData = await getDevicesAndBrowsers( - BrowserStackProducts.APP_AUTOMATE, + BrowserStackProducts.SELENIUM_AUTOMATE, ); } } @@ -167,12 +155,6 @@ export async function validateDevices( return validatedEnvironments; } -/** - * Validates mobile device strings for App Automate against real BrowserStack device data - * Device strings should be in format: "Device Name-OS Version" (e.g., "Samsung Galaxy S20-10.0") - * This prevents hallucination by checking against actual available devices - * Throws errors directly if validation fails - */ export async function validateAppAutomateDevices( deviceStrings: string[], ): Promise { diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index c170cd95..1473ccc3 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -12,7 +12,7 @@ export const PlatformEnum = { MACOS: "macos", ANDROID: "android", IOS: "ios", -} as const; +} as const; export const WindowsPlatformEnum = { WINDOWS: "windows", diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index 11f95956..18208cb5 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -106,14 +106,14 @@ export function runPercyWithBrowserstackSDK( }); } - const ymlInstructions = generateBrowserStackYMLInstructions( + const ymlInstructions = generateBrowserStackYMLInstructions({ // For now, feed a normalized summary string from devices for the comment - ((input as any).devices as string[][] | undefined)?.map((t) => + platforms: ((input as any).devices as string[][] | undefined)?.map((t) => t.join(" "), ) || [], - true, - input.projectName, - ); + enablePercy: true, + projectName: input.projectName, + }); if (ymlInstructions) { steps.push({ From 50f5273660ba4e549c789349ab17211fe80358f5 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 19 Sep 2025 01:19:13 +0530 Subject: [PATCH 65/84] enhance device validation and configuration --- src/lib/device-cache.ts | 2 +- src/lib/version-resolver.ts | 68 ++ .../appautomate-utils/appium-sdk/constants.ts | 5 +- .../appautomate-utils/appium-sdk/handler.ts | 11 +- .../native-execution/constants.ts | 4 +- src/tools/appautomate.ts | 18 +- src/tools/sdk-utils/bstack/configUtils.ts | 30 +- .../sdk-utils/common/device-validator.ts | 716 +++++++++++------- src/tools/sdk-utils/common/schema.ts | 2 +- src/tools/sdk-utils/percy-bstack/handler.ts | 7 +- 10 files changed, 551 insertions(+), 312 deletions(-) diff --git a/src/lib/device-cache.ts b/src/lib/device-cache.ts index fc832ee0..32066c3c 100644 --- a/src/lib/device-cache.ts +++ b/src/lib/device-cache.ts @@ -25,7 +25,7 @@ const URLS: Record = { [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/selenium_app_automate.json", + "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", }; diff --git a/src/lib/version-resolver.ts b/src/lib/version-resolver.ts index 25564e97..aef01118 100644 --- a/src/lib/version-resolver.ts +++ b/src/lib/version-resolver.ts @@ -47,3 +47,71 @@ export function resolveVersion(requested: string, available: string[]): string { // final fallback return uniq[0]; } + +export function resolveVersionWithFuzzy( + 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 + const nums = uniq + .map((v) => ({ v, n: parseFloat(v) })) + .filter((x) => !isNaN(x.n)) + .sort((a, b) => a.n - b.n); + if (nums.length) { + return requested === "latest" ? nums[nums.length - 1].v : nums[0].v; + } + // fallback lex + const lex = uniq.slice().sort(); + return requested === "latest" ? lex[lex.length - 1] : lex[0]; + } + + // exact match? + if (uniq.includes(requested)) { + return requested; + } + + // Try major version matching (e.g., "14" matches "14.0", "14.1", etc.) + const reqNum = parseFloat(requested); + 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; + } + } + + // Fallback: return the first available version + return uniq[0]; +} diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index 0d909b3b..34d5ad14 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -75,7 +75,9 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { ]), ) .max(3) - .default([[AppSDKSupportedPlatformEnum.android, 'Samsung Galaxy S24', 'latest']]) + .default([ + [AppSDKSupportedPlatformEnum.android, "Samsung Galaxy S24", "latest"], + ]) .describe( "Preferred input: 1-3 tuples describing target mobile devices. Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]", ), @@ -92,7 +94,6 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { .describe("Project name for organizing test runs on BrowserStack."), }; -// Legacy schema for backward compatibility export const SETUP_APP_AUTOMATE_SCHEMA_LEGACY = { detectedFramework: z .nativeEnum(AppSDKSupportedFrameworkEnum) diff --git a/src/tools/appautomate-utils/appium-sdk/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index bc680a5f..195ac546 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -13,7 +13,7 @@ import { generateAppBrowserStackYMLInstructions, } from "./index.js"; -import { validateDevices } from "../../sdk-utils/common/device-validator.js"; +import { validateAppAutomateDevices } from "../../sdk-utils/common/device-validator.js"; import { AppSDKSupportedLanguage, @@ -46,12 +46,13 @@ export async function setupAppAutomateHandler( validateSupportforAppAutomate(framework, language, testingFramework); // Use default mobile devices when array is empty - const devices = inputDevices.length === 0 - ? [['android', 'Samsung Galaxy S24', 'latest']] // Default mobile device for App Automate - : inputDevices; + const devices = + inputDevices.length === 0 + ? [["android", "Samsung Galaxy S24", "latest"]] + : inputDevices; // Validate devices against real BrowserStack device data - const validatedEnvironments = await validateDevices(devices, framework); + const validatedEnvironments = await validateAppAutomateDevices(devices); // Extract platforms for backward compatibility (if needed) const platforms = validatedEnvironments.map((env) => env.platform); diff --git a/src/tools/appautomate-utils/native-execution/constants.ts b/src/tools/appautomate-utils/native-execution/constants.ts index 722db385..56e7c2d4 100644 --- a/src/tools/appautomate-utils/native-execution/constants.ts +++ b/src/tools/appautomate-utils/native-execution/constants.ts @@ -29,9 +29,9 @@ export const RUN_APP_AUTOMATE_SCHEMA = { "If in other directory, provide existing test file path", ), devices: z - .array(z.string()) + .array(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'].", + "List of devices to run the test on, e.g., [['android', 'Samsung Galaxy S20', '10.0'], ['ios', 'iPhone 12 Pro', '16.0']].", ), project: z .string() diff --git a/src/tools/appautomate.ts b/src/tools/appautomate.ts index 57776849..29a7d57a 100644 --- a/src/tools/appautomate.ts +++ b/src/tools/appautomate.ts @@ -176,7 +176,7 @@ async function runAppTestsOnBrowserStack( testSuitePath?: string; browserstackAppUrl?: string; browserstackTestSuiteUrl?: string; - devices: string[]; + devices: Array>; project: string; detectedAutomationFramework: string; }, @@ -223,10 +223,16 @@ async function runAppTestsOnBrowserStack( logger.info(`Test suite uploaded. URL: ${test_suite_url}`); } + // Convert array format to string format for Espresso + const deviceStrings = args.devices.map((device) => { + const [, deviceName, osVersion] = device; + return `${deviceName}-${osVersion}`; + }); + const build_id = await triggerEspressoBuild( app_url, test_suite_url, - args.devices, + deviceStrings, args.project, ); @@ -268,10 +274,16 @@ async function runAppTestsOnBrowserStack( logger.info(`Test suite uploaded. URL: ${test_suite_url}`); } + // Convert array format to string format for XCUITest + const deviceStrings = args.devices.map((device) => { + const [, deviceName, osVersion] = device; + return `${deviceName}-${osVersion}`; + }); + const build_id = await triggerXcuiBuild( app_url, test_suite_url, - args.devices, + deviceStrings, args.project, config, ); diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index dedcadbb..1592368c 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -1,13 +1,11 @@ import { ValidatedEnvironment } from "../common/device-validator.js"; -export function generateBrowserStackYMLInstructions( - config: { - validatedEnvironments?: ValidatedEnvironment[]; - platforms?: string[]; - enablePercy?: boolean; - projectName: string; - } -): string { +export function generateBrowserStackYMLInstructions(config: { + validatedEnvironments?: ValidatedEnvironment[]; + platforms?: string[]; + enablePercy?: boolean; + projectName: string; +}): string { const enablePercy = config.enablePercy || false; const projectName = config.projectName; @@ -15,13 +13,15 @@ export function generateBrowserStackYMLInstructions( const platformConfigs = generatePlatformConfigs(config); // Determine build name and step title - const buildName = config.validatedEnvironments && config.validatedEnvironments.length > 0 - ? `${projectName}-Build` - : "Sample-Build"; + const buildName = + config.validatedEnvironments && config.validatedEnvironments.length > 0 + ? `${projectName}-Build` + : "Sample-Build"; - const stepTitle = config.validatedEnvironments && config.validatedEnvironments.length > 0 - ? "Create a browserstack.yml file in the project root with your validated device configurations:" - : "Create a browserstack.yml file in the project root. The file should be in the following format:"; + const stepTitle = + config.validatedEnvironments && config.validatedEnvironments.length > 0 + ? "Create a browserstack.yml file in the project root with your validated device configurations:" + : "Create a browserstack.yml file in the project root. The file should be in the following format:"; let ymlContent = ` # ====================== @@ -139,6 +139,6 @@ function generatePlatformConfigs(config: { browserName: chrome browserVersion: latest`; } - + return ""; } diff --git a/src/tools/sdk-utils/common/device-validator.ts b/src/tools/sdk-utils/common/device-validator.ts index b5a2572e..fe097cc4 100644 --- a/src/tools/sdk-utils/common/device-validator.ts +++ b/src/tools/sdk-utils/common/device-validator.ts @@ -6,6 +6,28 @@ import { resolveVersion } from "../../../lib/version-resolver.js"; import { customFuzzySearch } from "../../../lib/fuzzy.js"; import { SDKSupportedBrowserAutomationFrameworkEnum } from "./types.js"; +// ============================================================================ +// SHARED TYPES AND INTERFACES +// ============================================================================ + +// Type definitions for better type safety +export interface DesktopBrowserEntry { + os: string; + os_version: string; + browser: string; + browser_version: string; +} + +export interface MobileDeviceEntry { + os: "android" | "ios"; + os_version: string; + display_name: string; + browsers?: Array<{ + browser: string; + display_name?: string; + }>; +} + export interface ValidatedEnvironment { platform: string; osVersion: string; @@ -15,6 +37,34 @@ export interface ValidatedEnvironment { notes?: string; } +// Raw data interfaces for API responses +interface RawDesktopPlatform { + os: string; + os_version: string; + browsers: Array<{ + browser: string; + browser_version: string; + }>; +} + +interface RawMobileGroup { + os: "android" | "ios"; + devices: Array<{ + os_version: string; + display_name: string; + browser?: string; + browsers?: Array<{ + browser: string; + display_name?: string; + }>; + }>; +} + +interface RawDeviceData { + desktop?: RawDesktopPlatform[]; + mobile?: RawMobileGroup[]; +} + const DEFAULTS = { windows: { browser: "chrome" }, macos: { browser: "safari" }, @@ -22,6 +72,145 @@ const DEFAULTS = { ios: { device: "iPhone 15", browser: "safari" }, } as const; +// Performance optimization: Indexed maps for faster lookups +interface DesktopIndex { + byOS: Map; + byOSVersion: Map; + byBrowser: Map; + nested: Map>>; +} + +interface MobileIndex { + byPlatform: Map; + byDeviceName: Map; + byOSVersion: Map; +} + +// ============================================================================ +// AUTOMATE SECTION (Desktop + Mobile for BrowserStack SDK) +// ============================================================================ + +// Helper functions to build device entries and eliminate duplication +function buildDesktopEntries( + automateData: RawDeviceData, +): DesktopBrowserEntry[] { + if (!automateData.desktop) { + return []; + } + + return automateData.desktop.flatMap((platform: RawDesktopPlatform) => + platform.browsers.map((browser) => ({ + os: platform.os, + os_version: platform.os_version, + browser: browser.browser, + browser_version: browser.browser_version, + })), + ); +} + +function buildMobileEntries( + appAutomateData: RawDeviceData, + platform: "android" | "ios", +): MobileDeviceEntry[] { + if (!appAutomateData.mobile) { + return []; + } + + return appAutomateData.mobile + .filter((group: RawMobileGroup) => group.os === platform) + .flatMap((group: RawMobileGroup) => + group.devices.map((device) => ({ + os: group.os, + os_version: device.os_version, + display_name: device.display_name, + browsers: device.browsers || [ + { + browser: + device.browser || (platform === "android" ? "chrome" : "safari"), + }, + ], + })), + ); +} + +// Performance optimization: Create indexed maps for faster lookups +function createDesktopIndex(entries: DesktopBrowserEntry[]): DesktopIndex { + const byOS = new Map(); + const byOSVersion = new Map(); + const byBrowser = new Map(); + const nested = new Map< + string, + Map> + >(); + + for (const entry of entries) { + // Index by OS + if (!byOS.has(entry.os)) { + byOS.set(entry.os, []); + } + byOS.get(entry.os)!.push(entry); + + // Index by OS version + if (!byOSVersion.has(entry.os_version)) { + byOSVersion.set(entry.os_version, []); + } + byOSVersion.get(entry.os_version)!.push(entry); + + // Index by browser + if (!byBrowser.has(entry.browser)) { + byBrowser.set(entry.browser, []); + } + byBrowser.get(entry.browser)!.push(entry); + + // Build nested index: Map>> + if (!nested.has(entry.os)) { + nested.set(entry.os, new Map()); + } + const osMap = nested.get(entry.os)!; + + if (!osMap.has(entry.os_version)) { + osMap.set(entry.os_version, new Map()); + } + const osVersionMap = osMap.get(entry.os_version)!; + + if (!osVersionMap.has(entry.browser)) { + osVersionMap.set(entry.browser, []); + } + osVersionMap.get(entry.browser)!.push(entry); + } + + return { byOS, byOSVersion, byBrowser, nested }; +} + +function createMobileIndex(entries: MobileDeviceEntry[]): MobileIndex { + const byPlatform = new Map(); + const byDeviceName = new Map(); + const byOSVersion = new Map(); + + for (const entry of entries) { + // Index by platform + if (!byPlatform.has(entry.os)) { + byPlatform.set(entry.os, []); + } + byPlatform.get(entry.os)!.push(entry); + + // Index by device name (case-insensitive) + const deviceKey = entry.display_name.toLowerCase(); + if (!byDeviceName.has(deviceKey)) { + byDeviceName.set(deviceKey, []); + } + byDeviceName.get(deviceKey)!.push(entry); + + // Index by OS version + if (!byOSVersion.has(entry.os_version)) { + byOSVersion.set(entry.os_version, []); + } + byOSVersion.get(entry.os_version)!.push(entry); + } + + return { byPlatform, byDeviceName, byOSVersion }; +} + export async function validateDevices( devices: Array>, framework?: string, @@ -29,280 +218,157 @@ export async function validateDevices( const validatedEnvironments: ValidatedEnvironment[] = []; if (!devices || devices.length === 0) { - // Default fallback - no validation needed + // Use centralized default fallback return [ { platform: "windows", - osVersion: "latest", - browser: "chrome", + osVersion: "11", + browser: DEFAULTS.windows.browser, browserVersion: "latest", }, ]; } - try { - // Determine what data we need to fetch - const needsDesktop = devices.some((env) => - ["windows", "mac", "macos"].includes((env[0] || "").toLowerCase()), - ); - const needsMobile = devices.some((env) => - ["android", "ios"].includes((env[0] || "").toLowerCase()), - ); - - // Fetch only needed data - let liveData: any = null; - let appAutomateData: any = null; + // Determine what data we need to fetch + const needsDesktop = devices.some((env) => + ["windows", "macos"].includes((env[0] || "").toLowerCase()), + ); + const needsMobile = devices.some((env) => + ["android", "ios"].includes((env[0] || "").toLowerCase()), + ); - if (needsDesktop) { - liveData = await getDevicesAndBrowsers(BrowserStackProducts.LIVE); - } + // Fetch data using framework-specific endpoint for both desktop and mobile + let deviceData: RawDeviceData | null = null; - if (needsMobile) { - // Use framework-specific endpoint for app automate data + try { + if (needsDesktop || needsMobile) { if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) { - appAutomateData = await getDevicesAndBrowsers( + deviceData = (await getDevicesAndBrowsers( BrowserStackProducts.PLAYWRIGHT_AUTOMATE, - ); + )) as RawDeviceData; } else { - appAutomateData = await getDevicesAndBrowsers( + deviceData = (await getDevicesAndBrowsers( BrowserStackProducts.SELENIUM_AUTOMATE, - ); + )) as RawDeviceData; } } - - for (const env of devices) { - const discriminator = (env[0] || "").toLowerCase(); - let validatedEnv: ValidatedEnvironment; - - if (discriminator === "windows") { - const allEntries = liveData.desktop.flatMap((plat: any) => - plat.browsers.map((b: any) => ({ - os: plat.os, - os_version: plat.os_version, - browser: b.browser, - browser_version: b.browser_version, - })), - ); - validatedEnv = await validateDesktopEnvironment( - env, - allEntries, - "windows", - DEFAULTS.windows.browser, - ); - } else if (discriminator === "mac" || discriminator === "macos") { - const allEntries = liveData.desktop.flatMap((plat: any) => - plat.browsers.map((b: any) => ({ - os: plat.os, - os_version: plat.os_version, - browser: b.browser, - browser_version: b.browser_version, - })), - ); - validatedEnv = await validateDesktopEnvironment( - env, - allEntries, - "macos", - DEFAULTS.macos.browser, - ); - } else if (discriminator === "android") { - const allEntries = appAutomateData.mobile.flatMap((grp: any) => - grp.devices.map((d: any) => ({ - os: grp.os, - os_version: d.os_version, - display_name: d.display_name, - browsers: d.browsers || [ - { browser: d.browser, display_name: d.browser }, - ], - })), - ); - validatedEnv = await validateMobileEnvironment( - env, - allEntries, - "android", - DEFAULTS.android.device, - DEFAULTS.android.browser, - ); - } else if (discriminator === "ios") { - const allEntries = appAutomateData.mobile.flatMap((grp: any) => - grp.devices.map((d: any) => ({ - os: grp.os, - os_version: d.os_version, - display_name: d.display_name, - browsers: d.browsers || [ - { browser: d.browser, display_name: d.browser }, - ], - })), - ); - validatedEnv = await validateMobileEnvironment( - env, - allEntries, - "ios", - DEFAULTS.ios.device, - DEFAULTS.ios.browser, - ); - } else { - throw new Error(`Unsupported platform: ${discriminator}`); - } - - validatedEnvironments.push(validatedEnv); - } } catch (error) { throw new Error( `Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`, ); } - return validatedEnvironments; -} + // Preprocess data into indexed maps for better performance + let desktopIndex: DesktopIndex | null = null; + let androidIndex: MobileIndex | null = null; + let iosIndex: MobileIndex | null = null; -export async function validateAppAutomateDevices( - deviceStrings: string[], -): Promise { - const validatedDevices: ValidatedEnvironment[] = []; - - if (!deviceStrings || deviceStrings.length === 0) { - // Default fallback - no validation needed - return [ - { - platform: "android", - osVersion: "latest", - deviceName: "Samsung Galaxy S24", - }, - ]; + if (needsDesktop && deviceData) { + const desktopEntries = buildDesktopEntries(deviceData); + desktopIndex = createDesktopIndex(desktopEntries); } - try { - // Fetch app automate device data - const appAutomateData = await getDevicesAndBrowsers( - BrowserStackProducts.APP_AUTOMATE, - ); - - for (const deviceString of deviceStrings) { - // Parse device string in format "Device Name-OS Version" - const parts = deviceString.split("-"); - if (parts.length < 2) { - throw new Error( - `Invalid device format: "${deviceString}". Expected format: "Device Name-OS Version" (e.g., "Samsung Galaxy S20-10.0")`, - ); - } - - const deviceName = parts.slice(0, -1).join("-"); // Handle device names with hyphens - const osVersion = parts[parts.length - 1]; - - // Find matching device in the data - let validatedDevice: ValidatedEnvironment | null = null; - - for (const platformGroup of appAutomateData.mobile) { - const platformDevices = platformGroup.devices; - - // Find exact device name match (case-insensitive) - const exactMatch = platformDevices.find( - (d: any) => d.display_name.toLowerCase() === deviceName.toLowerCase(), - ); - - if (exactMatch) { - // Check if the OS version is available for this device - const deviceVersions = platformDevices - .filter((d: any) => d.display_name === exactMatch.display_name) - .map((d: any) => d.os_version); - - const validatedOSVersion = resolveVersion(osVersion, deviceVersions); - - if (!deviceVersions.includes(validatedOSVersion)) { - throw new Error( - `OS version "${osVersion}" not available for device "${deviceName}". Available versions: ${deviceVersions.join(", ")}`, - ); - } - - validatedDevice = { - platform: platformGroup.os, - osVersion: validatedOSVersion, - deviceName: exactMatch.display_name, - }; - break; - } - } - - if (!validatedDevice) { - // If no exact match found, suggest similar devices - const allDevices = appAutomateData.mobile.flatMap((grp: any) => - grp.devices.map((d: any) => ({ - ...d, - platform: grp.os, - })), - ); - - const deviceMatches = customFuzzySearch( - allDevices, - ["display_name"], - deviceName, - 5, - ); - - const suggestions = deviceMatches - .map((m) => `${m.display_name}`) - .join(", "); + if (needsMobile && deviceData) { + const androidEntries = buildMobileEntries(deviceData, "android"); + const iosEntries = buildMobileEntries(deviceData, "ios"); + androidIndex = createMobileIndex(androidEntries); + iosIndex = createMobileIndex(iosEntries); + } - throw new Error( - `Device "${deviceName}" not found.\nAvailable similar devices: ${suggestions}\nPlease use the exact device name with format: "Device Name-OS Version"`, - ); - } + for (const env of devices) { + const discriminator = (env[0] || "").toLowerCase(); + let validatedEnv: ValidatedEnvironment; - validatedDevices.push(validatedDevice); + if (discriminator === "windows") { + validatedEnv = validateDesktopEnvironment( + env, + desktopIndex!, + "windows", + DEFAULTS.windows.browser, + ); + } else if (discriminator === "macos") { + validatedEnv = validateDesktopEnvironment( + env, + desktopIndex!, + "macos", + DEFAULTS.macos.browser, + ); + } else if (discriminator === "android") { + validatedEnv = validateMobileEnvironment( + env, + androidIndex!, + "android", + DEFAULTS.android.device, + DEFAULTS.android.browser, + ); + } else if (discriminator === "ios") { + validatedEnv = validateMobileEnvironment( + env, + iosIndex!, + "ios", + DEFAULTS.ios.device, + DEFAULTS.ios.browser, + ); + } else { + throw new Error(`Unsupported platform: ${discriminator}`); } - } catch (error) { - throw new Error( - `Failed to validate devices: ${error instanceof Error ? error.message : String(error)}`, - ); + + validatedEnvironments.push(validatedEnv); } - return validatedDevices; + return validatedEnvironments; } -// Unified desktop validation helper -async function validateDesktopEnvironment( +// Optimized desktop validation using nested indexed maps for O(1) lookups +function validateDesktopEnvironment( env: string[], - entries: any[], + index: DesktopIndex, platform: "windows" | "macos", defaultBrowser: string, -): Promise { +): ValidatedEnvironment { const [, osVersion, browser, browserVersion] = env; - const platformEntries = entries.filter((e) => - platform === "windows" ? e.os === "Windows" : e.os === "OS X", - ); + const osKey = platform === "windows" ? "Windows" : "OS X"; - if (platformEntries.length === 0) { + // Use nested index for O(1) lookup instead of filtering + const osMap = index.nested.get(osKey); + if (!osMap) { throw new Error(`No ${platform} devices available`); } - const availableOSVersions = [ - ...new Set(platformEntries.map((e) => e.os_version)), - ] as string[]; - - const validatedOSVersion = - platform === "macos" - ? validateMacOSVersion(osVersion || "latest", availableOSVersions) - : resolveVersion(osVersion || "latest", availableOSVersions); + // Get available OS versions for this platform + const availableOSVersions = Array.from(osMap.keys()); - const osFiltered = platformEntries.filter( - (e) => e.os_version === validatedOSVersion, + const validatedOSVersion = resolveVersion( + osVersion || "latest", + availableOSVersions, ); - const availableBrowsers = [ - ...new Set(osFiltered.map((e) => e.browser)), - ] as string[]; - const validatedBrowser = validateBrowser( + // Use nested index for O(1) lookup + const osVersionMap = osMap.get(validatedOSVersion); + if (!osVersionMap) { + throw new Error( + `OS version "${validatedOSVersion}" not available for ${platform}`, + ); + } + + // Get available browsers for this OS version + const availableBrowsers = Array.from(osVersionMap.keys()); + const validatedBrowser = validateBrowserExact( browser || defaultBrowser, availableBrowsers, ); - const browserFiltered = osFiltered.filter( - (e) => e.browser === validatedBrowser, - ); + // Use nested index for O(1) lookup + const browserEntries = osVersionMap.get(validatedBrowser); + if (!browserEntries || browserEntries.length === 0) { + throw new Error( + `Browser "${validatedBrowser}" not available for ${platform} ${validatedOSVersion}`, + ); + } const availableBrowserVersions = [ - ...new Set(browserFiltered.map((e) => e.browser_version)), + ...new Set(browserEntries.map((e) => e.browser_version)), ] as string[]; const validatedBrowserVersion = resolveVersion( browserVersion || "latest", @@ -317,21 +383,22 @@ async function validateDesktopEnvironment( }; } -// Unified mobile validation helper -async function validateMobileEnvironment( +// Optimized mobile validation using indexed maps +function validateMobileEnvironment( env: string[], - entries: any[], + index: MobileIndex, platform: "android" | "ios", defaultDevice: string, defaultBrowser: string, -): Promise { +): ValidatedEnvironment { const [, deviceName, osVersion, browser] = env; - const platformEntries = entries.filter((e) => e.os === platform); + const platformEntries = index.byPlatform.get(platform) || []; if (platformEntries.length === 0) { throw new Error(`No ${platform} devices available`); } + // Use fuzzy search only for device names (as suggested in feedback) const deviceMatches = customFuzzySearch( platformEntries, ["display_name"], @@ -341,25 +408,28 @@ async function validateMobileEnvironment( if (deviceMatches.length === 0) { throw new Error( `No ${platform} devices matching "${deviceName}". Available devices: ${platformEntries - .map((d) => d.display_name || d.device || "unknown") + .map((d) => d.display_name || "unknown") .slice(0, 5) .join(", ")}`, ); } + // Try to find exact match first const exactMatch = deviceMatches.find( (m) => m.display_name.toLowerCase() === (deviceName || "").toLowerCase(), ); + + // If no exact match, throw error instead of using fuzzy match if (!exactMatch) { const suggestions = deviceMatches.map((m) => m.display_name).join(", "); throw new Error( - `Error Device "${deviceName}" not found for ${platform}.\nAvailable options: ${suggestions}\nPlease correct these issues and try again.`, + `Device "${deviceName}" not found exactly for ${platform}. Available similar devices: ${suggestions}. Please use the exact device name.`, ); } - const deviceFiltered = platformEntries.filter( - (d) => d.display_name === exactMatch.display_name, - ); + // Use index for faster filtering + const deviceKey = exactMatch.display_name.toLowerCase(); + const deviceFiltered = index.byDeviceName.get(deviceKey) || []; const availableOSVersions = [ ...new Set(deviceFiltered.map((d) => d.os_version)), @@ -369,12 +439,13 @@ async function validateMobileEnvironment( availableOSVersions, ); - // Filter by OS version - const osFiltered = deviceFiltered.filter( - (d) => d.os_version === validatedOSVersion, + // Use index for faster filtering + const osVersionEntries = index.byOSVersion.get(validatedOSVersion) || []; + const osFiltered = osVersionEntries.filter( + (d) => d.display_name.toLowerCase() === deviceKey, ); - // Validate browser if provided + // Validate browser if provided - use exact matching for browsers let validatedBrowser = browser || defaultBrowser; if (browser && osFiltered.length > 0) { // Extract browsers more carefully - handle different possible structures @@ -384,14 +455,11 @@ async function validateMobileEnvironment( if (d.browsers && Array.isArray(d.browsers)) { // If browsers is an array of objects with browser property return d.browsers - .map((b: any) => { + .map((b) => { // Use display_name for user-friendly browser names, fallback to browser field - return b.display_name || b.browser || b.browserName || b.name; + return b.display_name || b.browser; }) .filter(Boolean); - } else if (d.browser) { - // If there's just a browser property directly - return [d.browser]; } // For mobile devices, provide default browsers if none found return platform === "android" ? ["chrome"] : ["safari"]; @@ -401,7 +469,7 @@ async function validateMobileEnvironment( if (availableBrowsers.length > 0) { try { - validatedBrowser = validateBrowser(browser, availableBrowsers); + validatedBrowser = validateBrowserExact(browser, availableBrowsers); } catch (error) { // Add more context to browser validation errors throw new Error( @@ -423,7 +491,130 @@ async function validateMobileEnvironment( }; } -function validateBrowser( +// ============================================================================ +// APP AUTOMATE SECTION (Mobile devices for App Automate) +// ============================================================================ + +export async function validateAppAutomateDevices( + devices: Array>, +): Promise { + const validatedDevices: ValidatedEnvironment[] = []; + + if (!devices || devices.length === 0) { + // Use centralized default fallback + return [ + { + platform: "android", + osVersion: "latest", + deviceName: DEFAULTS.android.device, + }, + ]; + } + + let appAutomateData: RawDeviceData; + + try { + // Fetch app automate device data + appAutomateData = (await getDevicesAndBrowsers( + BrowserStackProducts.APP_AUTOMATE, + )) as RawDeviceData; + } catch (error) { + // Only wrap fetch-related errors + throw new Error( + `Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + for (const device of devices) { + // Parse device array in format ["android", "Device Name", "OS Version"] + const [platform, deviceName, osVersion] = device; + + // Find matching device in the data + let validatedDevice: ValidatedEnvironment | null = null; + + if (!appAutomateData.mobile) { + throw new Error("No mobile device data available"); + } + + // Filter by platform first + const platformGroup = appAutomateData.mobile.find( + (group) => group.os === platform.toLowerCase(), + ); + + if (!platformGroup) { + throw new Error(`Platform "${platform}" not supported for App Automate`); + } + + const platformDevices = platformGroup.devices; + + // Find exact device name match (case-insensitive) + const exactMatch = platformDevices.find( + (d) => d.display_name.toLowerCase() === deviceName.toLowerCase(), + ); + + if (exactMatch) { + // Check if the OS version is available for this device + const deviceVersions = platformDevices + .filter((d) => d.display_name === exactMatch.display_name) + .map((d) => d.os_version); + + const validatedOSVersion = resolveVersion( + osVersion || "latest", + deviceVersions, + ); + + validatedDevice = { + platform: platformGroup.os, + osVersion: validatedOSVersion, + deviceName: exactMatch.display_name, + }; + } + + if (!validatedDevice) { + // If no exact match found, suggest similar devices from the SAME platform only + const platformDevicesForSearch = platformDevices.map((d) => ({ + ...d, + platform: platformGroup.os, + })); + + // Try fuzzy search with a more lenient threshold + const deviceMatches = customFuzzySearch( + platformDevicesForSearch, + ["display_name"], + deviceName, + 5, + 0.8, // More lenient threshold + ); + + const suggestions = deviceMatches + .map((m) => `${m.display_name}`) + .join(", "); + + // If no fuzzy matches, show some available devices as fallback + const fallbackDevices = platformDevicesForSearch + .slice(0, 5) + .map((d) => d.display_name) + .join(", "); + + const errorMessage = suggestions + ? `Device "${deviceName}" not found for platform "${platform}".\nAvailable similar devices: ${suggestions}` + : `Device "${deviceName}" not found for platform "${platform}".\nAvailable devices: ${fallbackDevices}`; + + throw new Error(errorMessage); + } + + validatedDevices.push(validatedDevice); + } + + return validatedDevices; +} + +// ============================================================================ +// SHARED UTILITY FUNCTIONS +// ============================================================================ + +// Exact browser validation (preferred for structured fields) +function validateBrowserExact( requestedBrowser: string, availableBrowsers: string[], ): string { @@ -434,42 +625,7 @@ function validateBrowser( return exactMatch; } - const fuzzyMatches = customFuzzySearch( - availableBrowsers.map((b) => ({ browser: b })), - ["browser"], - requestedBrowser, - 1, - ); - - if (fuzzyMatches.length > 0) { - return fuzzyMatches[0].browser; - } - throw new Error( `Browser "${requestedBrowser}" not found. Available options: ${availableBrowsers.join(", ")}`, ); } - -function validateMacOSVersion(requested: string, available: string[]): string { - if (requested === "latest") { - return available[available.length - 1]; - } else if (requested === "oldest") { - return available[0]; - } else { - const fuzzy = customFuzzySearch( - available.map((v) => ({ os_version: v })), - ["os_version"], - requested, - 1, - ); - const matched = fuzzy.length ? fuzzy[0].os_version : requested; - - if (available.includes(matched)) { - return matched; - } else { - throw new Error( - `macOS version "${requested}" not found. Available options: ${available.join(", ")}`, - ); - } - } -} diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 1473ccc3..c170cd95 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -12,7 +12,7 @@ export const PlatformEnum = { MACOS: "macos", ANDROID: "android", IOS: "ios", -} as const; +} as const; export const WindowsPlatformEnum = { WINDOWS: "windows", diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index 18208cb5..a74cc0fa 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -108,9 +108,10 @@ export function runPercyWithBrowserstackSDK( const ymlInstructions = generateBrowserStackYMLInstructions({ // For now, feed a normalized summary string from devices for the comment - platforms: ((input as any).devices as string[][] | undefined)?.map((t) => - t.join(" "), - ) || [], + platforms: + ((input as any).devices as string[][] | undefined)?.map((t) => + t.join(" "), + ) || [], enablePercy: true, projectName: input.projectName, }); From 8da4c98aacd1ea9c74cfc4de9d1d373a83485ce9 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 19 Sep 2025 12:43:55 +0530 Subject: [PATCH 66/84] Advancements ++ --- src/lib/version-resolver.ts | 47 +------------------ .../appautomate-utils/appium-sdk/constants.ts | 6 +-- .../native-execution/constants.ts | 32 +++++++++++-- src/tools/sdk-utils/common/schema.ts | 3 +- 4 files changed, 33 insertions(+), 55 deletions(-) diff --git a/src/lib/version-resolver.ts b/src/lib/version-resolver.ts index aef01118..f20bac86 100644 --- a/src/lib/version-resolver.ts +++ b/src/lib/version-resolver.ts @@ -3,52 +3,7 @@ * Else if exact match, returns that * Else picks the numerically closest (or first) */ -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 - const nums = uniq - .map((v) => ({ v, n: parseFloat(v) })) - .filter((x) => !isNaN(x.n)) - .sort((a, b) => a.n - b.n); - if (nums.length) { - return requested === "latest" ? nums[nums.length - 1].v : nums[0].v; - } - // fallback lex - const lex = uniq.slice().sort(); - return requested === "latest" ? lex[lex.length - 1] : lex[0]; - } - - // exact? - if (uniq.includes(requested)) { - return requested; - } - - // try closest numeric - 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; - } - } - return best.v; - } - - // final fallback - return uniq[0]; -} - -export function resolveVersionWithFuzzy( +export function resolveVersion( requested: string, available: string[], ): string { diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index 34d5ad14..2a78e2b8 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -75,11 +75,9 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { ]), ) .max(3) - .default([ - [AppSDKSupportedPlatformEnum.android, "Samsung Galaxy S24", "latest"], - ]) + .default([]) .describe( - "Preferred input: 1-3 tuples describing target mobile devices. Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]", + "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 diff --git a/src/tools/appautomate-utils/native-execution/constants.ts b/src/tools/appautomate-utils/native-execution/constants.ts index 56e7c2d4..5e08a4b1 100644 --- a/src/tools/appautomate-utils/native-execution/constants.ts +++ b/src/tools/appautomate-utils/native-execution/constants.ts @@ -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.`; @@ -28,10 +29,35 @@ export const RUN_APP_AUTOMATE_SCHEMA = { " zip -r Tests.zip *.xctestrun *-Runner.app\n\n" + "If in other directory, provide existing test file path", ), - devices: z - .array(z.array(z.string())) + 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( - "List of devices to run the test on, e.g., [['android', 'Samsung Galaxy S20', '10.0'], ['ios', '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() diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index c170cd95..d433f358 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -19,7 +19,6 @@ export const WindowsPlatformEnum = { } as const; export const MacOSPlatformEnum = { - MAC: "mac", MACOS: "macos", } as const; @@ -99,7 +98,7 @@ export const RunTestsOnBrowserStackParamsShape = { .max(3) .default([]) .describe( - "Preferred input: 1-3 tuples describing target devices.Example: [['windows', '11', 'chrome', 'latest'], ['android', 'Samsung Galaxy S24', '14', 'chrome'], ['ios', 'iPhone 15', '17', 'safari']]", + "Preferred tuples of target devices.Add device only when user asks explicitly for it. Defaults to [] . Example: [['windows', '11', 'chrome', 'latest']]", ), }; From cacc90daf3e658a2fdb4fbe8056bb48692293f86 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 19 Sep 2025 13:41:07 +0530 Subject: [PATCH 67/84] Refactor Maven command construction in Java SDK utilities --- .../appium-sdk/languages/java.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/tools/appautomate-utils/appium-sdk/languages/java.ts b/src/tools/appautomate-utils/appium-sdk/languages/java.ts index 965f886f..96dc57fa 100644 --- a/src/tools/appautomate-utils/appium-sdk/languages/java.ts +++ b/src/tools/appautomate-utils/appium-sdk/languages/java.ts @@ -78,7 +78,7 @@ function getMavenCommandForWindows( accessKey: string, appPath?: string, ): string { - let command = ( + let command = `mvn archetype:generate -B ` + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + `-DarchetypeArtifactId="${mavenFramework}" ` + @@ -87,8 +87,7 @@ function getMavenCommandForWindows( `-DartifactId="${mavenFramework}" ` + `-Dversion="${version}" ` + `-DBROWSERSTACK_USERNAME="${username}" ` + - `-DBROWSERSTACK_ACCESS_KEY="${accessKey}"` - ); + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}"`; // Add framework parameter for browserstack-sdk-archetype-integrate if (mavenFramework === "browserstack-sdk-archetype-integrate") { @@ -111,7 +110,7 @@ function getMavenCommandForUnix( accessKey: string, appPath?: string, ): string { - let command = ( + let command = `mvn archetype:generate -B ` + `-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` + `-DarchetypeArtifactId="${mavenFramework}" ` + @@ -120,8 +119,7 @@ function getMavenCommandForUnix( `-DartifactId="${mavenFramework}" ` + `-Dversion="${version}" ` + `-DBROWSERSTACK_USERNAME="${username}" ` + - `-DBROWSERSTACK_ACCESS_KEY="${accessKey}"` - ); + `-DBROWSERSTACK_ACCESS_KEY="${accessKey}"`; // Add framework parameter for browserstack-sdk-archetype-integrate if (mavenFramework === "browserstack-sdk-archetype-integrate") { @@ -179,13 +177,21 @@ export function getJavaSDKCommand( const mavenStep = createStep( "Install BrowserStack SDK using Maven Archetype for App Automate", `Maven command for ${framework} (${getPlatformLabel()}): -\`\`\`bash -${mavenCommand} -\`\`\` + \`\`\`bash + ${mavenCommand} + \`\`\` + + Alternative setup for Gradle users: + ${GRADLE_APP_SETUP_INSTRUCTIONS}`, + ); -Alternative setup for Gradle users: -${GRADLE_APP_SETUP_INSTRUCTIONS}`, + const argsLineStep = createStep( + "Verifying dependency and argsLine", + `Verify browserstack-java-sdk with LATEST is added as dependency and add this line in pom.xml if not added: + \`\`\`xml + -javaagent:"\${com.browserstack:browserstack-java-sdk:jar}" + \`\`\``, ); - return combineInstructions(envStep, mavenStep); + return combineInstructions(envStep, mavenStep, argsLineStep); } From 7e3d7641c2e5be0e56bdf039f81d70fb7ac8d8d3 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 19 Sep 2025 15:20:28 +0530 Subject: [PATCH 68/84] Linting ++ --- src/lib/version-resolver.ts | 5 +-- .../appium-sdk/config-generator.ts | 3 +- .../appautomate-utils/appium-sdk/constants.ts | 37 ------------------- .../appautomate-utils/appium-sdk/handler.ts | 4 +- .../native-execution/constants.ts | 2 +- src/tools/sdk-utils/bstack/configUtils.ts | 28 ++++---------- src/tools/sdk-utils/percy-bstack/handler.ts | 1 - 7 files changed, 14 insertions(+), 66 deletions(-) diff --git a/src/lib/version-resolver.ts b/src/lib/version-resolver.ts index f20bac86..1530ae01 100644 --- a/src/lib/version-resolver.ts +++ b/src/lib/version-resolver.ts @@ -3,10 +3,7 @@ * Else if exact match, returns that * Else picks the numerically closest (or first) */ -export function resolveVersion( - requested: string, - available: string[], -): string { +export function resolveVersion(requested: string, available: string[]): string { // strip duplicates & sort const uniq = Array.from(new Set(available)); diff --git a/src/tools/appautomate-utils/appium-sdk/config-generator.ts b/src/tools/appautomate-utils/appium-sdk/config-generator.ts index 99bafe3e..959cf2d2 100644 --- a/src/tools/appautomate-utils/appium-sdk/config-generator.ts +++ b/src/tools/appautomate-utils/appium-sdk/config-generator.ts @@ -42,8 +42,9 @@ platforms: ${platformConfigs} parallelsPerPlatform: 1 browserstackLocal: true -buildName: ${buildName} +// TODO: replace projectName and buildName according to actual project projectName: ${projectName} +buildName: ${buildName} debug: true networkLogs: true percy: false diff --git a/src/tools/appautomate-utils/appium-sdk/constants.ts b/src/tools/appautomate-utils/appium-sdk/constants.ts index 2a78e2b8..5b9a2494 100644 --- a/src/tools/appautomate-utils/appium-sdk/constants.ts +++ b/src/tools/appautomate-utils/appium-sdk/constants.ts @@ -91,40 +91,3 @@ export const SETUP_APP_AUTOMATE_SCHEMA = { .default("BStack-AppAutomate-Suite") .describe("Project name for organizing test runs on BrowserStack."), }; - -export const SETUP_APP_AUTOMATE_SCHEMA_LEGACY = { - 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/handler.ts b/src/tools/appautomate-utils/appium-sdk/handler.ts index 195ac546..0a24358a 100644 --- a/src/tools/appautomate-utils/appium-sdk/handler.ts +++ b/src/tools/appautomate-utils/appium-sdk/handler.ts @@ -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, @@ -13,8 +15,6 @@ import { generateAppBrowserStackYMLInstructions, } from "./index.js"; -import { validateAppAutomateDevices } from "../../sdk-utils/common/device-validator.js"; - import { AppSDKSupportedLanguage, AppSDKSupportedTestingFramework, diff --git a/src/tools/appautomate-utils/native-execution/constants.ts b/src/tools/appautomate-utils/native-execution/constants.ts index 5e08a4b1..9239a432 100644 --- a/src/tools/appautomate-utils/native-execution/constants.ts +++ b/src/tools/appautomate-utils/native-execution/constants.ts @@ -29,7 +29,7 @@ export const RUN_APP_AUTOMATE_SCHEMA = { " zip -r Tests.zip *.xctestrun *-Runner.app\n\n" + "If in other directory, provide existing test file path", ), - devices: z + devices: z .array( z.union([ // Android: [android, deviceName, osVersion] diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index 1592368c..ac2b1a99 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -7,26 +7,22 @@ export function generateBrowserStackYMLInstructions(config: { projectName: string; }): string { const enablePercy = config.enablePercy || false; - const projectName = config.projectName; + const projectName = config.projectName || "BrowserStack Automate Build"; // Generate platform configurations using the utility function const platformConfigs = generatePlatformConfigs(config); - // Determine build name and step title - const buildName = - config.validatedEnvironments && config.validatedEnvironments.length > 0 - ? `${projectName}-Build` - : "Sample-Build"; - const stepTitle = - config.validatedEnvironments && config.validatedEnvironments.length > 0 - ? "Create a browserstack.yml file in the project root with your validated device configurations:" - : "Create a browserstack.yml file in the project root. The file should be in the following format:"; + "Create a browserstack.yml file in the project root with your validated device configurations:"; + + const buildName = `${projectName}-Build`; let ymlContent = ` # ====================== # BrowserStack Reporting # ====================== + +# TODO: Replace these sample values with your actual project details projectName: ${projectName} buildName: ${buildName} @@ -34,19 +30,10 @@ buildName: ${buildName} # Platforms (Browsers / Devices to test) # =======================================`; - if (config.validatedEnvironments && config.validatedEnvironments.length > 0) { - ymlContent += ` -# Auto-generated from validated device configurations -platforms: -${platformConfigs}`; - } else { - ymlContent += ` + ymlContent += ` # Platforms object contains all the browser / device combinations you want to test on. -# Generate this on the basis of the following platforms requested by the user: -# Requested platforms: ${config.platforms || []} platforms: ${platformConfigs}`; - } ymlContent += ` @@ -55,6 +42,7 @@ ${platformConfigs}`; # ======================= # The number of parallel threads to be used for each platform set. # BrowserStack's SDK runner will select the best strategy based on the configured value +# The number of parallel threads to be used for each platform set. parallelsPerPlatform: 1 # ================= diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index a74cc0fa..989c17a4 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -107,7 +107,6 @@ export function runPercyWithBrowserstackSDK( } const ymlInstructions = generateBrowserStackYMLInstructions({ - // For now, feed a normalized summary string from devices for the comment platforms: ((input as any).devices as string[][] | undefined)?.map((t) => t.join(" "), From dcd95f44f786937149455dcf61d98580f99f6d3d Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 24 Sep 2025 13:10:25 +0530 Subject: [PATCH 69/84] chore : bump version to 1.2.5 --- package-lock.json | 71 ++++++------------------------------------ package.json | 4 +-- src/tools/percy-sdk.ts | 2 +- 3 files changed, 13 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d005ca8..37a03f0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.4", + "version": "1.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@browserstack/mcp-server", - "version": "1.2.4", + "version": "1.2.5", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.4", + "@modelcontextprotocol/sdk": "^1.18.1", "@types/form-data": "^2.5.2", "axios": "^1.8.4", "browserstack-local": "^1.5.6", @@ -1125,16 +1125,17 @@ "dev": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.4.tgz", - "integrity": "sha512-OTbhe5slIjiOtLxXhKalkKGhIQrwvhgCDs/C2r8kcBTy5HR/g43aDQU0l7r8O0VGbJPTNJvDc7ZdQMdQDJXmbw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", + "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", "license": "MIT", "dependencies": { - "ajv": "^8.17.1", + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -1158,22 +1159,6 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -1288,12 +1273,6 @@ "node": ">=0.10.0" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -2402,7 +2381,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4073,8 +4051,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -4095,22 +4072,6 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -4995,8 +4956,7 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -6029,7 +5989,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -6174,15 +6133,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7161,7 +7111,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index 339558c0..19dd5c6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.4", + "version": "1.2.5", "description": "BrowserStack's Official MCP Server", "mcpName": "io.github.browserstack/mcp-server", "main": "dist/index.js", @@ -35,7 +35,7 @@ "author": "", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.4", + "@modelcontextprotocol/sdk": "^1.18.1", "@types/form-data": "^2.5.2", "axios": "^1.8.4", "browserstack-local": "^1.5.6", diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 2c12e0a8..f977ce82 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -58,7 +58,7 @@ export function registerPercyTools( }; }, ); - + tools.percyVisualTestIntegrationAgent = server.tool( "percyVisualTestIntegrationAgent", SIMULATE_PERCY_CHANGE_DESCRIPTION, From 047e6cdbc921d862a5cfead54856c3ca63b9a61a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:48:16 +0000 Subject: [PATCH 70/84] chore(deps): bump tar-fs from 3.0.9 to 3.1.1 Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.9 to 3.1.1. - [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.9...v3.1.1) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 3.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37a03f0b..f654e564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6830,9 +6830,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", From 4f44b9df701e85486162cf755ee5b811b3640b02 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 7 Oct 2025 23:19:29 +0530 Subject: [PATCH 71/84] refactor: change testId type from string to number across RCA-related modules --- src/tools/rca-agent-utils/constants.ts | 4 ++-- src/tools/rca-agent-utils/rca-data.ts | 8 ++++---- src/tools/rca-agent-utils/types.ts | 6 +++--- src/tools/rca-agent.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tools/rca-agent-utils/constants.ts b/src/tools/rca-agent-utils/constants.ts index c7a69c0f..5818733d 100644 --- a/src/tools/rca-agent-utils/constants.ts +++ b/src/tools/rca-agent-utils/constants.ts @@ -3,10 +3,10 @@ import { TestStatus } from "./types.js"; export const FETCH_RCA_PARAMS = { testId: z - .array(z.string()) + .array(z.number().int()) .max(3) .describe( - "Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed.", + "Array of integer test IDs to fetch RCA data for (maximum 3 IDs). These must be numeric test IDs, not session IDs or strings. If not provided, use the listTestIds tool to get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed.", ), }; diff --git a/src/tools/rca-agent-utils/rca-data.ts b/src/tools/rca-agent-utils/rca-data.ts index 1e66220e..de7949f5 100644 --- a/src/tools/rca-agent-utils/rca-data.ts +++ b/src/tools/rca-agent-utils/rca-data.ts @@ -97,11 +97,11 @@ async function updateProgress( } async function fetchInitialRCA( - testId: string, + testId: number, headers: Record, baseUrl: string, ): Promise { - const url = baseUrl.replace("{testId}", testId); + const url = baseUrl.replace("{testId}", testId.toString()); try { const response = await fetch(url, { headers }); @@ -179,7 +179,7 @@ async function pollRCAResults( await Promise.allSettled( inProgressCases.map(async (tc) => { try { - const pollUrl = baseUrl.replace("{testId}", tc.id); + const pollUrl = baseUrl.replace("{testId}", tc.id.toString()); const response = await fetch(pollUrl, { headers }); if (!response.ok) { const errorText = await response.text(); @@ -240,7 +240,7 @@ async function pollRCAResults( } export async function getRCAData( - testIds: string[], + testIds: number[], authString: string, context?: ScanProgressContext, ): Promise { diff --git a/src/tools/rca-agent-utils/types.ts b/src/tools/rca-agent-utils/types.ts index b2795dd7..41294d21 100644 --- a/src/tools/rca-agent-utils/types.ts +++ b/src/tools/rca-agent-utils/types.ts @@ -21,7 +21,7 @@ export interface TestRun { } export interface FailedTestInfo { - test_id: string; + test_id: number; test_name: string; } @@ -39,8 +39,8 @@ export enum RCAState { } export interface RCATestCase { - id: string; - testRunId: string; + id: number; + testRunId: number; state: RCAState; rcaData?: any; } diff --git a/src/tools/rca-agent.ts b/src/tools/rca-agent.ts index 4b341e43..920828f1 100644 --- a/src/tools/rca-agent.ts +++ b/src/tools/rca-agent.ts @@ -60,7 +60,7 @@ export async function getBuildIdTool( // Tool function that fetches RCA data export async function fetchRCADataTool( - args: { testId: string[] }, + args: { testId: number[] }, config: BrowserStackConfig, ): Promise { try { From c31ee989ba0d6a53690eafb4c3bb6e64e0c2f372 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 10 Oct 2025 17:15:02 +0530 Subject: [PATCH 72/84] Adding mcp registry workflow --- .github/workflows/mcp-registry-publish.yml | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/mcp-registry-publish.yml diff --git a/.github/workflows/mcp-registry-publish.yml b/.github/workflows/mcp-registry-publish.yml new file mode 100644 index 00000000..3dcafe7b --- /dev/null +++ b/.github/workflows/mcp-registry-publish.yml @@ -0,0 +1,41 @@ +name: Publish to MCP Registry + +on: + workflow_dispatch: + push: + tags: ["v*"] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "lts/*" + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test --if-present + + - name: Build package + run: npm run build --if-present + + - name: Install MCP Publisher + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/download/latest/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + + - name: Login to MCP Registry + run: ./mcp-publisher login github-oidc + + - name: Publish to MCP Registry + run: ./mcp-publisher publish From 828370d6c8053aad384fcb309292ca54d3ae540c Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 13 Oct 2025 19:58:31 +0530 Subject: [PATCH 73/84] fix: Enhance ApiClient to support responseType in requests --- src/lib/apiClient.ts | 3 +++ src/tools/automate-utils/fetch-screenshots.ts | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 97287b0c..fc6186ab 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -12,6 +12,7 @@ type RequestOptions = { params?: Record; body?: any; timeout?: number; + responseType?: AxiosRequestConfig["responseType"]; raise_error?: boolean; // default: true }; @@ -163,12 +164,14 @@ class ApiClient { headers, params, timeout, + responseType, raise_error = true, }: RequestOptions): Promise> { const config: AxiosRequestConfig = { headers, params, timeout, + responseType, httpsAgent: this.axiosAgent, }; return this.requestWrapper( diff --git a/src/tools/automate-utils/fetch-screenshots.ts b/src/tools/automate-utils/fetch-screenshots.ts index 5574f0bc..935ec4a0 100644 --- a/src/tools/automate-utils/fetch-screenshots.ts +++ b/src/tools/automate-utils/fetch-screenshots.ts @@ -59,7 +59,10 @@ async function convertUrlsToBase64( ): Promise> { const screenshots = await Promise.all( urls.map(async (url) => { - const response = await apiClient.get({ url }); + const response = await apiClient.get({ + url, + responseType: "arraybuffer" + }); // Axios returns response.data as a Buffer for binary data const base64 = Buffer.from(response.data).toString("base64"); From e17c31b78845000b91397b891c35bf067b4c445c Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 14 Oct 2025 01:58:43 +0530 Subject: [PATCH 74/84] feat: Enhance test file handling by supporting specific file paths and improving validation --- src/index.ts | 1 + src/lib/inmemory-store.ts | 9 +++ src/tools/automate-utils/fetch-screenshots.ts | 6 +- src/tools/list-test-files.ts | 55 ++++++++++++++----- src/tools/percy-sdk.ts | 12 ++-- src/tools/percy-snapshot-utils/constants.ts | 16 ------ src/tools/sdk-utils/common/schema.ts | 7 +++ src/tools/sdk-utils/common/utils.ts | 19 ++++++- src/tools/sdk-utils/handler.ts | 24 +++++++- 9 files changed, 103 insertions(+), 46 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6a17511b..ef48938d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,3 +51,4 @@ process.on("exit", () => { export { setLogger } from "./logger.js"; export { BrowserStackMcpServer } from "./server-factory.js"; export { trackMCP } from "./lib/instrumentation.js"; +export const PackageJsonVersion = packageJson.version; diff --git a/src/lib/inmemory-store.ts b/src/lib/inmemory-store.ts index 0c09d201..f1161653 100644 --- a/src/lib/inmemory-store.ts +++ b/src/lib/inmemory-store.ts @@ -1,2 +1,11 @@ export const signedUrlMap = new Map(); export const testFilePathsMap = new Map(); + +let _storedPercyResults: any = null; + +export const storedPercyResults = { + get: () => _storedPercyResults, + set: (value: any) => { + _storedPercyResults = value; + }, +}; diff --git a/src/tools/automate-utils/fetch-screenshots.ts b/src/tools/automate-utils/fetch-screenshots.ts index 935ec4a0..46eff517 100644 --- a/src/tools/automate-utils/fetch-screenshots.ts +++ b/src/tools/automate-utils/fetch-screenshots.ts @@ -59,9 +59,9 @@ async function convertUrlsToBase64( ): Promise> { const screenshots = await Promise.all( urls.map(async (url) => { - const response = await apiClient.get({ - url, - responseType: "arraybuffer" + const response = await apiClient.get({ + url, + responseType: "arraybuffer", }); // Axios returns response.data as a Buffer for binary data const base64 = Buffer.from(response.data).toString("base64"); diff --git a/src/tools/list-test-files.ts b/src/tools/list-test-files.ts index d3fea93b..8a931e21 100644 --- a/src/tools/list-test-files.ts +++ b/src/tools/list-test-files.ts @@ -1,32 +1,57 @@ import { listTestFiles } from "./percy-snapshot-utils/detect-test-files.js"; -import { testFilePathsMap } from "../lib/inmemory-store.js"; +import { testFilePathsMap, storedPercyResults } from "../lib/inmemory-store.js"; +import { updateFileAndStep } from "./percy-snapshot-utils/utils.js"; +import { percyWebSetupInstructions } from "./sdk-utils/percy-web/handler.js"; import crypto from "crypto"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -export async function addListTestFiles(args: any): Promise { - const { dirs, language, framework } = args; - let testFiles: string[] = []; - - if (!dirs || dirs.length === 0) { +export async function addListTestFiles(): Promise { + const storedResults = storedPercyResults.get(); + if (!storedResults) { throw new Error( - "No directories provided to add the test files. Please provide test directories to add percy snapshot commands.", + "No Framework details found. Please call expandPercyVisualTesting first to fetch the framework details.", ); } - for (const dir of dirs) { - const files = await listTestFiles({ - language, - framework, - baseDir: dir, - }); + const language = storedResults.detectedLanguage; + const framework = storedResults.detectedTestingFramework; + + // Use stored paths from setUpPercy + const dirs = storedResults.folderPaths; + const files = storedResults.filePaths; + + let testFiles: string[] = []; + + if (files && files.length > 0) { testFiles = testFiles.concat(files); } + if (dirs && dirs.length > 0) { + for (const dir of dirs) { + const discoveredFiles = await listTestFiles({ + language, + framework, + baseDir: dir, + }); + testFiles = testFiles.concat(discoveredFiles); + } + } + + // Validate that we have at least one test file if (testFiles.length === 0) { - throw new Error("No test files found"); + throw new Error( + "No test files found. Please provide either specific file paths (files) or directory paths (dirs) containing test files.", + ); + } + + if (testFiles.length === 1) { + const result = await updateFileAndStep(testFiles[0],0,1,percyWebSetupInstructions); + return { + content: result, + }; } - // Generate a UUID and store the test files in memory + // For multiple files, use the UUID workflow const uuid = crypto.randomUUID(); testFilePathsMap.set(uuid, testFiles); diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index f977ce82..dac7626a 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -18,11 +18,7 @@ import { PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, SIMULATE_PERCY_CHANGE_DESCRIPTION, } from "./sdk-utils/common/constants.js"; - -import { - ListTestFilesParamsShape, - UpdateTestFileWithInstructionsParams, -} from "./percy-snapshot-utils/constants.js"; +import { UpdateTestFileWithInstructionsParams } from "./percy-snapshot-utils/constants.js"; import { RunPercyScanParamsShape, @@ -126,11 +122,11 @@ export function registerPercyTools( tools.listTestFiles = server.tool( "listTestFiles", LIST_TEST_FILES_DESCRIPTION, - ListTestFilesParamsShape, - async (args) => { + {}, + async () => { try { trackMCP("listTestFiles", server.server.getClientVersion()!, config); - return addListTestFiles(args); + return addListTestFiles(); } catch (error) { return handleMCPError("listTestFiles", server, config, error); } diff --git a/src/tools/percy-snapshot-utils/constants.ts b/src/tools/percy-snapshot-utils/constants.ts index ef77a419..82d2fc6a 100644 --- a/src/tools/percy-snapshot-utils/constants.ts +++ b/src/tools/percy-snapshot-utils/constants.ts @@ -1,8 +1,4 @@ import { z } from "zod"; -import { - SDKSupportedLanguages, - SDKSupportedTestingFrameworks, -} from "../sdk-utils/common/types.js"; import { SDKSupportedLanguage } from "../sdk-utils/common/types.js"; import { DetectionConfig } from "./types.js"; @@ -13,18 +9,6 @@ export const UpdateTestFileWithInstructionsParams = { index: z.number().describe("Index of the test file to update"), }; -export const ListTestFilesParamsShape = { - dirs: z - .array(z.string()) - .describe("Array of directory paths to search for test files"), - language: z - .enum(SDKSupportedLanguages as [string, ...string[]]) - .describe("Programming language"), - framework: z - .enum(SDKSupportedTestingFrameworks as [string, ...string[]]) - .describe("Testing framework (optional)"), -}; - export const TEST_FILE_DETECTION: Record< SDKSupportedLanguage, DetectionConfig diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index d433f358..39c5cb4f 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -36,9 +36,16 @@ export const SetUpPercyParamsShape = { ), folderPaths: z .array(z.string()) + .optional() .describe( "An array of absolute folder paths containing UI test files. If not provided, analyze codebase for UI test folders by scanning for test patterns which contain UI test cases as per framework. Return empty array if none found.", ), + filePaths: z + .array(z.string()) + .optional() + .describe( + "An array of absolute file paths to specific UI test files. Use this when you want to target specific test files rather than entire folders. If not provided, will use folderPaths instead.", + ), }; export const RunTestsOnBrowserStackParamsShape = { diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts index 0bb433af..ba167c1a 100644 --- a/src/tools/sdk-utils/common/utils.ts +++ b/src/tools/sdk-utils/common/utils.ts @@ -11,6 +11,7 @@ import { PercyAutomateNotImplementedType, } from "./types.js"; import { IMPORTANT_SETUP_WARNING } from "./index.js"; +import { PackageJsonVersion } from "../../../index.js"; export function checkPercyIntegrationSupport(input: { integrationType: string; @@ -108,14 +109,14 @@ export function getPercyAutomateNotImplementedMessage( export function getBootstrapFailedMessage( error: unknown, - context: { config: unknown; percyMode?: string; sdkVersion?: string }, + context: { config: unknown; percyMode?: string }, ): string { const error_message = error instanceof Error ? error.message : "unknown error"; return `Failed to bootstrap project with BrowserStack SDK. Error: ${error_message} Percy Mode: ${context.percyMode ?? "automate"} -SDK Version: ${context.sdkVersion ?? "N/A"} +MCP Version: ${PackageJsonVersion} Please open an issue on GitHub if the problem persists.`; } @@ -136,3 +137,17 @@ export function percyUnsupportedResult( shouldSkipFormatting: true, }; } + +export function validatePercyPathandFolders(input: any): void { + const hasFolderPaths = input.folderPaths && input.folderPaths.length > 0; + const hasFilePaths = input.filePaths && input.filePaths.length > 0; + + if (!hasFolderPaths && !hasFilePaths) { + throw new Error( + "Please provide either:\n" + + "• folderPaths: Array of directory paths containing test files\n" + + "• filePaths: Array of specific test file paths\n\n" + + "Example: { filePaths: ['/path/to/test.spec.js'] }", + ); + } +} diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 8e09a7d5..ac6b4987 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -8,11 +8,15 @@ import { runPercyWeb } from "./percy-web/handler.js"; import { runPercyAutomateOnly } from "./percy-automate/handler.js"; import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js"; -import { checkPercyIntegrationSupport } from "./common/utils.js"; +import { + checkPercyIntegrationSupport, + validatePercyPathandFolders, +} from "./common/utils.js"; import { SetUpPercySchema, RunTestsOnBrowserStackSchema, } from "./common/schema.js"; +import { storedPercyResults } from "../../lib/inmemory-store.js"; import { getBootstrapFailedMessage, percyUnsupportedResult, @@ -39,8 +43,23 @@ export async function setUpPercyHandler( ): Promise { try { const input = SetUpPercySchema.parse(rawInput); + validatePercyPathandFolders(input); + + storedPercyResults.set({ + detectedLanguage: input.detectedLanguage, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, + detectedTestingFramework: input.detectedTestingFramework, + integrationType: input.integrationType, + folderPaths: input.folderPaths || [], + filePaths: input.filePaths || [], + }); + const authorization = getBrowserStackAuth(config); + const folderPaths = input.folderPaths || []; + const filePaths = input.filePaths || []; + const percyInput = { projectName: input.projectName, detectedLanguage: input.detectedLanguage, @@ -48,7 +67,8 @@ export async function setUpPercyHandler( input.detectedBrowserAutomationFramework, detectedTestingFramework: input.detectedTestingFramework, integrationType: input.integrationType, - folderPaths: input.folderPaths || [], + folderPaths, + filePaths, }; // Check for Percy Web integration support From 9340833c3f265fc5d4944d94b4a6a9c10a35f335 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 15 Oct 2025 01:48:44 +0530 Subject: [PATCH 75/84] feat: Refactor in-memory storage for Percy results and enhance test file management --- src/lib/inmemory-store.ts | 4 +- src/tools/add-percy-snapshots.ts | 15 +++- src/tools/list-test-files.ts | 22 ++++- src/tools/run-percy-scan.ts | 95 +++++++++++++++++----- src/tools/sdk-utils/handler.ts | 5 ++ src/tools/sdk-utils/percy-web/constants.ts | 48 +++++++++++ 6 files changed, 163 insertions(+), 26 deletions(-) diff --git a/src/lib/inmemory-store.ts b/src/lib/inmemory-store.ts index f1161653..1f3ac90c 100644 --- a/src/lib/inmemory-store.ts +++ b/src/lib/inmemory-store.ts @@ -1,5 +1,4 @@ export const signedUrlMap = new Map(); -export const testFilePathsMap = new Map(); let _storedPercyResults: any = null; @@ -8,4 +7,7 @@ export const storedPercyResults = { set: (value: any) => { _storedPercyResults = value; }, + clear: () => { + _storedPercyResults = null; + }, }; diff --git a/src/tools/add-percy-snapshots.ts b/src/tools/add-percy-snapshots.ts index 237ae55c..30e91005 100644 --- a/src/tools/add-percy-snapshots.ts +++ b/src/tools/add-percy-snapshots.ts @@ -1,4 +1,4 @@ -import { testFilePathsMap } from "../lib/inmemory-store.js"; +import { storedPercyResults } from "../lib/inmemory-store.js"; import { updateFileAndStep } from "./percy-snapshot-utils/utils.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js"; @@ -8,17 +8,21 @@ export async function updateTestsWithPercyCommands(args: { index: number; }): Promise { const { uuid, index } = args; - const filePaths = testFilePathsMap.get(uuid); + const stored = storedPercyResults.get(); - if (!filePaths) { + if (!stored || !stored.uuid || stored.uuid !== uuid || !stored[uuid]) { throw new Error(`No test files found in memory for UUID: ${uuid}`); } + const fileStatusMap = stored[uuid]; + const filePaths = Object.keys(fileStatusMap); + if (index < 0 || index >= filePaths.length) { throw new Error( `Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`, ); } + const result = await updateFileAndStep( filePaths[index], index, @@ -26,6 +30,11 @@ export async function updateTestsWithPercyCommands(args: { percyWebSetupInstructions, ); + // Mark this file as updated (true) in the unified structure + const updatedStored = { ...stored }; + updatedStored[uuid][filePaths[index]] = true; // true = updated + storedPercyResults.set(updatedStored); + return { content: result, }; diff --git a/src/tools/list-test-files.ts b/src/tools/list-test-files.ts index 8a931e21..76896c0f 100644 --- a/src/tools/list-test-files.ts +++ b/src/tools/list-test-files.ts @@ -1,5 +1,5 @@ import { listTestFiles } from "./percy-snapshot-utils/detect-test-files.js"; -import { testFilePathsMap, storedPercyResults } from "../lib/inmemory-store.js"; +import { storedPercyResults } from "../lib/inmemory-store.js"; import { updateFileAndStep } from "./percy-snapshot-utils/utils.js"; import { percyWebSetupInstructions } from "./sdk-utils/percy-web/handler.js"; import crypto from "crypto"; @@ -45,7 +45,12 @@ export async function addListTestFiles(): Promise { } if (testFiles.length === 1) { - const result = await updateFileAndStep(testFiles[0],0,1,percyWebSetupInstructions); + const result = await updateFileAndStep( + testFiles[0], + 0, + 1, + percyWebSetupInstructions, + ); return { content: result, }; @@ -53,7 +58,18 @@ export async function addListTestFiles(): Promise { // For multiple files, use the UUID workflow const uuid = crypto.randomUUID(); - testFilePathsMap.set(uuid, testFiles); + + // Store files in the unified structure with initial status false (not updated) + const fileStatusMap: { [key: string]: boolean } = {}; + testFiles.forEach((file) => { + fileStatusMap[file] = false; // false = not updated, true = updated + }); + + // Update storedPercyResults with single UUID for the project + const updatedStored = { ...storedResults }; + updatedStored.uuid = uuid; // Store the UUID reference + updatedStored[uuid] = fileStatusMap; // Store files under the UUID key + storedPercyResults.set(updatedStored); return { content: [ diff --git a/src/tools/run-percy-scan.ts b/src/tools/run-percy-scan.ts index 3db5a6fb..2157ad41 100644 --- a/src/tools/run-percy-scan.ts +++ b/src/tools/run-percy-scan.ts @@ -3,6 +3,12 @@ import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js"; import { BrowserStackConfig } from "../lib/types.js"; import { getBrowserStackAuth } from "../lib/get-auth.js"; import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js"; +import { storedPercyResults } from "../lib/inmemory-store.js"; +import { + getFrameworkTestCommand, + PERCY_FALLBACK_STEPS, +} from "./sdk-utils/percy-web/constants.js"; +import path from "path"; export async function runPercyScan( args: { @@ -18,25 +24,22 @@ export async function runPercyScan( type: integrationType, }); - const steps: string[] = [generatePercyTokenInstructions(percyToken)]; - - if (instruction) { - steps.push( - `Use the provided test command with Percy:\n${instruction}`, - `If this command fails or is incorrect, fall back to the default approach below.`, - ); - } - - steps.push( - `Attempt to infer the project's test command from context (high confidence commands first): - - Java → mvn test - - Python → pytest - - Node.js → npm test or yarn test - - Cypress → cypress run - or from package.json scripts`, - `Wrap the inferred command with Percy along with label: \nnpx percy exec --labels=mcp -- `, - `If the test command cannot be inferred confidently, ask the user directly for the correct test command.`, - ); + // Check if we have stored data and project matches + const stored = storedPercyResults.get(); + + // Compute if we have updated files to run + const hasUpdatedFiles = checkForUpdatedFiles(stored, projectName); + const updatedFiles = hasUpdatedFiles ? getUpdatedFiles(stored) : []; + + // Build steps array with conditional spread + const steps = [ + generatePercyTokenInstructions(percyToken), + ...(hasUpdatedFiles ? generateUpdatedFilesSteps(stored, updatedFiles) : []), + ...(instruction && !hasUpdatedFiles + ? generateInstructionSteps(instruction) + : []), + ...(!hasUpdatedFiles ? PERCY_FALLBACK_STEPS : []), + ]; const instructionContext = steps .map((step, index) => `${index + 1}. ${step}`) @@ -59,3 +62,57 @@ export PERCY_TOKEN="${percyToken}" (For Windows: use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)`; } + +const toAbs = (p: string): string | undefined => + p ? path.resolve(p) : undefined; + +function checkForUpdatedFiles( + stored: any, // storedPercyResults structure + projectName: string, +): boolean { + const projectMatches = stored?.projectName === projectName; + return ( + projectMatches && + stored?.uuid && + stored[stored.uuid] && + Object.values(stored[stored.uuid]).some((status) => status === true) + ); +} + +function getUpdatedFiles(stored: any): string[] { + const updatedFiles: string[] = []; + const fileStatusMap = stored[stored.uuid!]; + + Object.entries(fileStatusMap).forEach(([filePath, status]) => { + if (status === true) { + updatedFiles.push(filePath); + } + }); + + return updatedFiles; +} + +function generateUpdatedFilesSteps( + stored: any, + updatedFiles: string[], +): string[] { + const filesToRun = updatedFiles.map(toAbs).filter(Boolean) as string[]; + const { detectedLanguage, detectedTestingFramework } = stored; + const exampleCommand = getFrameworkTestCommand( + detectedLanguage, + detectedTestingFramework, + ); + + return [ + `Run only the updated files with Percy:\n` + + `Example: ${exampleCommand} -- ...`, + `Updated files to run:\n${filesToRun.join("\n")}`, + ]; +} + +function generateInstructionSteps(instruction: string): string[] { + return [ + `Use the provided test command with Percy:\n${instruction}`, + `If this command fails or is incorrect, fall back to the default approach below.`, + ]; +} diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index ac6b4987..e9dc6f84 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -45,7 +45,11 @@ export async function setUpPercyHandler( const input = SetUpPercySchema.parse(rawInput); validatePercyPathandFolders(input); + // Clear any previous Percy results for a fresh start + storedPercyResults.clear(); + storedPercyResults.set({ + projectName: input.projectName, detectedLanguage: input.detectedLanguage, detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, @@ -53,6 +57,7 @@ export async function setUpPercyHandler( integrationType: input.integrationType, folderPaths: input.folderPaths || [], filePaths: input.filePaths || [], + uuid: null, }); const authorization = getBrowserStackAuth(config); diff --git a/src/tools/sdk-utils/percy-web/constants.ts b/src/tools/sdk-utils/percy-web/constants.ts index 44728a0f..fbbb2b89 100644 --- a/src/tools/sdk-utils/percy-web/constants.ts +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -921,3 +921,51 @@ ${csharpPlaywrightInstructionsSnapshot} To run the Percy build, call the tool runPercyScan with the appropriate test command (e.g. npx percy exec -- ). ${percyReviewSnapshotsStep} `; + +export function getFrameworkTestCommand( + language: string, + framework: string, +): string { + const percyPrefix = "npx percy exec --labels=mcp --"; + + const nodeCommands: Record = { + cypress: "cypress run", + playwright: "playwright test", + webdriverio: "wdio", + puppeteer: "node", + testcafe: "testcafe", + nightwatch: "nightwatch", + protractor: "protractor", + gatsby: "gatsby build", + storybook: "storybook build", + ember: "ember test", + default: "npm test", + }; + + const languageMap: Record = { + python: "python -m pytest", + java: "mvn test", + ruby: "bundle exec rspec", + csharp: "dotnet test", + }; + + if (language === "nodejs") { + const cmd = nodeCommands[framework] ?? nodeCommands.default; + return `${percyPrefix} ${cmd}`; + } + + const cmd = languageMap[language]; + return cmd ? `${percyPrefix} ${cmd}` : `${percyPrefix} `; +} + +export const PERCY_FALLBACK_STEPS = [ + `Attempt to infer the project's test command from context (high confidence commands first): +- Node.js: npm test, cypress run, npx playwright test, npx wdio, npx testcafe, npx nightwatch, npx protractor, ember test, npx gatsby build, npx storybook build +- Python: python -m pytest +- Java: mvn test +- Ruby: bundle exec rspec +- C#: dotnet test +or from package.json scripts`, + `Wrap the inferred command with Percy along with label: \nnpx percy exec --labels=mcp -- `, + `If the test command cannot be inferred confidently, ask the user directly for the correct test command.`, +]; From 66803a06211e8433a37a207e346421cb2953b65a Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 15 Oct 2025 02:32:24 +0530 Subject: [PATCH 76/84] Simplify in-memory test file management by removing UUID references --- src/tools/add-percy-snapshots.ts | 17 ++++++++--------- src/tools/list-test-files.ts | 15 +++++---------- src/tools/percy-snapshot-utils/constants.ts | 3 --- src/tools/run-percy-scan.ts | 9 ++++----- src/tools/sdk-utils/common/constants.ts | 2 +- src/tools/sdk-utils/handler.ts | 2 +- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/tools/add-percy-snapshots.ts b/src/tools/add-percy-snapshots.ts index 30e91005..2642a582 100644 --- a/src/tools/add-percy-snapshots.ts +++ b/src/tools/add-percy-snapshots.ts @@ -4,22 +4,22 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js"; export async function updateTestsWithPercyCommands(args: { - uuid: string; index: number; }): Promise { - const { uuid, index } = args; + const { index } = args; const stored = storedPercyResults.get(); - - if (!stored || !stored.uuid || stored.uuid !== uuid || !stored[uuid]) { - throw new Error(`No test files found in memory for UUID: ${uuid}`); + if (!stored || !stored.testFiles) { + throw new Error( + `No test files found in memory. Please call listTestFiles first.`, + ); } - const fileStatusMap = stored[uuid]; + const fileStatusMap = stored.testFiles; const filePaths = Object.keys(fileStatusMap); if (index < 0 || index >= filePaths.length) { throw new Error( - `Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`, + `Invalid index: ${index}. There are ${filePaths.length} files available.`, ); } @@ -30,9 +30,8 @@ export async function updateTestsWithPercyCommands(args: { percyWebSetupInstructions, ); - // Mark this file as updated (true) in the unified structure const updatedStored = { ...stored }; - updatedStored[uuid][filePaths[index]] = true; // true = updated + updatedStored.testFiles[filePaths[index]] = true; // true = updated storedPercyResults.set(updatedStored); return { diff --git a/src/tools/list-test-files.ts b/src/tools/list-test-files.ts index 76896c0f..46374636 100644 --- a/src/tools/list-test-files.ts +++ b/src/tools/list-test-files.ts @@ -2,7 +2,6 @@ import { listTestFiles } from "./percy-snapshot-utils/detect-test-files.js"; import { storedPercyResults } from "../lib/inmemory-store.js"; import { updateFileAndStep } from "./percy-snapshot-utils/utils.js"; import { percyWebSetupInstructions } from "./sdk-utils/percy-web/handler.js"; -import crypto from "crypto"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; export async function addListTestFiles(): Promise { @@ -56,30 +55,26 @@ export async function addListTestFiles(): Promise { }; } - // For multiple files, use the UUID workflow - const uuid = crypto.randomUUID(); - - // Store files in the unified structure with initial status false (not updated) + // For multiple files, store directly in testFiles const fileStatusMap: { [key: string]: boolean } = {}; testFiles.forEach((file) => { fileStatusMap[file] = false; // false = not updated, true = updated }); - // Update storedPercyResults with single UUID for the project + // Update storedPercyResults with test files const updatedStored = { ...storedResults }; - updatedStored.uuid = uuid; // Store the UUID reference - updatedStored[uuid] = fileStatusMap; // Store files under the UUID key + updatedStored.testFiles = fileStatusMap; storedPercyResults.set(updatedStored); return { content: [ { type: "text", - text: `The Test files are stored in memory with id ${uuid} and the total number of tests files found is ${testFiles.length}. You can use this UUID to retrieve the tests file paths later.`, + text: `The Test files are stored in memory and the total number of tests files found is ${testFiles.length}.`, }, { type: "text", - text: `You can now use the tool addPercySnapshotCommands to update the test file with Percy commands for visual testing with the UUID ${uuid}`, + text: `You can now use the tool addPercySnapshotCommands to update the test file with Percy commands for visual testing.`, }, ], }; diff --git a/src/tools/percy-snapshot-utils/constants.ts b/src/tools/percy-snapshot-utils/constants.ts index 82d2fc6a..a76f4426 100644 --- a/src/tools/percy-snapshot-utils/constants.ts +++ b/src/tools/percy-snapshot-utils/constants.ts @@ -3,9 +3,6 @@ import { SDKSupportedLanguage } from "../sdk-utils/common/types.js"; import { DetectionConfig } from "./types.js"; export const UpdateTestFileWithInstructionsParams = { - uuid: z - .string() - .describe("UUID referencing the in-memory array of test file paths"), index: z.number().describe("Index of the test file to update"), }; diff --git a/src/tools/run-percy-scan.ts b/src/tools/run-percy-scan.ts index 2157ad41..6302dc1a 100644 --- a/src/tools/run-percy-scan.ts +++ b/src/tools/run-percy-scan.ts @@ -73,15 +73,14 @@ function checkForUpdatedFiles( const projectMatches = stored?.projectName === projectName; return ( projectMatches && - stored?.uuid && - stored[stored.uuid] && - Object.values(stored[stored.uuid]).some((status) => status === true) + stored?.testFiles && + Object.values(stored.testFiles).some((status) => status === true) ); } function getUpdatedFiles(stored: any): string[] { const updatedFiles: string[] = []; - const fileStatusMap = stored[stored.uuid!]; + const fileStatusMap = stored.testFiles; Object.entries(fileStatusMap).forEach(([filePath, status]) => { if (status === true) { @@ -105,7 +104,7 @@ function generateUpdatedFilesSteps( return [ `Run only the updated files with Percy:\n` + - `Example: ${exampleCommand} -- ...`, + `Example: ${exampleCommand} ...`, `Updated files to run:\n${filesToRun.join("\n")}`, ]; } diff --git a/src/tools/sdk-utils/common/constants.ts b/src/tools/sdk-utils/common/constants.ts index bd29767d..8898a298 100644 --- a/src/tools/sdk-utils/common/constants.ts +++ b/src/tools/sdk-utils/common/constants.ts @@ -19,7 +19,7 @@ export const PERCY_REPLACE_REGEX = /Invoke listTestFiles\(\) with the provided directories[\s\S]*?- DO NOT STOP until you add commands in all the files or you reach end of the files\./; export const PERCY_SNAPSHOT_INSTRUCTION = ` -Invoke listTestFiles() with the provided directories from user to gather all test files in memory and obtain the generated UUID ---STEP--- +Invoke listTestFiles() with the provided directories from user to gather all test files in memory ---STEP--- Process files in STRICT sequential order using tool addPercySnapshotCommands() with below instructions: - Start with index 0 - Then index 1 diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index e9dc6f84..317e2310 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -57,7 +57,7 @@ export async function setUpPercyHandler( integrationType: input.integrationType, folderPaths: input.folderPaths || [], filePaths: input.filePaths || [], - uuid: null, + testFiles: {}, }); const authorization = getBrowserStackAuth(config); From c468b36425a7ead4a82e7be20731e9153550f636 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 15 Oct 2025 15:00:34 +0530 Subject: [PATCH 77/84] refactor: Update Python instructions --- src/tools/sdk-utils/bstack/constants.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/tools/sdk-utils/bstack/constants.ts b/src/tools/sdk-utils/bstack/constants.ts index 161be95e..22fc9a94 100644 --- a/src/tools/sdk-utils/bstack/constants.ts +++ b/src/tools/sdk-utils/bstack/constants.ts @@ -12,13 +12,6 @@ Install the BrowserStack SDK: \`\`\`bash python3 -m pip install browserstack-sdk \`\`\` - ----STEP--- - -Setup the BrowserStack SDK with your credentials: -\`\`\`bash -browserstack-sdk setup --username "${username}" --key "${accessKey}" -\`\`\` `; const run = ` @@ -26,7 +19,12 @@ browserstack-sdk setup --username "${username}" --key "${accessKey}" Run your tests on BrowserStack: \`\`\`bash -browserstack-sdk python +browserstack-sdk pytest -s tests/.py +\`\`\` + +Or run all tests in a directory: +\`\`\`bash +browserstack-sdk pytest \`\`\` `; @@ -43,13 +41,6 @@ Install the BrowserStack SDK: \`\`\`bash python3 -m pip install browserstack-sdk \`\`\` - ----STEP--- - -Setup the BrowserStack SDK with framework-specific configuration: -\`\`\`bash -browserstack-sdk setup --framework "${framework}" --username "${username}" --key "${accessKey}" -\`\`\` `; const run = ` @@ -655,4 +646,4 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { cypress: { instructions: cypressInstructions }, }, }, -}; +}; \ No newline at end of file From aedc11b424e6444d25c64230faa34298e0b0e5f4 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 15 Oct 2025 15:03:08 +0530 Subject: [PATCH 78/84] refactor: Simplify pythonInstructions by removing unused parameters --- src/tools/automate-utils/fetch-screenshots.ts | 6 +++--- src/tools/sdk-utils/bstack/constants.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tools/automate-utils/fetch-screenshots.ts b/src/tools/automate-utils/fetch-screenshots.ts index 935ec4a0..46eff517 100644 --- a/src/tools/automate-utils/fetch-screenshots.ts +++ b/src/tools/automate-utils/fetch-screenshots.ts @@ -59,9 +59,9 @@ async function convertUrlsToBase64( ): Promise> { const screenshots = await Promise.all( urls.map(async (url) => { - const response = await apiClient.get({ - url, - responseType: "arraybuffer" + const response = await apiClient.get({ + url, + responseType: "arraybuffer", }); // Axios returns response.data as a Buffer for binary data const base64 = Buffer.from(response.data).toString("base64"); diff --git a/src/tools/sdk-utils/bstack/constants.ts b/src/tools/sdk-utils/bstack/constants.ts index 22fc9a94..b801bd77 100644 --- a/src/tools/sdk-utils/bstack/constants.ts +++ b/src/tools/sdk-utils/bstack/constants.ts @@ -4,7 +4,7 @@ import { ConfigMapping } from "../common/types.js"; * ---------- PYTHON INSTRUCTIONS ---------- */ -export const pythonInstructions = (username: string, accessKey: string) => { +export const pythonInstructions = () => { const setup = ` ---STEP--- @@ -32,7 +32,7 @@ browserstack-sdk pytest }; export const generatePythonFrameworkInstructions = - (framework: string) => (username: string, accessKey: string) => { + (framework: string) => () => { const setup = ` ---STEP--- @@ -646,4 +646,4 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { cypress: { instructions: cypressInstructions }, }, }, -}; \ No newline at end of file +}; From 47c707eb25651fd2a5d4200ebeb36b8e00de9669 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 15 Oct 2025 15:09:31 +0530 Subject: [PATCH 79/84] Add Python SDK setup instructions to commands --- src/tools/sdk-utils/bstack/commands.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/tools/sdk-utils/bstack/commands.ts b/src/tools/sdk-utils/bstack/commands.ts index 1f600d72..224b248a 100644 --- a/src/tools/sdk-utils/bstack/commands.ts +++ b/src/tools/sdk-utils/bstack/commands.ts @@ -99,6 +99,18 @@ Alternative setup for Gradle users: ${GRADLE_SETUP_INSTRUCTIONS}`; } +function getPythonSDKInstructions( + username: string, + accessKey: string, +): string { + return `---STEP--- +Install BrowserStack Python SDK and setup: +\`\`\`bash +pip install browserstack-sdk +browserstack-sdk setup --username "${username}" --key "${accessKey}" +\`\`\``; +} + // Main function to get SDK setup commands based on language and framework export function getSDKPrefixCommand( language: SDKSupportedLanguage, @@ -112,7 +124,8 @@ export function getSDKPrefixCommand( case "java": return getJavaSDKInstructions(framework, username, accessKey); - + case "python": + return getPythonSDKInstructions(username, accessKey); default: return ""; } From d1f4a9991b60392c12993fe5ca11936091598394 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 16 Oct 2025 02:12:44 +0530 Subject: [PATCH 80/84] update browser configuration instructions --- src/lib/version-resolver.ts | 7 +++++++ src/tools/sdk-utils/bstack/commands.ts | 5 +---- src/tools/sdk-utils/bstack/configUtils.ts | 2 +- src/tools/sdk-utils/common/device-validator.ts | 8 ++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/lib/version-resolver.ts b/src/lib/version-resolver.ts index 1530ae01..14ca3cb0 100644 --- a/src/lib/version-resolver.ts +++ b/src/lib/version-resolver.ts @@ -27,6 +27,13 @@ export function resolveVersion(requested: string, available: string[]): string { return requested; } + const caseInsensitiveMatch = uniq.find( + (v) => v.toLowerCase() === requested.toLowerCase(), + ); + if (caseInsensitiveMatch) { + return caseInsensitiveMatch; + } + // Try major version matching (e.g., "14" matches "14.0", "14.1", etc.) const reqNum = parseFloat(requested); if (!isNaN(reqNum)) { diff --git a/src/tools/sdk-utils/bstack/commands.ts b/src/tools/sdk-utils/bstack/commands.ts index 224b248a..ff71f0de 100644 --- a/src/tools/sdk-utils/bstack/commands.ts +++ b/src/tools/sdk-utils/bstack/commands.ts @@ -99,10 +99,7 @@ Alternative setup for Gradle users: ${GRADLE_SETUP_INSTRUCTIONS}`; } -function getPythonSDKInstructions( - username: string, - accessKey: string, -): string { +function getPythonSDKInstructions(username: string, accessKey: string): string { return `---STEP--- Install BrowserStack Python SDK and setup: \`\`\`bash diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index ac2b1a99..04391046 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -13,7 +13,7 @@ export function generateBrowserStackYMLInstructions(config: { const platformConfigs = generatePlatformConfigs(config); const stepTitle = - "Create a browserstack.yml file in the project root with your validated device configurations:"; + "Create a browserstack.yml file in the project root with your validated device configurations:If already exists, update it with the following content for devices and project details."; const buildName = `${projectName}-Build`; diff --git a/src/tools/sdk-utils/common/device-validator.ts b/src/tools/sdk-utils/common/device-validator.ts index fe097cc4..d2047b2b 100644 --- a/src/tools/sdk-utils/common/device-validator.ts +++ b/src/tools/sdk-utils/common/device-validator.ts @@ -316,6 +316,14 @@ export async function validateDevices( validatedEnvironments.push(validatedEnv); } + if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) { + validatedEnvironments.forEach((env) => { + if (env.browser) { + env.browser = env.browser.toLowerCase(); + } + }); + } + return validatedEnvironments; } From 56f311cd52a77a56e54a965a7dccd19002dbaaca Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 16 Oct 2025 13:23:20 +0530 Subject: [PATCH 81/84] chore: Bump version to 1.2.6 and refactor SDK setup instructions for improved clarity and functionality --- package-lock.json | 4 +-- package.json | 2 +- src/tools/sdk-utils/bstack/commands.ts | 12 +------- src/tools/sdk-utils/bstack/configUtils.ts | 26 +++++++++++----- src/tools/sdk-utils/bstack/constants.ts | 9 +++++- src/tools/sdk-utils/bstack/sdkHandler.ts | 33 ++++++++++++--------- src/tools/sdk-utils/percy-bstack/handler.ts | 19 +++++++----- 7 files changed, 61 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index f654e564..414e471a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.5", + "version": "1.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@browserstack/mcp-server", - "version": "1.2.5", + "version": "1.2.6", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.18.1", diff --git a/package.json b/package.json index 19dd5c6d..eae70a86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@browserstack/mcp-server", - "version": "1.2.5", + "version": "1.2.6", "description": "BrowserStack's Official MCP Server", "mcpName": "io.github.browserstack/mcp-server", "main": "dist/index.js", diff --git a/src/tools/sdk-utils/bstack/commands.ts b/src/tools/sdk-utils/bstack/commands.ts index ff71f0de..1f600d72 100644 --- a/src/tools/sdk-utils/bstack/commands.ts +++ b/src/tools/sdk-utils/bstack/commands.ts @@ -99,15 +99,6 @@ Alternative setup for Gradle users: ${GRADLE_SETUP_INSTRUCTIONS}`; } -function getPythonSDKInstructions(username: string, accessKey: string): string { - return `---STEP--- -Install BrowserStack Python SDK and setup: -\`\`\`bash -pip install browserstack-sdk -browserstack-sdk setup --username "${username}" --key "${accessKey}" -\`\`\``; -} - // Main function to get SDK setup commands based on language and framework export function getSDKPrefixCommand( language: SDKSupportedLanguage, @@ -121,8 +112,7 @@ export function getSDKPrefixCommand( case "java": return getJavaSDKInstructions(framework, username, accessKey); - case "python": - return getPythonSDKInstructions(username, accessKey); + default: return ""; } diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index 04391046..a38f13ae 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -1,14 +1,23 @@ import { ValidatedEnvironment } from "../common/device-validator.js"; - -export function generateBrowserStackYMLInstructions(config: { - validatedEnvironments?: ValidatedEnvironment[]; - platforms?: string[]; - enablePercy?: boolean; - projectName: string; -}): string { +import { BrowserStackConfig } from "../../../lib/types.js"; +import { getBrowserStackAuth } from "../../../lib/get-auth.js"; + +export function generateBrowserStackYMLInstructions( + config: { + validatedEnvironments?: ValidatedEnvironment[]; + platforms?: string[]; + enablePercy?: boolean; + projectName: string; + }, + browserStackConfig: BrowserStackConfig, +): string { const enablePercy = config.enablePercy || false; const projectName = config.projectName || "BrowserStack Automate Build"; + // Get credentials from config + const authString = getBrowserStackAuth(browserStackConfig); + const [username, accessKey] = authString.split(":"); + // Generate platform configurations using the utility function const platformConfigs = generatePlatformConfigs(config); @@ -22,6 +31,9 @@ export function generateBrowserStackYMLInstructions(config: { # BrowserStack Reporting # ====================== +userName: ${username} +accessKey: ${accessKey} + # TODO: Replace these sample values with your actual project details projectName: ${projectName} buildName: ${buildName} diff --git a/src/tools/sdk-utils/bstack/constants.ts b/src/tools/sdk-utils/bstack/constants.ts index b801bd77..47a7bac8 100644 --- a/src/tools/sdk-utils/bstack/constants.ts +++ b/src/tools/sdk-utils/bstack/constants.ts @@ -32,7 +32,7 @@ browserstack-sdk pytest }; export const generatePythonFrameworkInstructions = - (framework: string) => () => { + (framework: string) => (username: string, accessKey: string) => { const setup = ` ---STEP--- @@ -41,6 +41,13 @@ Install the BrowserStack SDK: \`\`\`bash python3 -m pip install browserstack-sdk \`\`\` + +---STEP--- + +Setup the BrowserStack SDK with framework-specific configuration: +\`\`\`bash +browserstack-sdk setup --framework "${framework}" --username "${username}" --key "${accessKey}" +\`\`\` `; const run = ` diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 22f00488..54171d4b 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -86,20 +86,6 @@ export async function runBstackSDKOnly( }); } - const ymlInstructions = generateBrowserStackYMLInstructions({ - validatedEnvironments, - enablePercy: false, - projectName: input.projectName, - }); - - if (ymlInstructions) { - steps.push({ - type: "instruction", - title: "Configure browserstack.yml", - content: ymlInstructions, - }); - } - const frameworkInstructions = getInstructionsForProjectConfiguration( input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework, input.detectedTestingFramework as SDKSupportedTestingFramework, @@ -116,7 +102,26 @@ export async function runBstackSDKOnly( content: frameworkInstructions.setup, }); } + } + const ymlInstructions = generateBrowserStackYMLInstructions( + { + validatedEnvironments, + enablePercy: false, + projectName: input.projectName, + }, + config, + ); + + if (ymlInstructions) { + steps.push({ + type: "instruction", + title: "Configure browserstack.yml", + content: ymlInstructions, + }); + } + + if (frameworkInstructions) { if (frameworkInstructions.run && !isPercyAutomate) { steps.push({ type: "instruction", diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index 989c17a4..9ee200fc 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -106,14 +106,17 @@ export function runPercyWithBrowserstackSDK( }); } - const ymlInstructions = generateBrowserStackYMLInstructions({ - platforms: - ((input as any).devices as string[][] | undefined)?.map((t) => - t.join(" "), - ) || [], - enablePercy: true, - projectName: input.projectName, - }); + const ymlInstructions = generateBrowserStackYMLInstructions( + { + platforms: + ((input as any).devices as string[][] | undefined)?.map((t) => + t.join(" "), + ) || [], + enablePercy: true, + projectName: input.projectName, + }, + config, + ); if (ymlInstructions) { steps.push({ From d6b5f9ca403a88abdb7c1dc0bc48e11004a17def Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 16 Oct 2025 16:17:00 +0530 Subject: [PATCH 82/84] Update MCP Publisher download URL to v1.1.0 --- .github/workflows/mcp-registry-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mcp-registry-publish.yml b/.github/workflows/mcp-registry-publish.yml index 3dcafe7b..44e2c34f 100644 --- a/.github/workflows/mcp-registry-publish.yml +++ b/.github/workflows/mcp-registry-publish.yml @@ -32,8 +32,8 @@ jobs: - name: Install MCP Publisher run: | - curl -L "https://github.com/modelcontextprotocol/registry/releases/download/latest/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher - + curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.1.0/mcp-publisher_1.1.0_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + - name: Login to MCP Registry run: ./mcp-publisher login github-oidc From ef0c7cf1719ab6d8fb88999842d36857b2286358 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 16 Oct 2025 16:28:52 +0530 Subject: [PATCH 83/84] Adding server.json --- server.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 server.json diff --git a/server.json b/server.json new file mode 100644 index 00000000..679868c4 --- /dev/null +++ b/server.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json", + "name": "io.github.browserstack/mcp-server", + "description": "BrowserStack's Official MCP Server", + "repository": { + "url": "https://github.com/browserstack/mcp-server", + "source": "github" + }, + "version": "1.2.6", + "packages": [ + { + "registryType": "npm", + "registryBaseUrl": "https://registry.npmjs.org", + "identifier": "@browserstack/mcp-server", + "version": "1.2.6", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "description": "BrowserStack Username", + "isRequired": true, + "format": "string", + "isSecret": false, + "name": "BROWSERSTACK_USERNAME" + }, + { + "description": "BrowserStack Access Key", + "isRequired": true, + "format": "string", + "isSecret": true, + "name": "BROWSERSTACK_ACCESS_KEY" + } + ] + } + ] +} \ No newline at end of file From 52c89102cc846ba5f1a2aeae0a6a632dc29f8963 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 16 Oct 2025 16:30:45 +0530 Subject: [PATCH 84/84] Changing version --- server.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.json b/server.json index 679868c4..23eb603e 100644 --- a/server.json +++ b/server.json @@ -6,7 +6,7 @@ "url": "https://github.com/browserstack/mcp-server", "source": "github" }, - "version": "1.2.6", + "version": "1.0.0", "packages": [ { "registryType": "npm",

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