From b589b4ee14bb37473d492df97dad757b957fd0ec Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 22 Jul 2025 11:36:04 +0530 Subject: [PATCH 01/23] Refactoring SDK Tool --- src/tools/bstack-sdk.ts | 252 +---------- src/tools/sdk-utils/bstack/commands.ts | 125 ++++++ src/tools/sdk-utils/bstack/configUtils.ts | 71 ++++ src/tools/sdk-utils/{ => bstack}/constants.ts | 145 +------ src/tools/sdk-utils/bstack/frameworks.ts | 59 +++ src/tools/sdk-utils/bstack/index.ts | 5 + src/tools/sdk-utils/bstack/sdkHandler.ts | 99 +++++ src/tools/sdk-utils/commands.ts | 81 ---- src/tools/sdk-utils/common/errorMessages.ts | 26 ++ src/tools/sdk-utils/common/formatUtils.ts | 38 ++ src/tools/sdk-utils/common/index.ts | 4 + .../sdk-utils/common/instructionBuilder.ts | 58 +++ .../sdk-utils/common/instructionUtils.ts | 49 +++ src/tools/sdk-utils/common/schema.ts | 44 ++ src/tools/sdk-utils/{ => common}/types.ts | 39 +- src/tools/sdk-utils/handler.ts | 91 ++++ src/tools/sdk-utils/instructions.ts | 138 ------ src/tools/sdk-utils/percy-automate/handler.ts | 43 ++ src/tools/sdk-utils/percy-automate/index.ts | 2 + .../{percy => percy-bstack}/constants.ts | 38 +- .../sdk-utils/percy-bstack/frameworks.ts | 25 ++ src/tools/sdk-utils/percy-bstack/handler.ts | 139 ++++++ 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 | 395 ++++++++++++++++++ .../sdk-utils/percy-web/fetchPercyToken.ts | 33 ++ src/tools/sdk-utils/percy-web/frameworks.ts | 24 ++ src/tools/sdk-utils/percy-web/handler.ts | 79 ++++ src/tools/sdk-utils/percy-web/index.ts | 5 + src/tools/sdk-utils/percy-web/types.ts | 11 + src/tools/sdk-utils/percy/types.ts | 21 - 32 files changed, 1524 insertions(+), 663 deletions(-) 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 (73%) 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/errorMessages.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/instructionBuilder.ts create mode 100644 src/tools/sdk-utils/common/instructionUtils.ts create mode 100644 src/tools/sdk-utils/common/schema.ts rename src/tools/sdk-utils/{ => common}/types.ts (58%) 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/handler.ts create mode 100644 src/tools/sdk-utils/percy-automate/index.ts rename src/tools/sdk-utils/{percy => percy-bstack}/constants.ts (79%) 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 (61%) 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/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index 83999b1..f207c37 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -1,250 +1,30 @@ 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"; /** - * 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. + * Tool description for Percy and BrowserStack testing */ -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, - }, - ], - }; -} +const TOOL_DESCRIPTION = + "Use this tool to get setup instructions for running tests and setting up BrowserStack and Percy SDK. Do NOT run tests directly — always use this tool to ensure correct execution."; -export default function addSDKTools( +/** + * Registers the runTestsOnBrowserStack tool with the MCP server. + * All logic, schema, and error messages are modularized for testability and maintainability. + */ +export function registerRunBrowserStackTestsTool( server: McpServer, config: BrowserStackConfig, ) { server.tool( - "runTestsOnBrowserStack", - "Use this tool to get instructions for running tests on BrowserStack and BrowserStack Percy. It sets up the BrowserStack SDK and runs your test cases on BrowserStack.", - { - 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.", - ), - }, - + "integrateTestsOnBrowserStackAndPercy", + TOOL_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, "default-project-mcp"); }, ); } + +export default registerRunBrowserStackTestsTool; diff --git a/src/tools/sdk-utils/bstack/commands.ts b/src/tools/sdk-utils/bstack/commands.ts new file mode 100644 index 0000000..5f02ec3 --- /dev/null +++ b/src/tools/sdk-utils/bstack/commands.ts @@ -0,0 +1,125 @@ +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} +\`\`\` +---STEP--- +Edit the browserstack.yml file that was created in the project root to add your desired platforms and browsers.`; + +// 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 0000000..0e802ea --- /dev/null +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -0,0 +1,71 @@ +/** + * Utilities for generating BrowserStack configuration files. + */ + +export function generateBrowserStackYMLInstructions( + desiredPlatforms: string[], + enablePercy: boolean = false, +) { + let ymlContent = ` +# ====================== +# BrowserStack Reporting +# ====================== +# A single name for your project to organize all your tests. This is required for Percy. +projectName: BrowserStack SDK Tests +# A name for the group of tests you are running +buildName: mcp-run + +# ======================================= +# 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 73% rename from src/tools/sdk-utils/constants.ts rename to src/tools/sdk-utils/bstack/constants.ts index 5149e41..b575faf 100644 --- a/src/tools/sdk-utils/constants.ts +++ b/src/tools/sdk-utils/bstack/constants.ts @@ -1,10 +1,8 @@ -import { ConfigMapping } from "./types.js"; - /** * ---------- PYTHON INSTRUCTIONS ---------- */ -const pythonInstructions = (username: string, accessKey: string) => ` +export const pythonInstructions = (username: string, accessKey: string) => ` ---STEP--- Install the BrowserStack SDK: @@ -27,7 +25,7 @@ browserstack-sdk python \`\`\` `; -const generatePythonFrameworkInstructions = +export const generatePythonFrameworkInstructions = (framework: string) => (username: string, accessKey: string) => ` ---STEP--- @@ -52,9 +50,9 @@ browserstack-sdk ${framework} \`\`\` `; -const robotInstructions = generatePythonFrameworkInstructions("robot"); -const behaveInstructions = generatePythonFrameworkInstructions("behave"); -const pytestInstructions = generatePythonFrameworkInstructions("pytest"); +export const robotInstructions = generatePythonFrameworkInstructions("robot"); +export const behaveInstructions = generatePythonFrameworkInstructions("behave"); +export const pytestInstructions = generatePythonFrameworkInstructions("pytest"); /** * ---------- JAVA INSTRUCTIONS ---------- @@ -63,7 +61,7 @@ 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) => ` ---STEP--- Add the BrowserStack Java SDK dependency to your \`pom.xml\`: @@ -106,68 +104,14 @@ 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 -\`\`\` -`; - /** * ---------- CSharp INSTRUCTIONS ---------- */ -const csharpCommonInstructions = (username: string, accessKey: string) => ` +export const csharpCommonInstructions = ( + username: string, + accessKey: string, +) => ` ---STEP--- Install BrowserStack TestAdapter NuGet package: @@ -230,7 +174,7 @@ Run the tests: \`\`\` `; -const csharpPlaywrightCommonInstructions = ( +export const csharpPlaywrightCommonInstructions = ( username: string, accessKey: string, ) => ` @@ -313,7 +257,7 @@ Run the tests: * ---------- NODEJS INSTRUCTIONS ---------- */ -const nodejsInstructions = (username: string, accessKey: string) => ` +export const nodejsInstructions = (username: string, accessKey: string) => ` ---STEP--- Ensure \`browserstack-node-sdk\` is present in package.json with the latest version: @@ -344,7 +288,10 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} * ---------- EXPORT CONFIG ---------- */ -const webdriverioInstructions = (username: string, accessKey: string) => ` +export const webdriverioInstructions = ( + username: string, + accessKey: string, +) => ` ---STEP--- Set BrowserStack Credentials: @@ -450,7 +397,7 @@ Run your tests: You can now run your tests on BrowserStack using your standard WebdriverIO command. `; -const cypressInstructions = (username: string, accessKey: string) => ` +export const cypressInstructions = (username: string, accessKey: string) => ` ---STEP--- Install the BrowserStack Cypress CLI: @@ -522,61 +469,3 @@ npx browserstack-cypress run --sync After the tests complete, you can view the results on your [BrowserStack Automate Dashboard](https://automate.browserstack.com/dashboard/). `; - -export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { - python: { - playwright: { - pytest: { instructions: pythonInstructions }, - }, - selenium: { - pytest: { instructions: pytestInstructions }, - robot: { instructions: robotInstructions }, - behave: { instructions: behaveInstructions }, - }, - }, - java: { - playwright: { - junit4: { instructions: javaInstructions }, - junit5: { instructions: javaInstructions }, - testng: { instructions: javaInstructions }, - }, - selenium: { - testng: { instructions: javaInstructions }, - cucumber: { instructions: javaInstructions }, - junit4: { instructions: javaInstructions }, - junit5: { instructions: javaInstructions }, - serenity: { instructions: serenityInstructions }, - }, - }, - csharp: { - playwright: { - nunit: { instructions: csharpPlaywrightCommonInstructions }, - mstest: { instructions: csharpPlaywrightCommonInstructions }, - }, - selenium: { - xunit: { instructions: csharpCommonInstructions }, - nunit: { instructions: csharpCommonInstructions }, - mstest: { instructions: csharpCommonInstructions }, - specflow: { instructions: csharpCommonInstructions }, - reqnroll: { instructions: csharpCommonInstructions }, - }, - }, - nodejs: { - playwright: { - jest: { instructions: nodejsInstructions }, - codeceptjs: { instructions: nodejsInstructions }, - playwright: { instructions: nodejsInstructions }, - }, - selenium: { - jest: { instructions: nodejsInstructions }, - webdriverio: { instructions: webdriverioInstructions }, - mocha: { instructions: nodejsInstructions }, - cucumber: { instructions: nodejsInstructions }, - nightwatch: { instructions: nodejsInstructions }, - codeceptjs: { instructions: nodejsInstructions }, - }, - cypress: { - cypress: { instructions: cypressInstructions }, - }, - }, -}; diff --git a/src/tools/sdk-utils/bstack/frameworks.ts b/src/tools/sdk-utils/bstack/frameworks.ts new file mode 100644 index 0000000..cfacb31 --- /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 0000000..d11f85f --- /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 0000000..6297126 --- /dev/null +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -0,0 +1,99 @@ +// 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, +): 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, + ); + + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions, + }); + + 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, + ); + + 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) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions, + }); + } + + 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 f257347..0000000 --- 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/errorMessages.ts b/src/tools/sdk-utils/common/errorMessages.ts new file mode 100644 index 0000000..b11edc9 --- /dev/null +++ b/src/tools/sdk-utils/common/errorMessages.ts @@ -0,0 +1,26 @@ +/** + * Centralized error and fallback messages for runTestsOnBrowserStack tool. + */ + +export const IMPORTANT_SETUP_WARNING = + "⚠️ IMPORTANT: DO NOT SKIP ANY STEP\nAll the setup steps described below MUST be executed regardless of any existing configuration or setup.\nThis ensures proper BrowserStack SDK setup."; + +export const UNKNOWN_CONFIGURATION_ERROR = + "Unknown configuration for runTestsOnBrowserStack. Please check your input parameters and try again."; + +export const PERCY_WEB_NOT_IMPLEMENTED = + "Percy Web (direct Percy SDK) support is not yet implemented. Please check back later."; + +export const PERCY_AUTOMATE_NOT_IMPLEMENTED = + "Percy Automate support is not yet implemented for this configuration. Please check back later."; + +export const BOOTSTRAP_FAILED = ( + error: unknown, + context: { config: unknown; percyMode?: string; sdkVersion?: string }, +) => + `Failed to bootstrap project with BrowserStack SDK. +Error: ${error} +Config: ${JSON.stringify(context.config, null, 2)} +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/common/formatUtils.ts b/src/tools/sdk-utils/common/formatUtils.ts new file mode 100644 index 0000000..dafc160 --- /dev/null +++ b/src/tools/sdk-utils/common/formatUtils.ts @@ -0,0 +1,38 @@ +/** + * Utilities for formatting instructions and generating verification messages. + */ + +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 0000000..9771d7d --- /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 "./errorMessages.js"; +export * from "./formatUtils.js"; diff --git a/src/tools/sdk-utils/common/instructionBuilder.ts b/src/tools/sdk-utils/common/instructionBuilder.ts new file mode 100644 index 0000000..7a673de --- /dev/null +++ b/src/tools/sdk-utils/common/instructionBuilder.ts @@ -0,0 +1,58 @@ +import { RunTestsOnBrowserStackInput, PercyMode } from "./schema.js"; +import { BrowserStackConfig } from "../../../lib/types.js"; +import { runPercyWeb } from "../percy-web/handler.js"; +import { runBstackSDKOnly } from "../bstack/sdkHandler.js"; +import { runPercyWithSDK } from "../percy-bstack/handler.js"; +import { fetchPercyToken } from "../percy-web/fetchPercyToken.js"; +import { getBrowserStackAuth } from "../../../lib/get-auth.js"; +import { RunTestsInstructionResult } from "./types.js"; + +/** + * Main instruction builder with clear execution paths + * Routes to appropriate handlers based on Percy mode + */ +export async function buildRunTestsInstructions( + input: RunTestsOnBrowserStackInput, + config: BrowserStackConfig, + projectName: string, +): Promise { + switch (input.percyMode) { + case PercyMode.PercyDisabled: + // BrowserStack SDK only - no Percy + return runBstackSDKOnly(input, config); + + case PercyMode.PercyWithSDK: + // BrowserStack SDK + Percy integration with automatic fallback + return handlePercyWithSDKFlow(input, config); + + case PercyMode.PercyWeb: { + const authorization = getBrowserStackAuth(config); + const percyToken = await fetchPercyToken(projectName, authorization); + return runPercyWeb(input, percyToken || "YOUR_PERCY_TOKEN_HERE"); + } + + default: + throw new Error(`Unsupported percy mode: ${input.percyMode}`); + } +} + +/** + * Handles Percy + SDK flow with automatic fallback logic + * First tries Percy with SDK, automatically falls back to Percy Automate if unsupported + * User never directly chooses Percy Automate - it's always a fallback + */ +function handlePercyWithSDKFlow( + input: RunTestsOnBrowserStackInput, + config: BrowserStackConfig, +): RunTestsInstructionResult { + // Try Percy with SDK first + const percyWithSDKResult = runPercyWithSDK(input, config); + + // If Percy with SDK fails (not supported), automatically fallback to Percy Automate + // if (percyWithSDKResult.steps.some((step) => step.isError)) { + // This is an internal fallback - user never directly requests this + // return runPercyAutomateOnly(); + // } + + return percyWithSDKResult; +} diff --git a/src/tools/sdk-utils/common/instructionUtils.ts b/src/tools/sdk-utils/common/instructionUtils.ts new file mode 100644 index 0000000..484928e --- /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 0000000..ac2d788 --- /dev/null +++ b/src/tools/sdk-utils/common/schema.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; +import { + SDKSupportedBrowserAutomationFrameworkEnum, + SDKSupportedTestingFrameworkEnum, + SDKSupportedLanguageEnum, +} from "./types.js"; + +export enum PercyMode { + PercyDisabled = "percy-disabled", // BrowserStack SDK only, no Percy + PercyWithSDK = "percy-on-browserstack-infra", // BrowserStack SDK + Percy integration (with fallback) + PercyWeb = "percy-on-local-infra", // Percy Web only (not implemented) +} + +// Internal enum for execution paths (includes fallback) +export enum InternalPercyMode { + PercyDisabled = "percy-disabled", + PercyWithSDK = "percy-on-browserstack-infra", + PercyAutomate = "percy-automate", + PercyWeb = "percy-on-local-infra", +} + +// Centralized enum for Percy (Zod) - Only user-facing options +export const PercyModeEnum = z.nativeEnum(PercyMode); + +// User-facing schema - only 3 options for user +export const RunTestsOnBrowserStackParamsShape = { + detectedBrowserAutomationFramework: z.nativeEnum( + SDKSupportedBrowserAutomationFrameworkEnum, + ), + detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), + detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), + desiredPlatforms: z.array(z.enum(["windows", "macos", "android", "ios"])), + percyMode: PercyModeEnum.default(PercyMode.PercyDisabled).describe( + "Percy mode: No 'Percy' in user input → percy-disabled. 'Percy' without 'BrowserStack'/'Automate' → percy-on-local-infra. 'Percy' with 'BrowserStack' or 'Automate' → percy-on-browserstack-infra. Always map user requests to the correct tool and mode per these rules.", + ), +}; + +export const RunTestsOnBrowserStackSchema = z.object( + RunTestsOnBrowserStackParamsShape, +); + +export type RunTestsOnBrowserStackInput = z.infer< + typeof RunTestsOnBrowserStackSchema +>; diff --git a/src/tools/sdk-utils/types.ts b/src/tools/sdk-utils/common/types.ts similarity index 58% rename from src/tools/sdk-utils/types.ts rename to src/tools/sdk-utils/common/types.ts index e30449e..1df343f 100644 --- a/src/tools/sdk-utils/types.ts +++ b/src/tools/sdk-utils/common/types.ts @@ -1,8 +1,10 @@ export enum SDKSupportedLanguageEnum { nodejs = "nodejs", + javascript = "javascript", python = "python", java = "java", csharp = "csharp", + ruby = "ruby", } export type SDKSupportedLanguage = keyof typeof SDKSupportedLanguageEnum; @@ -28,28 +30,45 @@ export enum SDKSupportedTestingFrameworkEnum { junit4 = "junit4", junit5 = "junit5", testng = "testng", - serenity = "serenity", cypress = "cypress", nunit = "nunit", mstest = "mstest", xunit = "xunit", specflow = "specflow", reqnroll = "reqnroll", + rspec = "rspec", } export type SDKSupportedTestingFramework = keyof typeof SDKSupportedTestingFrameworkEnum; -export type ConfigMapping = Record< - SDKSupportedLanguageEnum, - Partial< - Record< - SDKSupportedBrowserAutomationFrameworkEnum, - Partial< - Record< - SDKSupportedTestingFrameworkEnum, - { instructions: (username: string, accessKey: string) => string } +export type ConfigMapping = Partial< + Record< + SDKSupportedLanguageEnum, + Partial< + Record< + SDKSupportedBrowserAutomationFrameworkEnum, + Partial< + Record< + SDKSupportedTestingFrameworkEnum, + { instructions: (username: string, accessKey: string) => 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; +} diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts new file mode 100644 index 0000000..8e199b7 --- /dev/null +++ b/src/tools/sdk-utils/handler.ts @@ -0,0 +1,91 @@ +import { RunTestsOnBrowserStackSchema } from "./common/schema.js"; +import { buildRunTestsInstructions } from "./common/instructionBuilder.js"; +import { + BOOTSTRAP_FAILED, + IMPORTANT_SETUP_WARNING, +} from "./common/errorMessages.js"; +import { + formatInstructionsWithNumbers, + generateVerificationMessage, +} from "./common/formatUtils.js"; +import { BrowserStackConfig } from "../../lib/types.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +export async function runTestsOnBrowserStackHandler( + rawInput: unknown, + config: BrowserStackConfig, + projectName: string, +): Promise { + try { + // Validate input with schema + const input = RunTestsOnBrowserStackSchema.parse(rawInput); + + // Build instructions and metadata + const { steps, requiresPercy, missingDependencies, shouldSkipFormatting } = + await buildRunTestsInstructions(input, config, projectName); + + // If shouldSkipFormatting is true (for unsupported cases), return minimal response + if (shouldSkipFormatting) { + return { + content: steps.map((step: { content: string }) => ({ + type: "text" as const, + text: step.content, + })), + isError: steps.some((s: { isError?: boolean }) => s.isError), + steps, + requiresPercy, + missingDependencies, + }; + } + + // Combine all step content into a single string for formatting + const combinedInstructions = steps + .map((step: { content: string }) => step.content) + .join("\n"); + + // Apply step numbering using the formatInstructionsWithNumbers function + const { formattedSteps, stepCount } = + formatInstructionsWithNumbers(combinedInstructions); + + // Generate verification message + const verificationMessage = generateVerificationMessage(stepCount); + + // Create the final content with setup warning, formatted instructions, and verification + const finalContent = [ + { + type: "text" as const, + text: IMPORTANT_SETUP_WARNING, + }, + { + type: "text" as const, + text: formattedSteps, + }, + { + type: "text" as const, + text: verificationMessage, + }, + ]; + + // Structured output + return { + content: finalContent, + isError: steps.some((s: { isError?: boolean }) => s.isError), + steps, + requiresPercy, + missingDependencies, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: BOOTSTRAP_FAILED(error, { + config, + percyMode: (rawInput as any)?.percyMode, + }), + }, + ], + isError: true, + }; + } +} diff --git a/src/tools/sdk-utils/instructions.ts b/src/tools/sdk-utils/instructions.ts deleted file mode 100644 index d98c872..0000000 --- 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/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts new file mode 100644 index 0000000..b7ad3b3 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -0,0 +1,43 @@ +// Handler for Percy Automate only (fallback when Percy SDK not supported) +import { RunTestsInstructionResult } from "../common/types.js"; +import { PERCY_AUTOMATE_NOT_IMPLEMENTED } from "../common/errorMessages.js"; + +// Placeholder function for Percy Automate fallback +// Returns null if not supported, instructions string if supported +function getPercyAutomateInstructions(): string | null { + return "It worked as a fallback for Percy Automate."; +} + +export function runPercyAutomateOnly(): RunTestsInstructionResult { + const percyAutomateInstructions = getPercyAutomateInstructions(); + + if (percyAutomateInstructions) { + return { + steps: [ + { + type: "instruction", + title: "Percy Automate Setup (Fallback)", + content: percyAutomateInstructions, + }, + ], + requiresPercy: true, + missingDependencies: [], + shouldSkipFormatting: false, + }; + } + + // Percy Automate not supported - skip formatting for error case + return { + steps: [ + { + type: "error", + title: "Percy Automate Not Supported", + content: PERCY_AUTOMATE_NOT_IMPLEMENTED, + isError: true, + }, + ], + requiresPercy: true, + missingDependencies: [], + shouldSkipFormatting: true, + }; +} 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 0000000..230bfcb --- /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/constants.ts b/src/tools/sdk-utils/percy-bstack/constants.ts similarity index 79% rename from src/tools/sdk-utils/percy/constants.ts rename to src/tools/sdk-utils/percy-bstack/constants.ts index 39dea6a..722dd2e 100644 --- a/src/tools/sdk-utils/percy/constants.ts +++ b/src/tools/sdk-utils/percy-bstack/constants.ts @@ -1,6 +1,6 @@ -import { PercyConfigMapping } from "./types.js"; +// Percy + BrowserStack SDK configuration constants -const javaSeleniumInstructions = ` +export const javaSeleniumInstructions = ` Import the BrowserStack Percy SDK in your test script: Add the Percy import to your test file. @@ -54,7 +54,7 @@ describe("sample Test", () => { \`\`\` `; -const webdriverioPercyInstructions = ` +export const webdriverioPercyInstructions = ` Enable Percy in \`wdio.conf.js\`: In your WebdriverIO configuration file, modify the 'browserstack' service options to enable Percy. @@ -117,7 +117,7 @@ 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. @@ -151,33 +151,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 0000000..bc985e2 --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/frameworks.ts @@ -0,0 +1,25 @@ +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 }, + }, + }, + csharp: { + selenium: { + nunit: { 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 0000000..4079442 --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -0,0 +1,139 @@ +// 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 runPercyWithSDK( + 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, + ); + + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions, + }); + + steps.push({ + type: "instruction", + title: "Percy Setup (BrowserStack SDK + Percy)", + content: formatPercyInstructions(percyResult), + }); + + 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, + ); + + 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) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions, + }); + } + + steps.push({ + type: "instruction", + title: "Percy Setup (BrowserStack SDK + Percy)", + content: formatPercyInstructions(percyResult), + }); + + 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 0000000..15045b9 --- /dev/null +++ b/src/tools/sdk-utils/percy-bstack/index.ts @@ -0,0 +1,8 @@ +// Percy + BrowserStack SDK utilities +export { runPercyWithSDK } 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 61% rename from src/tools/sdk-utils/percy/instructions.ts rename to src/tools/sdk-utils/percy-bstack/instructions.ts index f642efa..e544594 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 `## 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 0000000..b45f962 --- /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 0000000..3bbcf75 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -0,0 +1,395 @@ +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 percyAggressiveInstruction = ` +**MANDATORY: Aggressive, Exhaustive Percy Integration** + +You MUST follow these steps with NO EXCEPTIONS: + +1. Recursively search the ENTIRE target directory and ALL subdirectories for EVERY file relevant to the language/framework. +2. For EACH relevant file, analyze the code to find EVERY location where a Percy snapshot (or equivalent) should be called (e.g., after each navigation, UI change, or significant state in tests). Insert snapshot calls at ALL such points, not just the first or most obvious one. +3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). +4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. +5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. +`; + +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 +${percyAggressiveInstruction} +Example: +\`\`\`python +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') +\`\`\` + +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 +${percyAggressiveInstruction} + - 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'); +\`\`\` + +---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 +${percyAggressiveInstruction} + - 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(); + } +} +\`\`\` + +---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 +${percyAggressiveInstruction} + - 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 +\`\`\` + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- + +${percyReviewSnapshotsStep} +`; + +/* rubyInstructions is already exported above */ + +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 +${percyAggressiveInstruction} + + - 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(); + } +} +\`\`\` + +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 +${percyAggressiveInstruction} + - 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(); + } + } +} +\`\`\` + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- + +${percyReviewSnapshotsStep} +`; + +export const jsPlaywrightInstructions = ` +Install Percy dependencies + - Install Percy Playwright SDK: + npm install @percy/playwright + +---STEP--- +Update your Playwright JavaScript test +${percyAggressiveInstruction} + - 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(); +})(); +\`\`\` + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- +${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 +${percyAggressiveInstruction} + - 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() +\`\`\` + +---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 +${percyAggressiveInstruction} + - 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(); + } +} +\`\`\` + +---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 0000000..0752b17 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts @@ -0,0 +1,33 @@ +//Fetches a Percy token for a given project name from the BrowserStack API. +// Returns the token if successful, or throws an error if not. + +export async function fetchPercyToken( + projectName: string, + authorization: string, +): Promise { + try { + const encodedAuth = `Basic ${Buffer.from(authorization).toString("base64")}`; + const response = await fetch( + `https://api.browserstack.com/api/app_percy/get_project_token?name=${encodeURIComponent(projectName)}&type=web`, + { + headers: { + Authorization: encodedAuth, + }, + }, + ); + if (!response.ok) { + throw new Error(`Failed to fetch Percy token: ${response.status}`); + } + const data = await response.json(); + if (!data || !data.token || !data.success) { + throw new Error( + "Looks like project already exists but uses automate for running tests. Use different project name all together.", + ); + } + return data.token; + } catch (error) { + throw new Error( + `Error retrieving Percy token: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } +} 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 0000000..eb82d7b --- /dev/null +++ b/src/tools/sdk-utils/percy-web/frameworks.ts @@ -0,0 +1,24 @@ +import { ConfigMapping } from "./types.js"; +import * as constants from "./constants.js"; + +export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { + python: { + selenium: { instructions: constants.pythonInstructions }, + playwright: { instructions: constants.pythonPlaywrightInstructions }, + }, + javascript: { + selenium: { instructions: constants.nodejsInstructions }, + playwright: { instructions: constants.jsPlaywrightInstructions }, + }, + java: { + selenium: { instructions: constants.javaInstructions }, + playwright: { instructions: constants.javaPlaywrightInstructions }, + }, + ruby: { + selenium: { instructions: constants.rubyInstructions }, + }, + csharp: { + selenium: { instructions: constants.csharpInstructions }, + playwright: { instructions: constants.csharpPlaywrightInstructions }, + }, +}; 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 0000000..e74f8f0 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -0,0 +1,79 @@ +// Handler for Percy Web only mode - Visual testing without BrowserStack infrastructure +import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; +import { RunTestsOnBrowserStackInput } from "../common/schema.js"; +import { SUPPORTED_CONFIGURATIONS } from "./frameworks.js"; +import { + SDKSupportedBrowserAutomationFramework, + SDKSupportedTestingFramework, + SDKSupportedLanguage, +} from "../common/types.js"; + +export function runPercyWeb( + input: RunTestsOnBrowserStackInput, + percyToken: string, +): RunTestsInstructionResult { + const steps: RunTestsStep[] = []; + + // Check if this configuration is supported for Percy Web + const languageConfig = + SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; + + if (!languageConfig) { + return { + steps: [ + { + type: "error", + title: "Language Not Supported", + content: `Percy Web does not support the language: ${input.detectedLanguage}. Supported languages are: ${Object.keys(SUPPORTED_CONFIGURATIONS).join(", ")}.`, + isError: true, + }, + ], + requiresPercy: true, + missingDependencies: [], + shouldSkipFormatting: true, + }; + } + + const frameworkConfig = + languageConfig[ + input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework + ]; + + if (!frameworkConfig) { + return { + steps: [ + { + type: "error", + title: "Framework Not Supported", + content: `Percy Web does not support ${input.detectedBrowserAutomationFramework} for ${input.detectedLanguage}. Supported frameworks for ${input.detectedLanguage} are: ${Object.keys(languageConfig).join(", ")}.`, + isError: true, + }, + ], + requiresPercy: true, + missingDependencies: [], + shouldSkipFormatting: true, + }; + } + + // Generate instructions for the supported configuration + const instructions = frameworkConfig.instructions; + + // Prepend a step to set the Percy token in the environment + steps.push({ + type: "instruction", + title: "Set Percy Token in Environment", + content: `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.)`, + }); + + steps.push({ + type: "instruction", + title: `Percy Web Setup for ${input.detectedLanguage} with ${input.detectedBrowserAutomationFramework}`, + 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 0000000..6dd10c6 --- /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 0000000..49b24b7 --- /dev/null +++ b/src/tools/sdk-utils/percy-web/types.ts @@ -0,0 +1,11 @@ +/** + * Type for Percy Web configuration mapping. + * Structure: language -> automationFramework -> { instructions: string } + */ +export type ConfigMapping = { + [language: string]: { + [automationFramework: string]: { + instructions: string; + }; + }; +}; diff --git a/src/tools/sdk-utils/percy/types.ts b/src/tools/sdk-utils/percy/types.ts deleted file mode 100644 index 1ddd464..0000000 --- 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 7f0bd97375dd652fcf9f7897d38d35568545be5b Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 23 Jul 2025 14:46:35 +0530 Subject: [PATCH 02/23] add projectName parameter to BrowserStack YML instructions --- src/tools/sdk-utils/bstack/configUtils.ts | 7 ++++--- src/tools/sdk-utils/bstack/sdkHandler.ts | 1 + src/tools/sdk-utils/common/instructionBuilder.ts | 3 +-- src/tools/sdk-utils/common/schema.ts | 1 + src/tools/sdk-utils/handler.ts | 2 +- src/tools/sdk-utils/percy-bstack/handler.ts | 1 + 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/tools/sdk-utils/bstack/configUtils.ts b/src/tools/sdk-utils/bstack/configUtils.ts index 0e802ea..587abd3 100644 --- a/src/tools/sdk-utils/bstack/configUtils.ts +++ b/src/tools/sdk-utils/bstack/configUtils.ts @@ -5,15 +5,16 @@ 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: BrowserStack SDK Tests -# A name for the group of tests you are running -buildName: mcp-run +projectName: ${projectName} +# TODO: Replace these sample values with your actual project details +buildName: Sample-Build # ======================================= # Platforms (Browsers / Devices to test) diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 6297126..7174187 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -65,6 +65,7 @@ export function runBstackSDKOnly( const ymlInstructions = generateBrowserStackYMLInstructions( input.desiredPlatforms as string[], false, + input.projectName, ); if (ymlInstructions) { diff --git a/src/tools/sdk-utils/common/instructionBuilder.ts b/src/tools/sdk-utils/common/instructionBuilder.ts index 7a673de..8aa7716 100644 --- a/src/tools/sdk-utils/common/instructionBuilder.ts +++ b/src/tools/sdk-utils/common/instructionBuilder.ts @@ -14,7 +14,6 @@ import { RunTestsInstructionResult } from "./types.js"; export async function buildRunTestsInstructions( input: RunTestsOnBrowserStackInput, config: BrowserStackConfig, - projectName: string, ): Promise { switch (input.percyMode) { case PercyMode.PercyDisabled: @@ -27,7 +26,7 @@ export async function buildRunTestsInstructions( case PercyMode.PercyWeb: { const authorization = getBrowserStackAuth(config); - const percyToken = await fetchPercyToken(projectName, authorization); + const percyToken = await fetchPercyToken(input.projectName, authorization); return runPercyWeb(input, percyToken || "YOUR_PERCY_TOKEN_HERE"); } diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index ac2d788..7a5ce90 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -30,6 +30,7 @@ export const RunTestsOnBrowserStackParamsShape = { detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum), desiredPlatforms: z.array(z.enum(["windows", "macos", "android", "ios"])), + projectName: z.string(), percyMode: PercyModeEnum.default(PercyMode.PercyDisabled).describe( "Percy mode: No 'Percy' in user input → percy-disabled. 'Percy' without 'BrowserStack'/'Automate' → percy-on-local-infra. 'Percy' with 'BrowserStack' or 'Automate' → percy-on-browserstack-infra. Always map user requests to the correct tool and mode per these rules.", ), diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 8e199b7..b58bea1 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -22,7 +22,7 @@ export async function runTestsOnBrowserStackHandler( // Build instructions and metadata const { steps, requiresPercy, missingDependencies, shouldSkipFormatting } = - await buildRunTestsInstructions(input, config, projectName); + await buildRunTestsInstructions(input, config); // If shouldSkipFormatting is true (for unsupported cases), return minimal response if (shouldSkipFormatting) { diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index 4079442..8fe50d9 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -99,6 +99,7 @@ export function runPercyWithSDK( const ymlInstructions = generateBrowserStackYMLInstructions( input.desiredPlatforms as string[], true, + input.projectName, ); if (ymlInstructions) { From d2f7a6da5c04541f5bf90d4a60ae213913706057 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 28 Jul 2025 19:07:59 +0530 Subject: [PATCH 03/23] Percy changes --- src/server-factory.ts | 2 + src/tools/bstack-sdk.ts | 17 +- src/tools/percy-sdk.ts | 30 ++ src/tools/sdk-utils/bstack/commands.ts | 4 +- src/tools/sdk-utils/common/errorMessages.ts | 20 ++ .../sdk-utils/common/instructionBuilder.ts | 57 ---- src/tools/sdk-utils/common/schema.ts | 54 ++-- src/tools/sdk-utils/common/types.ts | 5 + src/tools/sdk-utils/handler.ts | 185 ++++++++---- .../sdk-utils/percy-automate/constants.ts | 282 ++++++++++++++++++ .../sdk-utils/percy-automate/frameworks.ts | 22 ++ src/tools/sdk-utils/percy-automate/handler.ts | 97 ++++-- src/tools/sdk-utils/percy-automate/types.ts | 11 + .../sdk-utils/percy-bstack/instructions.ts | 4 +- .../sdk-utils/percy-web/fetchPercyToken.ts | 46 +-- src/tools/sdk-utils/percy-web/handler.ts | 5 +- 16 files changed, 646 insertions(+), 195 deletions(-) create mode 100644 src/tools/percy-sdk.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/types.ts diff --git a/src/server-factory.ts b/src/server-factory.ts index d471467..5c0ea1b 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -4,6 +4,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"; @@ -18,6 +19,7 @@ import { BrowserStackConfig } from "./lib/types.js"; function registerTools(server: McpServer, config: BrowserStackConfig) { addAccessibilityTools(server, config); addSDKTools(server, config); + addPercyTools(server, config); addAppLiveTools(server, config); addBrowserLiveTools(server, config); addTestManagementTools(server, config); diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index f207c37..de90b62 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -3,26 +3,19 @@ import { BrowserStackConfig } from "../lib/types.js"; import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js"; import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js"; -/** - * Tool description for Percy and BrowserStack testing - */ -const TOOL_DESCRIPTION = - "Use this tool to get setup instructions for running tests and setting up BrowserStack and Percy SDK. Do NOT run tests directly — always use this tool to ensure correct execution."; +const RUN_ON_BROWSERSTACK_DESCRIPTION = + "Use this tool to get setup instructions for running your functional tests on BrowserStack's cloud infrastructure. You can run tests using the BrowserStack SDK alone, or enable Percy visual testing integration (only where BrowserStack SDK has built-in Percy support). For standalone Percy setups (Percy Web or Percy Automate), use the Percy SDK tools instead."; -/** - * Registers the runTestsOnBrowserStack tool with the MCP server. - * All logic, schema, and error messages are modularized for testability and maintainability. - */ export function registerRunBrowserStackTestsTool( server: McpServer, config: BrowserStackConfig, ) { server.tool( - "integrateTestsOnBrowserStackAndPercy", - TOOL_DESCRIPTION, + "runTestsOnBrowserStack", + RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, async (args) => { - return runTestsOnBrowserStackHandler(args, config, "default-project-mcp"); + return runTestsOnBrowserStackHandler(args, config); }, ); } diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts new file mode 100644 index 0000000..88f31ff --- /dev/null +++ b/src/tools/percy-sdk.ts @@ -0,0 +1,30 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { BrowserStackConfig } from "../lib/types.js"; +import { SetUpPercyParamsShape } from "./sdk-utils/common/schema.js"; +import { setUpPercyHandler } from "./sdk-utils/handler.js"; + +/** + * Tool description for standalone Percy visual testing + */ +const SETUP_PERCY_DESCRIPTION = + "Use this tool to get setup instructions for standalone Percy visual testing. This sets up Percy with your own testing infrastructure: Percy Web for local projects, or Percy Automate for your own Selenium/Playwright grid. This does NOT integrate with BrowserStack's test execution infrastructure - use BrowserStack SDK tools for that."; + +/** + * Registers the standalone Percy setup tool with the MCP server. + * Focuses on Percy Web and Percy Automate without BrowserStack integration. + */ +export function registerPercySetupTool( + server: McpServer, + config: BrowserStackConfig, +) { + server.tool( + "setUpPercy", + SETUP_PERCY_DESCRIPTION, + SetUpPercyParamsShape, + async (args) => { + return setUpPercyHandler(args, config); + }, + ); +} + +export default registerPercySetupTool; diff --git a/src/tools/sdk-utils/bstack/commands.ts b/src/tools/sdk-utils/bstack/commands.ts index 5f02ec3..1f600d7 100644 --- a/src/tools/sdk-utils/bstack/commands.ts +++ b/src/tools/sdk-utils/bstack/commands.ts @@ -26,9 +26,7 @@ npm i -D browserstack-node-sdk@latest 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.`; +\`\`\``; // Template for Gradle setup instructions (platform-independent) const GRADLE_SETUP_INSTRUCTIONS = ` diff --git a/src/tools/sdk-utils/common/errorMessages.ts b/src/tools/sdk-utils/common/errorMessages.ts index b11edc9..7f24e59 100644 --- a/src/tools/sdk-utils/common/errorMessages.ts +++ b/src/tools/sdk-utils/common/errorMessages.ts @@ -14,6 +14,26 @@ export const PERCY_WEB_NOT_IMPLEMENTED = export const PERCY_AUTOMATE_NOT_IMPLEMENTED = "Percy Automate support is not yet implemented for this configuration. Please check back later."; +export enum PercyAutomateNotImplementedType { + LANGUAGE = "language", + FRAMEWORK = "framework", +} + +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 const BOOTSTRAP_FAILED = ( error: unknown, context: { config: unknown; percyMode?: string; sdkVersion?: string }, diff --git a/src/tools/sdk-utils/common/instructionBuilder.ts b/src/tools/sdk-utils/common/instructionBuilder.ts index 8aa7716..e69de29 100644 --- a/src/tools/sdk-utils/common/instructionBuilder.ts +++ b/src/tools/sdk-utils/common/instructionBuilder.ts @@ -1,57 +0,0 @@ -import { RunTestsOnBrowserStackInput, PercyMode } from "./schema.js"; -import { BrowserStackConfig } from "../../../lib/types.js"; -import { runPercyWeb } from "../percy-web/handler.js"; -import { runBstackSDKOnly } from "../bstack/sdkHandler.js"; -import { runPercyWithSDK } from "../percy-bstack/handler.js"; -import { fetchPercyToken } from "../percy-web/fetchPercyToken.js"; -import { getBrowserStackAuth } from "../../../lib/get-auth.js"; -import { RunTestsInstructionResult } from "./types.js"; - -/** - * Main instruction builder with clear execution paths - * Routes to appropriate handlers based on Percy mode - */ -export async function buildRunTestsInstructions( - input: RunTestsOnBrowserStackInput, - config: BrowserStackConfig, -): Promise { - switch (input.percyMode) { - case PercyMode.PercyDisabled: - // BrowserStack SDK only - no Percy - return runBstackSDKOnly(input, config); - - case PercyMode.PercyWithSDK: - // BrowserStack SDK + Percy integration with automatic fallback - return handlePercyWithSDKFlow(input, config); - - case PercyMode.PercyWeb: { - const authorization = getBrowserStackAuth(config); - const percyToken = await fetchPercyToken(input.projectName, authorization); - return runPercyWeb(input, percyToken || "YOUR_PERCY_TOKEN_HERE"); - } - - default: - throw new Error(`Unsupported percy mode: ${input.percyMode}`); - } -} - -/** - * Handles Percy + SDK flow with automatic fallback logic - * First tries Percy with SDK, automatically falls back to Percy Automate if unsupported - * User never directly chooses Percy Automate - it's always a fallback - */ -function handlePercyWithSDKFlow( - input: RunTestsOnBrowserStackInput, - config: BrowserStackConfig, -): RunTestsInstructionResult { - // Try Percy with SDK first - const percyWithSDKResult = runPercyWithSDK(input, config); - - // If Percy with SDK fails (not supported), automatically fallback to Percy Automate - // if (percyWithSDKResult.steps.some((step) => step.isError)) { - // This is an internal fallback - user never directly requests this - // return runPercyAutomateOnly(); - // } - - return percyWithSDKResult; -} diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 7a5ce90..6ff3847 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -3,43 +3,51 @@ import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum, + PercyIntegrationTypeEnum, } from "./types.js"; -export enum PercyMode { - PercyDisabled = "percy-disabled", // BrowserStack SDK only, no Percy - PercyWithSDK = "percy-on-browserstack-infra", // BrowserStack SDK + Percy integration (with fallback) - PercyWeb = "percy-on-local-infra", // Percy Web only (not implemented) -} - -// Internal enum for execution paths (includes fallback) -export enum InternalPercyMode { - PercyDisabled = "percy-disabled", - PercyWithSDK = "percy-on-browserstack-infra", - PercyAutomate = "percy-automate", - PercyWeb = "percy-on-local-infra", -} - -// Centralized enum for Percy (Zod) - Only user-facing options -export const PercyModeEnum = z.nativeEnum(PercyMode); - -// User-facing schema - only 3 options for user -export const RunTestsOnBrowserStackParamsShape = { +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( + "The type of Percy integration. 'web' for Percy Web SDK for local setups, 'app' for Percy App SDK.", + ), +}; + +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), - desiredPlatforms: z.array(z.enum(["windows", "macos", "android", "ios"])), - projectName: z.string(), - percyMode: PercyModeEnum.default(PercyMode.PercyDisabled).describe( - "Percy mode: No 'Percy' in user input → percy-disabled. 'Percy' without 'BrowserStack'/'Automate' → percy-on-local-infra. 'Percy' with 'BrowserStack' or 'Automate' → percy-on-browserstack-infra. Always map user requests to the correct tool and mode per these rules.", + 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."), + enablePercy: z + .boolean() + .default(false) + .describe( + "Set to true to enable Percy visual testing alongside your functional tests on BrowserStack.", + ), }; +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 index 1df343f..cac50d0 100644 --- a/src/tools/sdk-utils/common/types.ts +++ b/src/tools/sdk-utils/common/types.ts @@ -1,3 +1,8 @@ +export enum PercyIntegrationTypeEnum { + WEB = "web", + APP = "app", +} + export enum SDKSupportedLanguageEnum { nodejs = "nodejs", javascript = "javascript", diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index b58bea1..31461d3 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -1,5 +1,7 @@ -import { RunTestsOnBrowserStackSchema } from "./common/schema.js"; -import { buildRunTestsInstructions } from "./common/instructionBuilder.js"; +import { + SetUpPercySchema, + RunTestsOnBrowserStackSchema, +} from "./common/schema.js"; import { BOOTSTRAP_FAILED, IMPORTANT_SETUP_WARNING, @@ -10,70 +12,149 @@ import { } from "./common/formatUtils.js"; import { BrowserStackConfig } from "../../lib/types.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { + RunTestsInstructionResult, + 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 { runPercyWithSDK } from "./percy-bstack/handler.js"; + +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 async function runTestsOnBrowserStackHandler( rawInput: unknown, config: BrowserStackConfig, - projectName: string, ): Promise { try { - // Validate input with schema const input = RunTestsOnBrowserStackSchema.parse(rawInput); - // Build instructions and metadata - const { steps, requiresPercy, missingDependencies, shouldSkipFormatting } = - await buildRunTestsInstructions(input, config); + if (!input.enablePercy) { + const result = runBstackSDKOnly(input, config); + return await formatToolResult(result); + } else { + const percyWithSDKResult = runPercyWithSDK(input, config); - // If shouldSkipFormatting is true (for unsupported cases), return minimal response - if (shouldSkipFormatting) { - return { - content: steps.map((step: { content: string }) => ({ - type: "text" as const, - text: step.content, - })), - isError: steps.some((s: { isError?: boolean }) => s.isError), - steps, - requiresPercy, - missingDependencies, - }; + if (percyWithSDKResult.steps.some((step) => step.isError)) { + const { + detectedLanguage, + detectedBrowserAutomationFramework, + detectedTestingFramework, + } = input; + return { + content: [ + { + type: "text", + text: `Percy is not supported for this configuration using the BrowserStack SDK. Language: ${detectedLanguage} Framework: ${detectedBrowserAutomationFramework} Testing Framework: ${detectedTestingFramework} Would you like to try running this with the Percy SDK instead?`, + }, + ], + isError: true, + shouldSkipFormatting: true, + }; + } + return await formatToolResult(percyWithSDKResult); } + } catch (error) { + return { + content: [ + { + type: "text", + text: BOOTSTRAP_FAILED(error, { + config, + percyMode: (rawInput as any)?.enablePercy + ? "percy-on-bstack" + : "bstack-only", + }), + }, + ], + isError: true, + }; + } +} - // Combine all step content into a single string for formatting - const combinedInstructions = steps - .map((step: { content: string }) => step.content) - .join("\n"); - - // Apply step numbering using the formatInstructionsWithNumbers function - const { formattedSteps, stepCount } = - formatInstructionsWithNumbers(combinedInstructions); +export async function setUpPercyHandler( + rawInput: unknown, + config: BrowserStackConfig, +): Promise { + try { + const input = SetUpPercySchema.parse(rawInput); + const authorization = getBrowserStackAuth(config); - // Generate verification message - const verificationMessage = generateVerificationMessage(stepCount); + // Determine options for fetchPercyToken based on integrationType + let percyTokenOptions = {}; + if (input.integrationType === PercyIntegrationTypeEnum.WEB) { + percyTokenOptions = { type: PercyIntegrationTypeEnum.WEB }; + } else if (input.integrationType === PercyIntegrationTypeEnum.APP) { + percyTokenOptions = { type: PercyIntegrationTypeEnum.APP }; + } - // Create the final content with setup warning, formatted instructions, and verification - const finalContent = [ - { - type: "text" as const, - text: IMPORTANT_SETUP_WARNING, - }, - { - type: "text" as const, - text: formattedSteps, - }, - { - type: "text" as const, - text: verificationMessage, - }, - ]; + const percyToken = await fetchPercyToken( + input.projectName, + authorization, + percyTokenOptions, + ); - // Structured output - return { - content: finalContent, - isError: steps.some((s: { isError?: boolean }) => s.isError), - steps, - requiresPercy, - missingDependencies, + // Create adapter object for Percy handlers + const percyInput = { + projectName: input.projectName, + detectedLanguage: input.detectedLanguage, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, + detectedTestingFramework: input.detectedTestingFramework, + integrationType: input.integrationType, }; + + let result: RunTestsInstructionResult; + + if (input.integrationType === PercyIntegrationTypeEnum.WEB) { + result = runPercyWeb(percyInput, percyToken || "YOUR_PERCY_TOKEN_HERE"); + } else { + result = runPercyAutomateOnly( + percyInput, + percyToken || "YOUR_PERCY_TOKEN_HERE", + ); + } + + return await formatToolResult(result); } catch (error) { return { content: [ @@ -81,7 +162,7 @@ export async function runTestsOnBrowserStackHandler( type: "text", text: BOOTSTRAP_FAILED(error, { config, - percyMode: (rawInput as any)?.percyMode, + percyMode: (rawInput as any)?.integrationType, }), }, ], 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 0000000..89619c9 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -0,0 +1,282 @@ +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 percyAutomateAggressiveInstruction = ` +**MANDATORY: Aggressive, Exhaustive Percy Automate Integration** + +You MUST follow these steps with NO EXCEPTIONS: + +1. Recursively search the ENTIRE target directory and ALL subdirectories for EVERY file relevant to the language/framework. +2. For EACH relevant file, analyze the code to find EVERY location where a Percy snapshot (or equivalent) should be called (e.g., after each navigation, UI change, or significant state in tests). Insert snapshot calls at ALL such points, not just the first or most obvious one. +3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). +4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. +5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. +`; + +export const pythonPytestPercyAutomateInstructions = ` +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 +${percyAutomateAggressiveInstruction} + - Import the Percy snapshot helper: + from percy import percy_snapshot + - In your test, take snapshots at key points: + percy_snapshot(driver, "Your snapshot name") + +Example: +\`\`\`python +import pytest +from selenium import webdriver +from percy import percy_snapshot + +@pytest.fixture +def driver(): + driver = webdriver.Chrome() + yield driver + driver.quit() + +def test_homepage(driver): + driver.get("http://localhost:8000") + percy_snapshot(driver, "Home page") + # ... more test steps ... + percy_snapshot(driver, "After login") +\`\`\` + +---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 +${percyAutomateAggressiveInstruction} + - 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} +`; + +export const testngPercyAutomateInstructions = ` +---STEP--- +Install or upgrade BrowserStack SDK + - Install the BrowserStack SDK using Maven: + mvn archetype:generate -B -DarchetypeGroupId=com.browserstack -DarchetypeArtifactId=browserstack-sdk-archetype-integrate -DgroupId=com.browserstack -DartifactId=browserstack-sdk-archetype-integrate -DBROWSERSTACK_USERNAME=YOUR_USERNAME -DBROWSERSTACK_ACCESS_KEY=YOUR_ACCESS_KEY -DBROWSERSTACK_FRAMEWORK=testng + +---STEP--- +Update your browsersstack.yml config file + 1. Set \`percy: true\` + 2. Set a \`projectName\` + 3. Set \`percyCaptureMode: manual\` + +---STEP--- +Update your TestNG Script +${percyAutomateAggressiveInstruction} + 1. Import the BrowserStack Percy SDK in your test script: + import com.browserstack.PercySDK; + 2. Add the \`PercySDK.screenshot(driver, name)\` method at required points in your test script. + +Example: +\`\`\`java +// ...imports +import com.browserstack.PercySDK; +public class TestNG extends SeleniumTest { + @Test + public void test() throws Exception { + // ... + PercySDK.screenshot(driver, "My Screenshot"); + // ... + } +} +\`\`\` + +---STEP--- +Run your test script + - npx percy exec -- mvn test -P sample-percy-test + +${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('browsersstack-node-sdk'); + 2. Add the \`await percy.screenshot(driver, name)\` method at required points in your test script. + +Example: +\`\`\`javascript +const { percy } = require('browsersstack-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 +${percyAutomateAggressiveInstruction} + - 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} +`; 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 0000000..49b2dea --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/frameworks.ts @@ -0,0 +1,22 @@ +import { ConfigMapping } from "./types.js"; +import * as instructions from "./constants.js"; + +export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { + python: { + pytest: { + instructions: instructions.pythonPytestPercyAutomateInstructions, + }, + }, + javascript: { + cypress: { instructions: instructions.jsCypressPercyAutomateInstructions }, + mocha: { instructions: instructions.mochaPercyAutomateInstructions }, + jest: { instructions: instructions.jestPercyAutomateInstructions }, + webdriverio: { + instructions: instructions.webdriverioPercyAutomateInstructions, + }, + testcafe: { instructions: instructions.testcafePercyAutomateInstructions }, + }, + java: { + testng: { instructions: instructions.testngPercyAutomateInstructions }, + }, +}; diff --git a/src/tools/sdk-utils/percy-automate/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts index b7ad3b3..1af51d9 100644 --- a/src/tools/sdk-utils/percy-automate/handler.ts +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -1,43 +1,88 @@ -// Handler for Percy Automate only (fallback when Percy SDK not supported) -import { RunTestsInstructionResult } from "../common/types.js"; -import { PERCY_AUTOMATE_NOT_IMPLEMENTED } from "../common/errorMessages.js"; - -// Placeholder function for Percy Automate fallback -// Returns null if not supported, instructions string if supported -function getPercyAutomateInstructions(): string | null { - return "It worked as a fallback for Percy Automate."; -} +// Handler for Percy Automate 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 { SDKSupportedLanguage } from "../common/types.js"; +import { + PercyAutomateNotImplementedType, + getPercyAutomateNotImplementedMessage, +} from "../common/errorMessages.js"; + +export function runPercyAutomateOnly( + input: SetUpPercyInput, + percyToken: string, +): RunTestsInstructionResult { + const steps: RunTestsStep[] = []; + + // Check if this configuration is supported for Percy Automate + const languageConfig = + SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; + + if (!languageConfig) { + return { + steps: [ + { + type: "error", + title: "Language Not Supported", + content: getPercyAutomateNotImplementedMessage( + PercyAutomateNotImplementedType.LANGUAGE, + input, + Object.keys(SUPPORTED_CONFIGURATIONS), + ), + isError: true, + }, + ], + requiresPercy: true, + missingDependencies: [], + shouldSkipFormatting: true, + }; + } -export function runPercyAutomateOnly(): RunTestsInstructionResult { - const percyAutomateInstructions = getPercyAutomateInstructions(); + const testingFrameworkConfig = languageConfig[input.detectedTestingFramework]; - if (percyAutomateInstructions) { + if (!testingFrameworkConfig) { return { steps: [ { - type: "instruction", - title: "Percy Automate Setup (Fallback)", - content: percyAutomateInstructions, + type: "error", + title: "Testing Framework Not Supported", + content: getPercyAutomateNotImplementedMessage( + PercyAutomateNotImplementedType.FRAMEWORK, + { + ...input, + detectedBrowserAutomationFramework: + input.detectedTestingFramework, + }, + Object.keys(languageConfig), + ), + isError: true, }, ], requiresPercy: true, missingDependencies: [], - shouldSkipFormatting: false, + shouldSkipFormatting: true, }; } - // Percy Automate not supported - skip formatting for error case + // Generate instructions for the supported configuration with project name + const instructions = testingFrameworkConfig.instructions; + + // Prepend a step to set the Percy token in the environment + steps.push({ + type: "instruction", + title: "Set Percy Token in Environment", + content: `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.)`, + }); + + steps.push({ + type: "instruction", + title: `Percy Automate Setup for ${input.detectedLanguage} with ${input.detectedTestingFramework}`, + content: instructions, + }); + return { - steps: [ - { - type: "error", - title: "Percy Automate Not Supported", - content: PERCY_AUTOMATE_NOT_IMPLEMENTED, - isError: true, - }, - ], + steps, requiresPercy: true, missingDependencies: [], - shouldSkipFormatting: true, }; } 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 0000000..9ad18c2 --- /dev/null +++ b/src/tools/sdk-utils/percy-automate/types.ts @@ -0,0 +1,11 @@ +/** + * Type for Percy Automate configuration mapping. + * Structure: language -> testingFramework -> { instructions: (projectName: string) => string } + */ +export type ConfigMapping = { + [language: string]: { + [testingFramework: string]: { + instructions: string; + }; + }; +}; diff --git a/src/tools/sdk-utils/percy-bstack/instructions.ts b/src/tools/sdk-utils/percy-bstack/instructions.ts index e544594..d748e1e 100644 --- a/src/tools/sdk-utils/percy-bstack/instructions.ts +++ b/src/tools/sdk-utils/percy-bstack/instructions.ts @@ -34,8 +34,8 @@ export function getPercyInstructions( export function formatPercyInstructions(instructions: { instructions: string; }): string { - return `## Percy Visual Testing Setup + 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.instructions} `; -} +} diff --git a/src/tools/sdk-utils/percy-web/fetchPercyToken.ts b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts index 0752b17..7b0579b 100644 --- a/src/tools/sdk-utils/percy-web/fetchPercyToken.ts +++ b/src/tools/sdk-utils/percy-web/fetchPercyToken.ts @@ -1,33 +1,45 @@ -//Fetches a Percy token for a given project name from the BrowserStack API. -// Returns the token if successful, or throws an error if not. +import { PercyIntegrationTypeEnum } from "../common/types.js"; export async function fetchPercyToken( projectName: string, authorization: string, + options: { type?: PercyIntegrationTypeEnum } = {}, ): Promise { try { - const encodedAuth = `Basic ${Buffer.from(authorization).toString("base64")}`; - const response = await fetch( - `https://api.browserstack.com/api/app_percy/get_project_token?name=${encodeURIComponent(projectName)}&type=web`, - { - headers: { - Authorization: encodedAuth, - }, + 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: ${response.status}`); + throw new Error( + `Failed to fetch Percy token (status: ${response.status})`, + ); } + const data = await response.json(); - if (!data || !data.token || !data.success) { + + if (!data?.token || !data?.success) { throw new Error( - "Looks like project already exists but uses automate for running tests. Use different project name all together.", + "Project exists but is likely set up for Automate. Please use a different project name.", ); } + return data.token; - } catch (error) { - throw new Error( - `Error retrieving Percy token: ${error instanceof Error ? error.message : "Unknown error"}`, - ); + } 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/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts index e74f8f0..6bb10d5 100644 --- a/src/tools/sdk-utils/percy-web/handler.ts +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -1,15 +1,14 @@ // Handler for Percy Web only mode - Visual testing without BrowserStack infrastructure import { RunTestsInstructionResult, RunTestsStep } from "../common/types.js"; -import { RunTestsOnBrowserStackInput } from "../common/schema.js"; +import { SetUpPercyInput } from "../common/schema.js"; import { SUPPORTED_CONFIGURATIONS } from "./frameworks.js"; import { SDKSupportedBrowserAutomationFramework, - SDKSupportedTestingFramework, SDKSupportedLanguage, } from "../common/types.js"; export function runPercyWeb( - input: RunTestsOnBrowserStackInput, + input: SetUpPercyInput, percyToken: string, ): RunTestsInstructionResult { const steps: RunTestsStep[] = []; From 395153cec60d73e140f8af56285916320a0e5649 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 28 Jul 2025 19:14:42 +0530 Subject: [PATCH 04/23] Add framework compatibility checks for Percy integration types --- src/tools/sdk-utils/handler.ts | 64 +++++++++++++++---- .../sdk-utils/percy-automate/frameworks.ts | 13 ++++ src/tools/sdk-utils/percy-web/frameworks.ts | 13 ++++ 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 31461d3..8a2cc56 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -19,7 +19,9 @@ import { import { getBrowserStackAuth } from "../../lib/get-auth.js"; import { fetchPercyToken } from "./percy-web/fetchPercyToken.js"; import { runPercyWeb } from "./percy-web/handler.js"; +import { isPercyWebFrameworkSupported } from "./percy-web/frameworks.js"; import { runPercyAutomateOnly } from "./percy-automate/handler.js"; +import { isPercyAutomateFrameworkSupported } from "./percy-automate/frameworks.js"; import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithSDK } from "./percy-bstack/handler.js"; @@ -119,6 +121,56 @@ export async function setUpPercyHandler( const input = SetUpPercySchema.parse(rawInput); const authorization = getBrowserStackAuth(config); + // Create adapter object for Percy handlers + const percyInput = { + projectName: input.projectName, + detectedLanguage: input.detectedLanguage, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, + detectedTestingFramework: input.detectedTestingFramework, + integrationType: input.integrationType, + }; + + let result: RunTestsInstructionResult; + + if (input.integrationType === PercyIntegrationTypeEnum.APP) { + // Check framework compatibility before fetching token + const isSupported = isPercyAutomateFrameworkSupported( + input.detectedLanguage, + input.detectedTestingFramework + ); + if (!isSupported) { + return { + content: [ + { + type: "text", + text: `Percy Automate is not supported for this configuration. Language: ${input.detectedLanguage} Testing Framework: ${input.detectedTestingFramework}`, + }, + ], + isError: true, + shouldSkipFormatting: true, + }; + } + } else if (input.integrationType === PercyIntegrationTypeEnum.WEB) { + // Check framework compatibility before fetching token for Percy Web + const isSupported = isPercyWebFrameworkSupported( + input.detectedLanguage, + input.detectedBrowserAutomationFramework + ); + if (!isSupported) { + return { + content: [ + { + type: "text", + text: `Percy Web is not supported for this configuration. Language: ${input.detectedLanguage} Browser Automation Framework: ${input.detectedBrowserAutomationFramework}`, + }, + ], + isError: true, + shouldSkipFormatting: true, + }; + } + } + // Determine options for fetchPercyToken based on integrationType let percyTokenOptions = {}; if (input.integrationType === PercyIntegrationTypeEnum.WEB) { @@ -133,18 +185,6 @@ export async function setUpPercyHandler( percyTokenOptions, ); - // Create adapter object for Percy handlers - const percyInput = { - projectName: input.projectName, - detectedLanguage: input.detectedLanguage, - detectedBrowserAutomationFramework: - input.detectedBrowserAutomationFramework, - detectedTestingFramework: input.detectedTestingFramework, - integrationType: input.integrationType, - }; - - let result: RunTestsInstructionResult; - if (input.integrationType === PercyIntegrationTypeEnum.WEB) { result = runPercyWeb(percyInput, percyToken || "YOUR_PERCY_TOKEN_HERE"); } else { diff --git a/src/tools/sdk-utils/percy-automate/frameworks.ts b/src/tools/sdk-utils/percy-automate/frameworks.ts index 49b2dea..40769aa 100644 --- a/src/tools/sdk-utils/percy-automate/frameworks.ts +++ b/src/tools/sdk-utils/percy-automate/frameworks.ts @@ -20,3 +20,16 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { testng: { instructions: instructions.testngPercyAutomateInstructions }, }, }; + +/** + * Utility function to check if a given language and testing framework + * are supported by Percy Automate. + */ +export function isPercyAutomateFrameworkSupported( + 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/frameworks.ts b/src/tools/sdk-utils/percy-web/frameworks.ts index eb82d7b..071ac36 100644 --- a/src/tools/sdk-utils/percy-web/frameworks.ts +++ b/src/tools/sdk-utils/percy-web/frameworks.ts @@ -22,3 +22,16 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { playwright: { instructions: constants.csharpPlaywrightInstructions }, }, }; + +/** + * 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]; +} From ccf16a9a81a5c95d4edd106a76bb667b94b889b8 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 28 Jul 2025 19:26:07 +0530 Subject: [PATCH 05/23] Refactor Percy handler functions to remove unsupported configuration checks --- src/tools/sdk-utils/percy-automate/handler.ts | 51 +------------------ src/tools/sdk-utils/percy-web/handler.ts | 35 +------------ 2 files changed, 2 insertions(+), 84 deletions(-) diff --git a/src/tools/sdk-utils/percy-automate/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts index 1af51d9..3e221f5 100644 --- a/src/tools/sdk-utils/percy-automate/handler.ts +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -3,10 +3,6 @@ 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"; -import { - PercyAutomateNotImplementedType, - getPercyAutomateNotImplementedMessage, -} from "../common/errorMessages.js"; export function runPercyAutomateOnly( input: SetUpPercyInput, @@ -14,56 +10,11 @@ export function runPercyAutomateOnly( ): RunTestsInstructionResult { const steps: RunTestsStep[] = []; - // Check if this configuration is supported for Percy Automate + // Assume configuration is supported due to guardrails at orchestration layer const languageConfig = SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; - - if (!languageConfig) { - return { - steps: [ - { - type: "error", - title: "Language Not Supported", - content: getPercyAutomateNotImplementedMessage( - PercyAutomateNotImplementedType.LANGUAGE, - input, - Object.keys(SUPPORTED_CONFIGURATIONS), - ), - isError: true, - }, - ], - requiresPercy: true, - missingDependencies: [], - shouldSkipFormatting: true, - }; - } - const testingFrameworkConfig = languageConfig[input.detectedTestingFramework]; - if (!testingFrameworkConfig) { - return { - steps: [ - { - type: "error", - title: "Testing Framework Not Supported", - content: getPercyAutomateNotImplementedMessage( - PercyAutomateNotImplementedType.FRAMEWORK, - { - ...input, - detectedBrowserAutomationFramework: - input.detectedTestingFramework, - }, - Object.keys(languageConfig), - ), - isError: true, - }, - ], - requiresPercy: true, - missingDependencies: [], - shouldSkipFormatting: true, - }; - } - // Generate instructions for the supported configuration with project name const instructions = testingFrameworkConfig.instructions; diff --git a/src/tools/sdk-utils/percy-web/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts index 6bb10d5..8b00518 100644 --- a/src/tools/sdk-utils/percy-web/handler.ts +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -13,47 +13,14 @@ export function runPercyWeb( ): RunTestsInstructionResult { const steps: RunTestsStep[] = []; - // Check if this configuration is supported for Percy Web + // Assume configuration is supported due to guardrails at orchestration layer const languageConfig = SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; - - if (!languageConfig) { - return { - steps: [ - { - type: "error", - title: "Language Not Supported", - content: `Percy Web does not support the language: ${input.detectedLanguage}. Supported languages are: ${Object.keys(SUPPORTED_CONFIGURATIONS).join(", ")}.`, - isError: true, - }, - ], - requiresPercy: true, - missingDependencies: [], - shouldSkipFormatting: true, - }; - } - const frameworkConfig = languageConfig[ input.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework ]; - if (!frameworkConfig) { - return { - steps: [ - { - type: "error", - title: "Framework Not Supported", - content: `Percy Web does not support ${input.detectedBrowserAutomationFramework} for ${input.detectedLanguage}. Supported frameworks for ${input.detectedLanguage} are: ${Object.keys(languageConfig).join(", ")}.`, - isError: true, - }, - ], - requiresPercy: true, - missingDependencies: [], - shouldSkipFormatting: true, - }; - } - // Generate instructions for the supported configuration const instructions = frameworkConfig.instructions; From c2e6d8271490ff183123b76bd098d1d33dcb47f7 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Tue, 29 Jul 2025 02:14:27 +0530 Subject: [PATCH 06/23] Update tool descriptions and registration names for BrowserStack and Percy integration --- src/tools/bstack-sdk.ts | 4 ++-- src/tools/percy-sdk.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index de90b62..2087ef1 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -4,14 +4,14 @@ import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js" import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js"; const RUN_ON_BROWSERSTACK_DESCRIPTION = - "Use this tool to get setup instructions for running your functional tests on BrowserStack's cloud infrastructure. You can run tests using the BrowserStack SDK alone, or enable Percy visual testing integration (only where BrowserStack SDK has built-in Percy support). For standalone Percy setups (Percy Web or Percy Automate), use the Percy SDK tools instead."; +"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."; export function registerRunBrowserStackTestsTool( server: McpServer, config: BrowserStackConfig, ) { server.tool( - "runTestsOnBrowserStack", + "setupBrowserStackAutomatedTests", RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, async (args) => { diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 88f31ff..ddfc429 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,7 +7,7 @@ import { setUpPercyHandler } from "./sdk-utils/handler.js"; * Tool description for standalone Percy visual testing */ const SETUP_PERCY_DESCRIPTION = - "Use this tool to get setup instructions for standalone Percy visual testing. This sets up Percy with your own testing infrastructure: Percy Web for local projects, or Percy Automate for your own Selenium/Playwright grid. This does NOT integrate with BrowserStack's test execution infrastructure - use BrowserStack SDK tools for that."; + "Set up standalone Percy visual testing using the Percy SDK. Use for Percy Web (integrationType: 'web') or Percy Automate (integrationType: 'app'). Example prompts: set up Percy automate for this project; set up Percy web for this; set up Percy for this."; /** * Registers the standalone Percy setup tool with the MCP server. @@ -18,7 +18,7 @@ export function registerPercySetupTool( config: BrowserStackConfig, ) { server.tool( - "setUpPercy", + "setupPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, async (args) => { From 6bc96cdc6c15d0c7632963df0dbdaa2b7506bfe9 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Wed, 30 Jul 2025 15:27:17 +0530 Subject: [PATCH 07/23] Update Percy integration type and adjust related documentation for Automate SDK --- src/tools/sdk-utils/common/schema.ts | 2 +- src/tools/sdk-utils/common/types.ts | 2 +- src/tools/sdk-utils/handler.ts | 6 +++--- src/tools/sdk-utils/percy-automate/constants.ts | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 6ff3847..f1a7bc3 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -16,7 +16,7 @@ export const SetUpPercyParamsShape = { integrationType: z .nativeEnum(PercyIntegrationTypeEnum) .describe( - "The type of Percy integration. 'web' for Percy Web SDK for local setups, 'app' for Percy App SDK.", + "The type of Percy integration. 'web' for Percy Web SDK for local setups, 'automate' for Percy Automate SDK.", ), }; diff --git a/src/tools/sdk-utils/common/types.ts b/src/tools/sdk-utils/common/types.ts index cac50d0..6cc0e78 100644 --- a/src/tools/sdk-utils/common/types.ts +++ b/src/tools/sdk-utils/common/types.ts @@ -1,6 +1,6 @@ export enum PercyIntegrationTypeEnum { WEB = "web", - APP = "app", + AUTOMATE = "automate", } export enum SDKSupportedLanguageEnum { diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 8a2cc56..4748dd9 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -133,7 +133,7 @@ export async function setUpPercyHandler( let result: RunTestsInstructionResult; - if (input.integrationType === PercyIntegrationTypeEnum.APP) { + if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { // Check framework compatibility before fetching token const isSupported = isPercyAutomateFrameworkSupported( input.detectedLanguage, @@ -175,8 +175,8 @@ export async function setUpPercyHandler( let percyTokenOptions = {}; if (input.integrationType === PercyIntegrationTypeEnum.WEB) { percyTokenOptions = { type: PercyIntegrationTypeEnum.WEB }; - } else if (input.integrationType === PercyIntegrationTypeEnum.APP) { - percyTokenOptions = { type: PercyIntegrationTypeEnum.APP }; + } else if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { + percyTokenOptions = { type: PercyIntegrationTypeEnum.AUTOMATE }; } const percyToken = await fetchPercyToken( diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index 89619c9..068fd49 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -27,15 +27,15 @@ Install Percy Automate dependencies Update your Pytest test script ${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper: - from percy import percy_snapshot + from percy import percy_screenshot - In your test, take snapshots at key points: - percy_snapshot(driver, "Your snapshot name") + percy_screenshot(driver, "Your snapshot name") Example: \`\`\`python import pytest from selenium import webdriver -from percy import percy_snapshot +from percy import percy_screenshot @pytest.fixture def driver(): @@ -45,9 +45,9 @@ def driver(): def test_homepage(driver): driver.get("http://localhost:8000") - percy_snapshot(driver, "Home page") + percy_screenshot(driver, "Home page") # ... more test steps ... - percy_snapshot(driver, "After login") + percy_screenshot(driver, "After login") \`\`\` ---STEP--- From c67e85713192b9c7790d2a809f31434d0e006689 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 00:13:29 +0530 Subject: [PATCH 08/23] Refactor BrowserStack and Percy integration tools for improved setup instructions and error handling --- src/tools/bstack-sdk.ts | 2 +- src/tools/percy-sdk.ts | 2 +- src/tools/sdk-utils/bstack/constants.ts | 71 ++++++++++++++++--- src/tools/sdk-utils/bstack/sdkHandler.ts | 27 ++++--- .../{errorMessages.ts => commonMessages.ts} | 12 ++++ src/tools/sdk-utils/common/index.ts | 2 +- src/tools/sdk-utils/common/types.ts | 2 +- src/tools/sdk-utils/handler.ts | 68 +++++++++++++++--- .../sdk-utils/percy-automate/constants.ts | 12 +--- src/tools/sdk-utils/percy-automate/handler.ts | 2 +- src/tools/sdk-utils/percy-bstack/constants.ts | 14 ++-- src/tools/sdk-utils/percy-bstack/handler.ts | 32 +++++++-- src/tools/sdk-utils/percy-web/constants.ts | 34 +++------ src/tools/sdk-utils/percy-web/handler.ts | 2 +- 14 files changed, 201 insertions(+), 81 deletions(-) rename src/tools/sdk-utils/common/{errorMessages.ts => commonMessages.ts} (65%) diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index 2087ef1..171b849 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -11,7 +11,7 @@ export function registerRunBrowserStackTestsTool( config: BrowserStackConfig, ) { server.tool( - "setupBrowserStackAutomatedTests", + "setupBrowserStackAutomateTests", RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, async (args) => { diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index ddfc429..90d6210 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,7 +7,7 @@ import { setUpPercyHandler } from "./sdk-utils/handler.js"; * Tool description for standalone Percy visual testing */ const SETUP_PERCY_DESCRIPTION = - "Set up standalone Percy visual testing using the Percy SDK. Use for Percy Web (integrationType: 'web') or Percy Automate (integrationType: 'app'). Example prompts: set up Percy automate for this project; set up Percy web for this; set up Percy for this."; + "Set up standalone Percy visual testing using the Percy Web SDK. This tool is only for Percy Web (integrationType: 'web'), for users who want to use Percy on their own infrastructure. For Percy Automate, use the setupBrowserStackAutomateTests tool first to setup automate flow and then use this tool but do not run the tests before setting up Percy using this tool"; /** * Registers the standalone Percy setup tool with the MCP server. diff --git a/src/tools/sdk-utils/bstack/constants.ts b/src/tools/sdk-utils/bstack/constants.ts index b575faf..dd697e0 100644 --- a/src/tools/sdk-utils/bstack/constants.ts +++ b/src/tools/sdk-utils/bstack/constants.ts @@ -2,7 +2,8 @@ * ---------- PYTHON INSTRUCTIONS ---------- */ -export const pythonInstructions = (username: string, accessKey: string) => ` +export const pythonInstructions = (username: string, accessKey: string) => { + const setup = ` ---STEP--- Install the BrowserStack SDK: @@ -16,7 +17,9 @@ Setup the BrowserStack SDK with your credentials: \`\`\`bash browserstack-sdk setup --username "${username}" --key "${accessKey}" \`\`\` +`; + const run = ` ---STEP--- Run your tests on BrowserStack: @@ -25,8 +28,12 @@ browserstack-sdk python \`\`\` `; + return { setup, run }; +}; + export const generatePythonFrameworkInstructions = - (framework: string) => (username: string, accessKey: string) => ` + (framework: string) => (username: string, accessKey: string) => { + const setup = ` ---STEP--- Install the BrowserStack SDK: @@ -41,7 +48,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: @@ -50,6 +59,9 @@ browserstack-sdk ${framework} \`\`\` `; + return { setup, run }; + }; + export const robotInstructions = generatePythonFrameworkInstructions("robot"); export const behaveInstructions = generatePythonFrameworkInstructions("behave"); export const pytestInstructions = generatePythonFrameworkInstructions("pytest"); @@ -61,7 +73,8 @@ export const pytestInstructions = generatePythonFrameworkInstructions("pytest"); const argsInstruction = '-javaagent:"${com.browserstack:browserstack-java-sdk:jar}"'; -export 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\`: @@ -90,7 +103,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: @@ -104,6 +119,9 @@ gradle clean test \`\`\` `; + return { setup, run }; +}; + /** * ---------- CSharp INSTRUCTIONS ---------- */ @@ -111,7 +129,8 @@ gradle clean test export const csharpCommonInstructions = ( username: string, accessKey: string, -) => ` +) => { + const setup = ` ---STEP--- Install BrowserStack TestAdapter NuGet package: @@ -160,7 +179,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: @@ -174,10 +195,14 @@ Run the tests: \`\`\` `; + return { setup, run }; +}; + export const csharpPlaywrightCommonInstructions = ( username: string, accessKey: string, -) => ` +) => { + const setup = ` ---STEP--- Install BrowserStack TestAdapter NuGet package: @@ -239,7 +264,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: @@ -253,11 +280,15 @@ Run the tests: \`\`\` `; + return { setup, run }; +}; + /** * ---------- NODEJS INSTRUCTIONS ---------- */ -export 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: @@ -284,6 +315,18 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} \`\`\` `; + const run = ` +---STEP--- + +Run your tests on BrowserStack: +\`\`\`bash +npm run test:browserstack +\`\`\` +`; + + return { setup, run }; +}; + /** * ---------- EXPORT CONFIG ---------- */ @@ -291,7 +334,8 @@ export BROWSERSTACK_ACCESS_KEY=${accessKey} export const webdriverioInstructions = ( username: string, accessKey: string, -) => ` +) => { + const setup = ` ---STEP--- Set BrowserStack Credentials: @@ -390,14 +434,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. `; -export const cypressInstructions = (username: string, accessKey: string) => ` + return { setup, run }; +}; + +export const cypressInstructions = (username: string, accessKey: string) => { + const setup = ` ---STEP--- Install the BrowserStack Cypress CLI: @@ -458,7 +508,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: @@ -469,3 +521,6 @@ npx browserstack-cypress run --sync After the tests complete, you can view the results on your [BrowserStack Automate Dashboard](https://automate.browserstack.com/dashboard/). `; + + return { setup, run }; +}; diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 7174187..a63a7e9 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -15,6 +15,7 @@ import { export function runBstackSDKOnly( input: RunTestsOnBrowserStackInput, config: BrowserStackConfig, + isPercyAutomate = false, ): RunTestsInstructionResult { const steps: RunTestsStep[] = []; const authString = getBrowserStackAuth(config); @@ -33,12 +34,6 @@ export function runBstackSDKOnly( accessKey, ); - steps.push({ - type: "instruction", - title: "Framework-Specific Setup", - content: frameworkInstructions, - }); - return { steps, requiresPercy: false, @@ -85,11 +80,21 @@ export function runBstackSDKOnly( ); if (frameworkInstructions) { - steps.push({ - type: "instruction", - title: "Framework-Specific Setup", - content: 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 { diff --git a/src/tools/sdk-utils/common/errorMessages.ts b/src/tools/sdk-utils/common/commonMessages.ts similarity index 65% rename from src/tools/sdk-utils/common/errorMessages.ts rename to src/tools/sdk-utils/common/commonMessages.ts index 7f24e59..a081ee3 100644 --- a/src/tools/sdk-utils/common/errorMessages.ts +++ b/src/tools/sdk-utils/common/commonMessages.ts @@ -44,3 +44,15 @@ Config: ${JSON.stringify(context.config, null, 2)} Percy Mode: ${context.percyMode ?? "automate"} SDK Version: ${context.sdkVersion ?? "N/A"} Please open an issue on GitHub if the problem persists.`; + +export const percyAutomateAggressiveInstruction = ` +**MANDATORY: Aggressive, Exhaustive Percy Automate Integration** + +You MUST follow these steps with NO EXCEPTIONS: + +1. Recursively search the ENTIRE target directory and ALL subdirectories for EVERY file relevant to the language/framework. +2. For EACH relevant file, analyze the code to find EVERY location where a Percy snapshot (or equivalent) should be called (e.g., after each navigation, UI change, or significant state in tests). Insert snapshot calls at ALL such points, not just the first or most obvious one. +3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). +4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. +5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. +`; \ No newline at end of file diff --git a/src/tools/sdk-utils/common/index.ts b/src/tools/sdk-utils/common/index.ts index 9771d7d..1c89809 100644 --- a/src/tools/sdk-utils/common/index.ts +++ b/src/tools/sdk-utils/common/index.ts @@ -1,4 +1,4 @@ // Common utilities and types for SDK tools export * from "./types.js"; -export * from "./errorMessages.js"; +export * from "./commonMessages.js"; export * from "./formatUtils.js"; diff --git a/src/tools/sdk-utils/common/types.ts b/src/tools/sdk-utils/common/types.ts index 6cc0e78..0bd9b93 100644 --- a/src/tools/sdk-utils/common/types.ts +++ b/src/tools/sdk-utils/common/types.ts @@ -55,7 +55,7 @@ export type ConfigMapping = Partial< Partial< Record< SDKSupportedTestingFrameworkEnum, - { instructions: (username: string, accessKey: string) => string } + { instructions: (username: string, accessKey: string) => { setup: string; run: string } } > > > diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 4748dd9..c284124 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -5,7 +5,7 @@ import { import { BOOTSTRAP_FAILED, IMPORTANT_SETUP_WARNING, -} from "./common/errorMessages.js"; +} from "./common/commonMessages.js"; import { formatInstructionsWithNumbers, generateVerificationMessage, @@ -78,20 +78,68 @@ export async function runTestsOnBrowserStackHandler( if (percyWithSDKResult.steps.some((step) => step.isError)) { const { + projectName, detectedLanguage, detectedBrowserAutomationFramework, detectedTestingFramework, } = input; - return { - content: [ - { - type: "text", - text: `Percy is not supported for this configuration using the BrowserStack SDK. Language: ${detectedLanguage} Framework: ${detectedBrowserAutomationFramework} Testing Framework: ${detectedTestingFramework} Would you like to try running this with the Percy SDK instead?`, - }, - ], - isError: true, - shouldSkipFormatting: true, + + // Check if standalone Percy Automate supports this configuration. + const isStandaloneSupported = isPercyAutomateFrameworkSupported( + detectedLanguage, + detectedTestingFramework, + ); + + if (!isStandaloneSupported) { + // If fallback is also not supported, return a definitive error. + const errorMessage = `Percy is not supported for this configuration with either BrowserStack SDK or the standalone Percy SDK. + - Language: ${detectedLanguage} + - Browser Automation Framework: ${detectedBrowserAutomationFramework} + - Testing Framework: ${detectedTestingFramework} + Please try running without Percy or check for a supported configuration.`; + return { + content: [{ type: "text", text: errorMessage }], + isError: true, + shouldSkipFormatting: true, + }; + } + + // Standalone Percy Automate is supported, proceed with its setup flow. + const percyAutomateInput = { + projectName, + detectedLanguage, + detectedBrowserAutomationFramework, + detectedTestingFramework, + integrationType: PercyIntegrationTypeEnum.AUTOMATE, }; + const authorization = getBrowserStackAuth(config); + + // 1. Get BrowserStack SDK setup steps (for Automate, without Percy) + const sdkResult = await runBstackSDKOnly(input, config,true); + + // 2. Get Percy Automate setup steps + const percyAutomateResult = await runPercyAutomateOnly( + percyAutomateInput, + "YOUR_PERCY_TOKEN_HERE", + ); + + // 3. Combine steps: warning, SDK steps, Percy Automate steps + const combinedSteps = [ + { + type: "warning" as const, + content: `Note: Percy with the BrowserStack SDK is not supported for your project's configuration (Language: ${detectedLanguage}, Testing Framework: ${detectedTestingFramework}). Falling back to the standalone Percy Automate SDK setup. You must set up both BrowserStack Automate and Percy Automate.`, + title: "Percy Automate Fallback", + isError: false, + }, + ...(sdkResult.steps || []), + ...(percyAutomateResult.steps || []), + ]; + + // 4. Pass combined steps to formatToolResult + return await formatToolResult({ + ...percyAutomateResult, + steps: combinedSteps, + }); } return await formatToolResult(percyWithSDKResult); } diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index 068fd49..4b2c9d3 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -1,20 +1,10 @@ +import { percyAutomateAggressiveInstruction } from "../common/commonMessages.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 percyAutomateAggressiveInstruction = ` -**MANDATORY: Aggressive, Exhaustive Percy Automate Integration** - -You MUST follow these steps with NO EXCEPTIONS: - -1. Recursively search the ENTIRE target directory and ALL subdirectories for EVERY file relevant to the language/framework. -2. For EACH relevant file, analyze the code to find EVERY location where a Percy snapshot (or equivalent) should be called (e.g., after each navigation, UI change, or significant state in tests). Insert snapshot calls at ALL such points, not just the first or most obvious one. -3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). -4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. -5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. -`; export const pythonPytestPercyAutomateInstructions = ` Install Percy Automate dependencies diff --git a/src/tools/sdk-utils/percy-automate/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts index 3e221f5..dc33843 100644 --- a/src/tools/sdk-utils/percy-automate/handler.ts +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -22,7 +22,7 @@ export function runPercyAutomateOnly( steps.push({ type: "instruction", title: "Set Percy Token in Environment", - content: `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.)`, + 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({ diff --git a/src/tools/sdk-utils/percy-bstack/constants.ts b/src/tools/sdk-utils/percy-bstack/constants.ts index 722dd2e..cf06dde 100644 --- a/src/tools/sdk-utils/percy-bstack/constants.ts +++ b/src/tools/sdk-utils/percy-bstack/constants.ts @@ -1,10 +1,10 @@ -// Percy + BrowserStack SDK configuration constants +import { percyAutomateAggressiveInstruction } from "../common/commonMessages.js"; export const javaSeleniumInstructions = ` Import the BrowserStack Percy SDK in your test script: Add the Percy import to your test file. ----STEP--- +${percyAutomateAggressiveInstruction} 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. +${percyAutomateAggressiveInstruction} + ---STEP--- Add screenshot capture method at required points: @@ -60,7 +62,7 @@ In your WebdriverIO configuration file, modify the 'browserstack' service option - 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 = { }; \`\`\` +${percyAutomateAggressiveInstruction} + ---STEP--- Manually Capturing Screenshots (Optional): @@ -121,7 +125,7 @@ export const csharpSeleniumInstructions = ` Import the BrowserStack Percy SDK in your test script: Add the Percy import to your test file. ----STEP--- +${percyAutomateAggressiveInstruction} 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. diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index 8fe50d9..2140d25 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -61,11 +61,13 @@ export function runPercyWithSDK( accessKey, ); - steps.push({ - type: "instruction", - title: "Framework-Specific Setup", - content: frameworkInstructions, - }); + if (frameworkInstructions && frameworkInstructions.setup) { + steps.push({ + type: "instruction", + title: "Framework-Specific Setup", + content: frameworkInstructions.setup, + }); + } steps.push({ type: "instruction", @@ -73,6 +75,14 @@ export function runPercyWithSDK( content: formatPercyInstructions(percyResult), }); + if (frameworkInstructions && frameworkInstructions.run) { + steps.push({ + type: "instruction", + title: "Run the tests", + content: frameworkInstructions.run, + }); + } + return { steps, requiresPercy: true, @@ -118,11 +128,11 @@ export function runPercyWithSDK( accessKey, ); - if (frameworkInstructions) { + if (frameworkInstructions && frameworkInstructions.setup) { steps.push({ type: "instruction", title: "Framework-Specific Setup", - content: frameworkInstructions, + content: frameworkInstructions.setup, }); } @@ -132,6 +142,14 @@ export function runPercyWithSDK( content: formatPercyInstructions(percyResult), }); + if (frameworkInstructions && frameworkInstructions.run) { + steps.push({ + type: "instruction", + title: "Run the tests", + content: frameworkInstructions.run, + }); + } + return { steps, requiresPercy: true, diff --git a/src/tools/sdk-utils/percy-web/constants.ts b/src/tools/sdk-utils/percy-web/constants.ts index 3bbcf75..5a8a1d0 100644 --- a/src/tools/sdk-utils/percy-web/constants.ts +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -1,20 +1,10 @@ +import { percyAutomateAggressiveInstruction } from "../common/commonMessages.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 percyAggressiveInstruction = ` -**MANDATORY: Aggressive, Exhaustive Percy Integration** - -You MUST follow these steps with NO EXCEPTIONS: - -1. Recursively search the ENTIRE target directory and ALL subdirectories for EVERY file relevant to the language/framework. -2. For EACH relevant file, analyze the code to find EVERY location where a Percy snapshot (or equivalent) should be called (e.g., after each navigation, UI change, or significant state in tests). Insert snapshot calls at ALL such points, not just the first or most obvious one. -3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). -4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. -5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. -`; export const pythonInstructions = ` Install Percy dependencies @@ -24,7 +14,7 @@ Install Percy dependencies pip install percy-selenium Update your Python Selenium script -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} Example: \`\`\`python from selenium import webdriver @@ -32,7 +22,7 @@ from percy import percy_snapshot driver = webdriver.Chrome() driver.get('http://localhost:8000') - percy_snapshot(driver, 'Home page') +percy_snapshot(driver, 'Home page') # ... more test steps ... percy_snapshot(driver, 'After login') \`\`\` @@ -60,7 +50,7 @@ Install Percy dependencies npm install @percy/selenium-webdriver ---STEP--- Update your Node.js Selenium script -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper: const { percySnapshot } = require('@percy/selenium-js'); - In your test, take snapshots like this: @@ -100,7 +90,7 @@ Add Percy dependencies to your project ---STEP--- Update your Java Selenium test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper: import io.percy.selenium.Percy; - In your test, take snapshots like this: @@ -142,7 +132,7 @@ Install Percy dependencies ---STEP--- Update your Ruby Selenium test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Require the Percy snapshot helper: require 'percy' - In your test, take snapshots like this: @@ -167,8 +157,6 @@ Run Percy with your tests ${percyReviewSnapshotsStep} `; -/* rubyInstructions is already exported above */ - export const csharpInstructions = ` Install Percy CLI by running the following command: npm install --save-dev @percy/cli @@ -180,7 +168,7 @@ Add Percy dependencies to your project ---STEP--- Update your C# Selenium test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper: using PercyIO.Selenium; @@ -223,7 +211,7 @@ Install Percy dependencies ---STEP--- Update your Java Playwright test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy library and use the snapshot method: percy.snapshot("snapshot_1"); - You can also pass options: @@ -270,7 +258,7 @@ Install Percy dependencies ---STEP--- Update your Playwright JavaScript test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper: const percySnapshot = require('@percy/playwright'); - In your test, take snapshots like this: @@ -318,7 +306,7 @@ Install Percy dependencies ---STEP--- Update your Playwright Python test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper and use the snapshot method: percy_snapshot(page, name="Your snapshot name") - You can also use: @@ -356,7 +344,7 @@ Install Percy dependencies ---STEP--- Update your Playwright .NET test -${percyAggressiveInstruction} +${percyAutomateAggressiveInstruction} - Import the Percy snapshot helper and use the snapshot method: Percy.Snapshot(page, "Your snapshot name"); - You can also pass options: diff --git a/src/tools/sdk-utils/percy-web/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts index 8b00518..1cb6ecf 100644 --- a/src/tools/sdk-utils/percy-web/handler.ts +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -28,7 +28,7 @@ export function runPercyWeb( steps.push({ type: "instruction", title: "Set Percy Token in Environment", - content: `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.)`, + 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.)`, }); steps.push({ From 38fd22266bed36a6924592a929aeb1ee1e2ab97b Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 00:44:19 +0530 Subject: [PATCH 09/23] Update command for running Percy Automate with BrowserStack and Pytest --- src/tools/sdk-utils/percy-automate/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index 4b2c9d3..6ca3027 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -43,7 +43,7 @@ def test_homepage(driver): ---STEP--- Run Percy Automate with your tests - Use the following command: - npx percy exec -- + npx percy exec -- browserstack-sdk pytest ${percyAutomateReviewSnapshotsStep} `; From 7132f0a54928b4219903b89e10b2f3e662832c57 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 01:19:21 +0530 Subject: [PATCH 10/23] Add Percy tools integration and support checks for BrowserStack --- src/server-factory.ts | 1 + src/tools/bstack-sdk.ts | 3 +- src/tools/percy-sdk.ts | 6 +- src/tools/sdk-utils/common/utils.ts | 39 ++++++++++ src/tools/sdk-utils/handler.ts | 96 ++++++------------------ src/tools/sdk-utils/percy-web/handler.ts | 2 +- 6 files changed, 72 insertions(+), 75 deletions(-) create mode 100644 src/tools/sdk-utils/common/utils.ts diff --git a/src/server-factory.ts b/src/server-factory.ts index c15575b..734129c 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -50,6 +50,7 @@ export class BrowserStackMcpServer { const toolAdders = [ addAccessibilityTools, addSDKTools, + addPercyTools, addAppLiveTools, addBrowserLiveTools, addTestManagementTools, diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index 3ca0bef..4bc6715 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -10,7 +10,8 @@ export function registerRunBrowserStackTestsTool( server: McpServer, config: BrowserStackConfig, ) { - server.tool( + const tools: Record = {}; + tools.setupBrowserStackAutomateTests = server.tool( "setupBrowserStackAutomateTests", RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 90d6210..de62f2f 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,7 +7,7 @@ import { setUpPercyHandler } from "./sdk-utils/handler.js"; * Tool description for standalone Percy visual testing */ const SETUP_PERCY_DESCRIPTION = - "Set up standalone Percy visual testing using the Percy Web SDK. This tool is only for Percy Web (integrationType: 'web'), for users who want to use Percy on their own infrastructure. For Percy Automate, use the setupBrowserStackAutomateTests tool first to setup automate flow and then use this tool but do not run the tests before setting up Percy using this tool"; + "Set up standalone Percy visual testing using the Percy Web SDK. This tool is only for Percy Web (integrationType: 'web'), for users who want to use Percy on their own infrastructure. For Percy Automate, use the setupBrowserStackAutomateTests tool"; /** * Registers the standalone Percy setup tool with the MCP server. @@ -17,7 +17,8 @@ export function registerPercySetupTool( server: McpServer, config: BrowserStackConfig, ) { - server.tool( + const tools: Record = {}; + tools.setupPercyVisualTesting = server.tool( "setupPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, @@ -25,6 +26,7 @@ export function registerPercySetupTool( return setUpPercyHandler(args, config); }, ); + return tools; } export default registerPercySetupTool; diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts new file mode 100644 index 0000000..372a7e3 --- /dev/null +++ b/src/tools/sdk-utils/common/utils.ts @@ -0,0 +1,39 @@ +import { PercyIntegrationTypeEnum } from "../common/types.js"; +import { isPercyAutomateFrameworkSupported } from "../percy-automate/frameworks.js"; +import { isPercyWebFrameworkSupported } from "../percy-web/frameworks.js"; + +/** + * Utility to check Percy integration support for a given input. + * Returns { supported: boolean, errorMessage?: string } + */ +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.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 }; +} diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index c284124..797ff4c 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -19,11 +19,10 @@ import { import { getBrowserStackAuth } from "../../lib/get-auth.js"; import { fetchPercyToken } from "./percy-web/fetchPercyToken.js"; import { runPercyWeb } from "./percy-web/handler.js"; -import { isPercyWebFrameworkSupported } from "./percy-web/frameworks.js"; import { runPercyAutomateOnly } from "./percy-automate/handler.js"; -import { isPercyAutomateFrameworkSupported } from "./percy-automate/frameworks.js"; import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithSDK } from "./percy-bstack/handler.js"; +import { checkPercyIntegrationSupport } from "./common/utils.js"; async function formatToolResult( resultPromise: Promise | RunTestsInstructionResult, @@ -76,7 +75,8 @@ export async function runTestsOnBrowserStackHandler( } else { const percyWithSDKResult = runPercyWithSDK(input, config); - if (percyWithSDKResult.steps.some((step) => step.isError)) { + const hasPercySDKError = percyWithSDKResult.steps.some((step) => step.isError); + if (hasPercySDKError) { const { projectName, detectedLanguage, @@ -85,20 +85,15 @@ export async function runTestsOnBrowserStackHandler( } = input; // Check if standalone Percy Automate supports this configuration. - const isStandaloneSupported = isPercyAutomateFrameworkSupported( + const percyWithBrowserstackSDK = checkPercyIntegrationSupport({ + integrationType: PercyIntegrationTypeEnum.AUTOMATE, detectedLanguage, detectedTestingFramework, - ); + }); - if (!isStandaloneSupported) { - // If fallback is also not supported, return a definitive error. - const errorMessage = `Percy is not supported for this configuration with either BrowserStack SDK or the standalone Percy SDK. - - Language: ${detectedLanguage} - - Browser Automation Framework: ${detectedBrowserAutomationFramework} - - Testing Framework: ${detectedTestingFramework} - Please try running without Percy or check for a supported configuration.`; + if (!percyWithBrowserstackSDK.supported) { return { - content: [{ type: "text", text: errorMessage }], + content: [{ type: "text", text: percyWithBrowserstackSDK.errorMessage || "Percy Automate is not supported for this configuration." }], isError: true, shouldSkipFormatting: true, }; @@ -115,10 +110,10 @@ export async function runTestsOnBrowserStackHandler( const authorization = getBrowserStackAuth(config); // 1. Get BrowserStack SDK setup steps (for Automate, without Percy) - const sdkResult = await runBstackSDKOnly(input, config,true); + const sdkResult = runBstackSDKOnly(input, config,true); // 2. Get Percy Automate setup steps - const percyAutomateResult = await runPercyAutomateOnly( + const percyAutomateResult = runPercyAutomateOnly( percyAutomateInput, "YOUR_PERCY_TOKEN_HERE", ); @@ -176,71 +171,30 @@ export async function setUpPercyHandler( detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, detectedTestingFramework: input.detectedTestingFramework, - integrationType: input.integrationType, + integrationType: PercyIntegrationTypeEnum, }; - let result: RunTestsInstructionResult; - - if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { - // Check framework compatibility before fetching token - const isSupported = isPercyAutomateFrameworkSupported( - input.detectedLanguage, - input.detectedTestingFramework - ); - if (!isSupported) { - return { - content: [ - { - type: "text", - text: `Percy Automate is not supported for this configuration. Language: ${input.detectedLanguage} Testing Framework: ${input.detectedTestingFramework}`, - }, - ], - isError: true, - shouldSkipFormatting: true, - }; - } - } else if (input.integrationType === PercyIntegrationTypeEnum.WEB) { - // Check framework compatibility before fetching token for Percy Web - const isSupported = isPercyWebFrameworkSupported( - input.detectedLanguage, - input.detectedBrowserAutomationFramework - ); - if (!isSupported) { - return { - content: [ - { - type: "text", - text: `Percy Web is not supported for this configuration. Language: ${input.detectedLanguage} Browser Automation Framework: ${input.detectedBrowserAutomationFramework}`, - }, - ], - isError: true, - shouldSkipFormatting: true, - }; - } - } - - // Determine options for fetchPercyToken based on integrationType - let percyTokenOptions = {}; - if (input.integrationType === PercyIntegrationTypeEnum.WEB) { - percyTokenOptions = { type: PercyIntegrationTypeEnum.WEB }; - } else if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { - percyTokenOptions = { type: PercyIntegrationTypeEnum.AUTOMATE }; + const supportCheck = checkPercyIntegrationSupport(input); + if (!supportCheck.supported) { + return { + content: [ + { + type: "text", + text: supportCheck.errorMessage || "Percy integration not supported for this configuration.", + }, + ], + isError: true, + shouldSkipFormatting: true, + }; } const percyToken = await fetchPercyToken( input.projectName, authorization, - percyTokenOptions, + { type: PercyIntegrationTypeEnum.WEB }, ); - if (input.integrationType === PercyIntegrationTypeEnum.WEB) { - result = runPercyWeb(percyInput, percyToken || "YOUR_PERCY_TOKEN_HERE"); - } else { - result = runPercyAutomateOnly( - percyInput, - percyToken || "YOUR_PERCY_TOKEN_HERE", - ); - } + const result = runPercyWeb(percyInput, percyToken || "YOUR_PERCY_TOKEN_HERE"); return await formatToolResult(result); } catch (error) { diff --git a/src/tools/sdk-utils/percy-web/handler.ts b/src/tools/sdk-utils/percy-web/handler.ts index 1cb6ecf..6a739fe 100644 --- a/src/tools/sdk-utils/percy-web/handler.ts +++ b/src/tools/sdk-utils/percy-web/handler.ts @@ -28,7 +28,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.)`, + 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({ From f1456b8299ed376655991593ecc7f9b0f121ddd5 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 01:36:41 +0530 Subject: [PATCH 11/23] Enhance BrowserStack SDK integration --- src/tools/sdk-utils/bstack/sdkHandler.ts | 18 ++++++++ src/tools/sdk-utils/common/formatUtils.ts | 3 -- src/tools/sdk-utils/common/schema.ts | 1 - src/tools/sdk-utils/common/utils.ts | 47 ++++++++++++++++++++ src/tools/sdk-utils/handler.ts | 54 ++--------------------- 5 files changed, 69 insertions(+), 54 deletions(-) diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index a63a7e9..6c91ad3 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -34,6 +34,24 @@ export function runBstackSDKOnly( 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, diff --git a/src/tools/sdk-utils/common/formatUtils.ts b/src/tools/sdk-utils/common/formatUtils.ts index dafc160..4ed47d2 100644 --- a/src/tools/sdk-utils/common/formatUtils.ts +++ b/src/tools/sdk-utils/common/formatUtils.ts @@ -1,6 +1,3 @@ -/** - * Utilities for formatting instructions and generating verification messages. - */ export function formatInstructionsWithNumbers( instructionText: string, diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index f1a7bc3..61f1391 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -36,7 +36,6 @@ export const RunTestsOnBrowserStackParamsShape = { .describe("An array of platforms to run tests on."), enablePercy: z .boolean() - .default(false) .describe( "Set to true to enable Percy visual testing alongside your functional tests on BrowserStack.", ), diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts index 372a7e3..9cf435a 100644 --- a/src/tools/sdk-utils/common/utils.ts +++ b/src/tools/sdk-utils/common/utils.ts @@ -1,6 +1,15 @@ +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, +} from "./types.js"; +import { IMPORTANT_SETUP_WARNING } from "./index.js"; /** * Utility to check Percy integration support for a given input. @@ -37,3 +46,41 @@ export function checkPercyIntegrationSupport(input: { } 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, + }; +} \ No newline at end of file diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 797ff4c..834097c 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -2,20 +2,11 @@ import { SetUpPercySchema, RunTestsOnBrowserStackSchema, } from "./common/schema.js"; -import { - BOOTSTRAP_FAILED, - IMPORTANT_SETUP_WARNING, -} from "./common/commonMessages.js"; -import { - formatInstructionsWithNumbers, - generateVerificationMessage, -} from "./common/formatUtils.js"; +import {BOOTSTRAP_FAILED} from "./common/commonMessages.js"; +import {formatToolResult} from "./common/utils.js"; import { BrowserStackConfig } from "../../lib/types.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { - RunTestsInstructionResult, - PercyIntegrationTypeEnum, -} from "./common/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"; @@ -24,43 +15,6 @@ import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithSDK } from "./percy-bstack/handler.js"; import { checkPercyIntegrationSupport } from "./common/utils.js"; -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 async function runTestsOnBrowserStackHandler( rawInput: unknown, @@ -171,7 +125,7 @@ export async function setUpPercyHandler( detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, detectedTestingFramework: input.detectedTestingFramework, - integrationType: PercyIntegrationTypeEnum, + integrationType: PercyIntegrationTypeEnum.WEB, }; const supportCheck = checkPercyIntegrationSupport(input); From c7c5c5689b42a4a40e47bb343ba2e3886fd4c7c1 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 01:55:47 +0530 Subject: [PATCH 12/23] Refactor Percy token handling --- src/tools/sdk-utils/handler.ts | 13 +++++++++---- src/tools/sdk-utils/percy-automate/handler.ts | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 834097c..a89cbec 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -28,7 +28,6 @@ export async function runTestsOnBrowserStackHandler( return await formatToolResult(result); } else { const percyWithSDKResult = runPercyWithSDK(input, config); - const hasPercySDKError = percyWithSDKResult.steps.some((step) => step.isError); if (hasPercySDKError) { const { @@ -61,7 +60,13 @@ export async function runTestsOnBrowserStackHandler( detectedTestingFramework, integrationType: PercyIntegrationTypeEnum.AUTOMATE, }; + const authorization = getBrowserStackAuth(config); + const percyToken = await fetchPercyToken( + projectName, + authorization, + { type: PercyIntegrationTypeEnum.AUTOMATE }, + ); // 1. Get BrowserStack SDK setup steps (for Automate, without Percy) const sdkResult = runBstackSDKOnly(input, config,true); @@ -69,8 +74,8 @@ export async function runTestsOnBrowserStackHandler( // 2. Get Percy Automate setup steps const percyAutomateResult = runPercyAutomateOnly( percyAutomateInput, - "YOUR_PERCY_TOKEN_HERE", - ); + percyToken + ); // 3. Combine steps: warning, SDK steps, Percy Automate steps const combinedSteps = [ @@ -148,7 +153,7 @@ export async function setUpPercyHandler( { type: PercyIntegrationTypeEnum.WEB }, ); - const result = runPercyWeb(percyInput, percyToken || "YOUR_PERCY_TOKEN_HERE"); + const result = runPercyWeb(percyInput, percyToken); return await formatToolResult(result); } catch (error) { diff --git a/src/tools/sdk-utils/percy-automate/handler.ts b/src/tools/sdk-utils/percy-automate/handler.ts index dc33843..df0b3ad 100644 --- a/src/tools/sdk-utils/percy-automate/handler.ts +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -1,4 +1,3 @@ -// Handler for Percy Automate 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"; From ef7a337cfcbe1b85ee608bbaabb93b3d52a85a7e Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 01:56:53 +0530 Subject: [PATCH 13/23] linting --- src/index.ts | 1 - src/server-factory.ts | 3 +- src/tools/bstack-sdk.ts | 6 +-- src/tools/sdk-utils/bstack/sdkHandler.ts | 8 ++-- src/tools/sdk-utils/common/commonMessages.ts | 2 +- src/tools/sdk-utils/common/formatUtils.ts | 1 - src/tools/sdk-utils/common/types.ts | 7 ++- src/tools/sdk-utils/common/utils.ts | 10 ++--- src/tools/sdk-utils/handler.ts | 44 +++++++++++-------- .../sdk-utils/percy-automate/constants.ts | 1 - .../sdk-utils/percy-automate/frameworks.ts | 5 ++- .../sdk-utils/percy-bstack/instructions.ts | 2 +- src/tools/sdk-utils/percy-web/constants.ts | 1 - src/tools/sdk-utils/percy-web/frameworks.ts | 5 ++- 14 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2c5d1a7..dec3584 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,5 @@ process.on("exit", () => { logger.flush(); }); - export { setLogger } from "./logger.js"; export { BrowserStackMcpServer } from "./server-factory.js"; diff --git a/src/server-factory.ts b/src/server-factory.ts index 734129c..6c7afec 100644 --- a/src/server-factory.ts +++ b/src/server-factory.ts @@ -42,7 +42,6 @@ export class BrowserStackMcpServer { this.registerTools(); } - /** * Calls each tool-adder function and collects their returned tools */ @@ -83,7 +82,7 @@ export class BrowserStackMcpServer { public getTools(): Record { return this.tools; } - + public getTool(name: string): RegisteredTool | undefined { return this.tools[name]; } diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index 4bc6715..80a94a1 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -4,14 +4,14 @@ import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js" import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js"; const RUN_ON_BROWSERSTACK_DESCRIPTION = -"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."; + "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."; export function registerRunBrowserStackTestsTool( server: McpServer, config: BrowserStackConfig, ) { - const tools: Record = {}; - tools.setupBrowserStackAutomateTests = server.tool( + const tools: Record = {}; + tools.setupBrowserStackAutomateTests = server.tool( "setupBrowserStackAutomateTests", RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, diff --git a/src/tools/sdk-utils/bstack/sdkHandler.ts b/src/tools/sdk-utils/bstack/sdkHandler.ts index 6c91ad3..23957d7 100644 --- a/src/tools/sdk-utils/bstack/sdkHandler.ts +++ b/src/tools/sdk-utils/bstack/sdkHandler.ts @@ -39,10 +39,10 @@ export function runBstackSDKOnly( steps.push({ type: "instruction", title: "Framework-Specific Setup", - content: frameworkInstructions.setup , + content: frameworkInstructions.setup, }); } - + if (frameworkInstructions.run && !isPercyAutomate) { steps.push({ type: "instruction", @@ -102,10 +102,10 @@ export function runBstackSDKOnly( steps.push({ type: "instruction", title: "Framework-Specific Setup", - content: frameworkInstructions.setup , + content: frameworkInstructions.setup, }); } - + if (frameworkInstructions.run && !isPercyAutomate) { steps.push({ type: "instruction", diff --git a/src/tools/sdk-utils/common/commonMessages.ts b/src/tools/sdk-utils/common/commonMessages.ts index a081ee3..5dabe86 100644 --- a/src/tools/sdk-utils/common/commonMessages.ts +++ b/src/tools/sdk-utils/common/commonMessages.ts @@ -55,4 +55,4 @@ You MUST follow these steps with NO EXCEPTIONS: 3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). 4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. 5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. -`; \ No newline at end of file +`; diff --git a/src/tools/sdk-utils/common/formatUtils.ts b/src/tools/sdk-utils/common/formatUtils.ts index 4ed47d2..e50527b 100644 --- a/src/tools/sdk-utils/common/formatUtils.ts +++ b/src/tools/sdk-utils/common/formatUtils.ts @@ -1,4 +1,3 @@ - export function formatInstructionsWithNumbers( instructionText: string, separator: string = "---STEP---", diff --git a/src/tools/sdk-utils/common/types.ts b/src/tools/sdk-utils/common/types.ts index 0bd9b93..18b5545 100644 --- a/src/tools/sdk-utils/common/types.ts +++ b/src/tools/sdk-utils/common/types.ts @@ -55,7 +55,12 @@ export type ConfigMapping = Partial< Partial< Record< SDKSupportedTestingFrameworkEnum, - { instructions: (username: string, accessKey: string) => { setup: string; run: string } } + { + instructions: ( + username: string, + accessKey: string, + ) => { setup: string; run: string }; + } > > > diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts index 9cf435a..23a8b10 100644 --- a/src/tools/sdk-utils/common/utils.ts +++ b/src/tools/sdk-utils/common/utils.ts @@ -6,9 +6,7 @@ import { formatInstructionsWithNumbers, generateVerificationMessage, } from "./formatUtils.js"; -import { - RunTestsInstructionResult, -} from "./types.js"; +import { RunTestsInstructionResult } from "./types.js"; import { IMPORTANT_SETUP_WARNING } from "./index.js"; /** @@ -24,7 +22,7 @@ export function checkPercyIntegrationSupport(input: { if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { const isSupported = isPercyAutomateFrameworkSupported( input.detectedLanguage, - input.detectedTestingFramework || "" + input.detectedTestingFramework || "", ); if (!isSupported) { return { @@ -35,7 +33,7 @@ export function checkPercyIntegrationSupport(input: { } else if (input.integrationType === PercyIntegrationTypeEnum.WEB) { const isSupported = isPercyWebFrameworkSupported( input.detectedLanguage, - input.detectedBrowserAutomationFramework || "" + input.detectedBrowserAutomationFramework || "", ); if (!isSupported) { return { @@ -83,4 +81,4 @@ export async function formatToolResult( requiresPercy, missingDependencies, }; -} \ No newline at end of file +} diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index a89cbec..e6d58dc 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -2,11 +2,11 @@ import { SetUpPercySchema, RunTestsOnBrowserStackSchema, } from "./common/schema.js"; -import {BOOTSTRAP_FAILED} from "./common/commonMessages.js"; -import {formatToolResult} from "./common/utils.js"; +import { BOOTSTRAP_FAILED } from "./common/commonMessages.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 { 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"; @@ -15,7 +15,6 @@ import { runBstackSDKOnly } from "./bstack/sdkHandler.js"; import { runPercyWithSDK } from "./percy-bstack/handler.js"; import { checkPercyIntegrationSupport } from "./common/utils.js"; - export async function runTestsOnBrowserStackHandler( rawInput: unknown, config: BrowserStackConfig, @@ -28,7 +27,9 @@ export async function runTestsOnBrowserStackHandler( return await formatToolResult(result); } else { const percyWithSDKResult = runPercyWithSDK(input, config); - const hasPercySDKError = percyWithSDKResult.steps.some((step) => step.isError); + const hasPercySDKError = percyWithSDKResult.steps.some( + (step) => step.isError, + ); if (hasPercySDKError) { const { projectName, @@ -46,7 +47,14 @@ export async function runTestsOnBrowserStackHandler( if (!percyWithBrowserstackSDK.supported) { return { - content: [{ type: "text", text: percyWithBrowserstackSDK.errorMessage || "Percy Automate is not supported for this configuration." }], + content: [ + { + type: "text", + text: + percyWithBrowserstackSDK.errorMessage || + "Percy Automate is not supported for this configuration.", + }, + ], isError: true, shouldSkipFormatting: true, }; @@ -62,19 +70,17 @@ export async function runTestsOnBrowserStackHandler( }; const authorization = getBrowserStackAuth(config); - const percyToken = await fetchPercyToken( - projectName, - authorization, - { type: PercyIntegrationTypeEnum.AUTOMATE }, - ); + const percyToken = await fetchPercyToken(projectName, authorization, { + type: PercyIntegrationTypeEnum.AUTOMATE, + }); // 1. Get BrowserStack SDK setup steps (for Automate, without Percy) - const sdkResult = runBstackSDKOnly(input, config,true); + const sdkResult = runBstackSDKOnly(input, config, true); // 2. Get Percy Automate setup steps const percyAutomateResult = runPercyAutomateOnly( percyAutomateInput, - percyToken + percyToken, ); // 3. Combine steps: warning, SDK steps, Percy Automate steps @@ -139,7 +145,9 @@ export async function setUpPercyHandler( content: [ { type: "text", - text: supportCheck.errorMessage || "Percy integration not supported for this configuration.", + text: + supportCheck.errorMessage || + "Percy integration not supported for this configuration.", }, ], isError: true, @@ -147,11 +155,9 @@ export async function setUpPercyHandler( }; } - const percyToken = await fetchPercyToken( - input.projectName, - authorization, - { type: PercyIntegrationTypeEnum.WEB }, - ); + const percyToken = await fetchPercyToken(input.projectName, authorization, { + type: PercyIntegrationTypeEnum.WEB, + }); const result = runPercyWeb(percyInput, percyToken); diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index 6ca3027..5562f82 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -5,7 +5,6 @@ Review the snapshots - Go to your Percy project on https://percy.io to review snapshots and approve/reject any visual changes. `; - export const pythonPytestPercyAutomateInstructions = ` Install Percy Automate dependencies - Install Percy CLI: diff --git a/src/tools/sdk-utils/percy-automate/frameworks.ts b/src/tools/sdk-utils/percy-automate/frameworks.ts index 40769aa..e145441 100644 --- a/src/tools/sdk-utils/percy-automate/frameworks.ts +++ b/src/tools/sdk-utils/percy-automate/frameworks.ts @@ -27,9 +27,10 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { */ export function isPercyAutomateFrameworkSupported( language: string, - framework: string + framework: string, ): boolean { - const languageConfig = SUPPORTED_CONFIGURATIONS[language as keyof typeof SUPPORTED_CONFIGURATIONS]; + 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-bstack/instructions.ts b/src/tools/sdk-utils/percy-bstack/instructions.ts index d748e1e..dab7941 100644 --- a/src/tools/sdk-utils/percy-bstack/instructions.ts +++ b/src/tools/sdk-utils/percy-bstack/instructions.ts @@ -38,4 +38,4 @@ export function formatPercyInstructions(instructions: { To enable visual testing with Percy, you need to make the following changes to your project configuration and test scripts. ${instructions.instructions} `; -} +} diff --git a/src/tools/sdk-utils/percy-web/constants.ts b/src/tools/sdk-utils/percy-web/constants.ts index 5a8a1d0..c86ce05 100644 --- a/src/tools/sdk-utils/percy-web/constants.ts +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -5,7 +5,6 @@ Review the snapshots - Go to your Percy project on https://percy.io to review snapshots and approve/reject any visual changes. `; - export const pythonInstructions = ` Install Percy dependencies - Install Percy CLI: diff --git a/src/tools/sdk-utils/percy-web/frameworks.ts b/src/tools/sdk-utils/percy-web/frameworks.ts index 071ac36..a920cb9 100644 --- a/src/tools/sdk-utils/percy-web/frameworks.ts +++ b/src/tools/sdk-utils/percy-web/frameworks.ts @@ -29,9 +29,10 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { */ export function isPercyWebFrameworkSupported( language: string, - framework: string + framework: string, ): boolean { - const languageConfig = SUPPORTED_CONFIGURATIONS[language as keyof typeof SUPPORTED_CONFIGURATIONS]; + const languageConfig = + SUPPORTED_CONFIGURATIONS[language as keyof typeof SUPPORTED_CONFIGURATIONS]; if (!languageConfig) return false; return !!languageConfig[framework as keyof typeof languageConfig]; } From c5b07c5586595d9440f71f5239a0cace7205ac3f Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 12:31:16 +0530 Subject: [PATCH 14/23] Remove config logging from bootstrap failure message and delete unused instructionBuilder file --- src/tools/sdk-utils/common/commonMessages.ts | 1 - src/tools/sdk-utils/common/instructionBuilder.ts | 0 2 files changed, 1 deletion(-) delete mode 100644 src/tools/sdk-utils/common/instructionBuilder.ts diff --git a/src/tools/sdk-utils/common/commonMessages.ts b/src/tools/sdk-utils/common/commonMessages.ts index 5dabe86..c7cea86 100644 --- a/src/tools/sdk-utils/common/commonMessages.ts +++ b/src/tools/sdk-utils/common/commonMessages.ts @@ -40,7 +40,6 @@ export const BOOTSTRAP_FAILED = ( ) => `Failed to bootstrap project with BrowserStack SDK. Error: ${error} -Config: ${JSON.stringify(context.config, null, 2)} 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/common/instructionBuilder.ts b/src/tools/sdk-utils/common/instructionBuilder.ts deleted file mode 100644 index e69de29..0000000 From 970d3ec6cf266ed2f301b3b115618fc0734465c6 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 13:12:03 +0530 Subject: [PATCH 15/23] Remove integrationType from SetUpPercyParamsShape and update support check in setUpPercyHandler --- src/tools/sdk-utils/common/schema.ts | 6 ------ src/tools/sdk-utils/handler.ts | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 61f1391..bdeabd0 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -3,7 +3,6 @@ import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum, - PercyIntegrationTypeEnum, } from "./types.js"; export const SetUpPercyParamsShape = { @@ -13,11 +12,6 @@ export const SetUpPercyParamsShape = { SDKSupportedBrowserAutomationFrameworkEnum, ), detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), - integrationType: z - .nativeEnum(PercyIntegrationTypeEnum) - .describe( - "The type of Percy integration. 'web' for Percy Web SDK for local setups, 'automate' for Percy Automate SDK.", - ), }; export const RunTestsOnBrowserStackParamsShape = { diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index e6d58dc..2cc6dd5 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -139,7 +139,7 @@ export async function setUpPercyHandler( integrationType: PercyIntegrationTypeEnum.WEB, }; - const supportCheck = checkPercyIntegrationSupport(input); + const supportCheck = checkPercyIntegrationSupport(percyInput); if (!supportCheck.supported) { return { content: [ From 8e29b42d6df1e7c85b9198546d619b6a3265025b Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Thu, 31 Jul 2025 15:55:54 +0530 Subject: [PATCH 16/23] Update SETUP_PERCY_DESCRIPTION for clarity on tool usage and integration --- src/tools/percy-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index de62f2f..ade3655 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,7 +7,7 @@ import { setUpPercyHandler } from "./sdk-utils/handler.js"; * Tool description for standalone Percy visual testing */ const SETUP_PERCY_DESCRIPTION = - "Set up standalone Percy visual testing using the Percy Web SDK. This tool is only for Percy Web (integrationType: 'web'), for users who want to use Percy on their own infrastructure. For Percy Automate, use the setupBrowserStackAutomateTests tool"; + "This tool is intended only for Percy Web (integrationType: 'web') and is meant for users running Percy on their own infrastructure. If you're using Percy Automate, use the setupBrowserStackAutomateTests tool instead. If the user simply requests \"setup Percy\", and the codebase already uses BrowserStack Automate, default to using setupBrowserStackAutomateTests"; /** * Registers the standalone Percy setup tool with the MCP server. From 6185e9ef83ce02925223dc186746551f043b99b9 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 1 Aug 2025 11:10:40 +0530 Subject: [PATCH 17/23] Refactor BrowserStack and Percy integration descriptions for clarity and intent --- src/tools/bstack-sdk.ts | 2 +- src/tools/percy-sdk.ts | 2 +- src/tools/sdk-utils/common/schema.ts | 11 +- src/tools/sdk-utils/handler.ts | 196 +++++++++++++-------------- 4 files changed, 103 insertions(+), 108 deletions(-) diff --git a/src/tools/bstack-sdk.ts b/src/tools/bstack-sdk.ts index 80a94a1..17c819a 100644 --- a/src/tools/bstack-sdk.ts +++ b/src/tools/bstack-sdk.ts @@ -4,7 +4,7 @@ import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js" import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js"; const RUN_ON_BROWSERSTACK_DESCRIPTION = - "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."; + "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 function registerRunBrowserStackTestsTool( server: McpServer, diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index ade3655..8b91655 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,7 +7,7 @@ import { setUpPercyHandler } from "./sdk-utils/handler.js"; * Tool description for standalone Percy visual testing */ const SETUP_PERCY_DESCRIPTION = - "This tool is intended only for Percy Web (integrationType: 'web') and is meant for users running Percy on their own infrastructure. If you're using Percy Automate, use the setupBrowserStackAutomateTests tool instead. If the user simply requests \"setup Percy\", and the codebase already uses BrowserStack Automate, default to using setupBrowserStackAutomateTests"; + "Set up Percy visual testing for your project. Do not invoke this tool without clear user intent or codebase inspection. If the user says 'setup Percy' and the codebase contains BrowserStack Automate SDK or any yml related to it, always use 'automate'; otherwise, use 'web'. If the user says 'run Percy automate', always use 'automate'."; /** * Registers the standalone Percy setup tool with the MCP server. diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index bdeabd0..b6c4f8d 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -4,6 +4,7 @@ import { SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum, } from "./types.js"; +import { PercyIntegrationTypeEnum } from "./types.js"; export const SetUpPercyParamsShape = { projectName: z.string().describe("A unique name for your Percy project."), @@ -12,6 +13,11 @@ export const SetUpPercyParamsShape = { SDKSupportedBrowserAutomationFrameworkEnum, ), detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum), + integrationType: z + .nativeEnum(PercyIntegrationTypeEnum) + .describe( + "Set to 'automate' if the user says 'run Percy automate' or if the codebase contains BrowserStack Automate SDK or any related yml file. In all other cases, set to 'web'. This value must be provided explicitly or determined by clear codebase inspection—never inferred automatically." + ), }; export const RunTestsOnBrowserStackParamsShape = { @@ -28,11 +34,6 @@ export const RunTestsOnBrowserStackParamsShape = { desiredPlatforms: z .array(z.enum(["windows", "macos", "android", "ios"])) .describe("An array of platforms to run tests on."), - enablePercy: z - .boolean() - .describe( - "Set to true to enable Percy visual testing alongside your functional tests on BrowserStack.", - ), }; export const SetUpPercySchema = z.object(SetUpPercyParamsShape); diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 2cc6dd5..4705826 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -22,146 +22,140 @@ export async function runTestsOnBrowserStackHandler( try { const input = RunTestsOnBrowserStackSchema.parse(rawInput); - if (!input.enablePercy) { - const result = runBstackSDKOnly(input, config); + // Only handle BrowserStack SDK setup for functional/integration tests. + const result = runBstackSDKOnly(input, config); + return await formatToolResult(result); + } catch (error) { + return { + content: [ + { + type: "text", + text: BOOTSTRAP_FAILED(error, { config }), + }, + ], + isError: true, + }; + } +} + +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, + }; + + if (input.integrationType === PercyIntegrationTypeEnum.WEB) { + const supportCheck = checkPercyIntegrationSupport(percyInput); + if (!supportCheck.supported) { + return { + content: [ + { + type: "text", + text: + supportCheck.errorMessage || + "Percy Web integration is not supported for this configuration.", + }, + ], + isError: true, + shouldSkipFormatting: true, + }; + } + const percyToken = await fetchPercyToken( + input.projectName, + authorization, + { type: PercyIntegrationTypeEnum.WEB } + ); + const result = runPercyWeb(percyInput, percyToken); return await formatToolResult(result); - } else { - const percyWithSDKResult = runPercyWithSDK(input, config); - const hasPercySDKError = percyWithSDKResult.steps.some( - (step) => step.isError, + } else if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { + // First try Percy with BrowserStack SDK + const percyWithSDKResult = runPercyWithSDK( + { + ...percyInput, + desiredPlatforms: [], + }, + config ); - if (hasPercySDKError) { - const { - projectName, - detectedLanguage, - detectedBrowserAutomationFramework, - detectedTestingFramework, - } = input; - - // Check if standalone Percy Automate supports this configuration. - const percyWithBrowserstackSDK = checkPercyIntegrationSupport({ + const hasPercySDKError = + percyWithSDKResult.steps && percyWithSDKResult.steps.some((step) => step.isError); + + if (!hasPercySDKError) { + // Percy with SDK is supported, return those steps + return await formatToolResult(percyWithSDKResult); + } else { + // Fallback to standalone Percy Automate if supported + const supportCheck = checkPercyIntegrationSupport({ + ...percyInput, integrationType: PercyIntegrationTypeEnum.AUTOMATE, - detectedLanguage, - detectedTestingFramework, }); - - if (!percyWithBrowserstackSDK.supported) { + if (!supportCheck.supported) { return { content: [ { type: "text", text: - percyWithBrowserstackSDK.errorMessage || - "Percy Automate is not supported for this configuration.", + supportCheck.errorMessage || + "Percy Automate integration is not supported for this configuration.", }, ], isError: true, shouldSkipFormatting: true, }; } - - // Standalone Percy Automate is supported, proceed with its setup flow. - const percyAutomateInput = { - projectName, - detectedLanguage, - detectedBrowserAutomationFramework, - detectedTestingFramework, - integrationType: PercyIntegrationTypeEnum.AUTOMATE, + // SDK setup instructions (for Automate, without Percy) + const sdkInput = { + projectName: input.projectName, + detectedLanguage: input.detectedLanguage, + detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, + detectedTestingFramework: input.detectedTestingFramework, + desiredPlatforms: [], }; - - const authorization = getBrowserStackAuth(config); - const percyToken = await fetchPercyToken(projectName, authorization, { - type: PercyIntegrationTypeEnum.AUTOMATE, - }); - - // 1. Get BrowserStack SDK setup steps (for Automate, without Percy) - const sdkResult = runBstackSDKOnly(input, config, true); - - // 2. Get Percy Automate setup steps - const percyAutomateResult = runPercyAutomateOnly( - percyAutomateInput, - percyToken, + const sdkResult = runBstackSDKOnly(sdkInput, config, true); + // Percy Automate instructions + const percyToken = await fetchPercyToken( + input.projectName, + authorization, + { type: PercyIntegrationTypeEnum.AUTOMATE } ); - - // 3. Combine steps: warning, SDK steps, Percy Automate steps - const combinedSteps = [ + const percyAutomateResult = runPercyAutomateOnly(percyInput, percyToken); + // Combine steps: warning, SDK steps, Percy Automate steps + const steps = [ { type: "warning" as const, - content: `Note: Percy with the BrowserStack SDK is not supported for your project's configuration (Language: ${detectedLanguage}, Testing Framework: ${detectedTestingFramework}). Falling back to the standalone Percy Automate SDK setup. You must set up both BrowserStack Automate and Percy Automate.`, + content: `Note: Percy with the BrowserStack SDK is not supported for your project's configuration. Falling back to the standalone Percy Automate SDK setup. You must set up both BrowserStack Automate and Percy Automate.`, title: "Percy Automate Fallback", isError: false, }, ...(sdkResult.steps || []), ...(percyAutomateResult.steps || []), ]; - - // 4. Pass combined steps to formatToolResult return await formatToolResult({ ...percyAutomateResult, - steps: combinedSteps, + steps, }); } - return await formatToolResult(percyWithSDKResult); - } - } catch (error) { - return { - content: [ - { - type: "text", - text: BOOTSTRAP_FAILED(error, { - config, - percyMode: (rawInput as any)?.enablePercy - ? "percy-on-bstack" - : "bstack-only", - }), - }, - ], - isError: true, - }; - } -} - -export async function setUpPercyHandler( - rawInput: unknown, - config: BrowserStackConfig, -): Promise { - try { - const input = SetUpPercySchema.parse(rawInput); - const authorization = getBrowserStackAuth(config); - - // Create adapter object for Percy handlers - const percyInput = { - projectName: input.projectName, - detectedLanguage: input.detectedLanguage, - detectedBrowserAutomationFramework: - input.detectedBrowserAutomationFramework, - detectedTestingFramework: input.detectedTestingFramework, - integrationType: PercyIntegrationTypeEnum.WEB, - }; - - const supportCheck = checkPercyIntegrationSupport(percyInput); - if (!supportCheck.supported) { + } else { return { content: [ { type: "text", - text: - supportCheck.errorMessage || - "Percy integration not supported for this configuration.", + text: "Unknown or unsupported Percy integration type requested.", }, ], isError: true, shouldSkipFormatting: true, }; } - - const percyToken = await fetchPercyToken(input.projectName, authorization, { - type: PercyIntegrationTypeEnum.WEB, - }); - - const result = runPercyWeb(percyInput, percyToken); - - return await formatToolResult(result); } catch (error) { return { content: [ From c80f25c18c67e4a22740c89761d84b68b5777a72 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 1 Aug 2025 17:43:15 +0530 Subject: [PATCH 18/23] Percy changes --- src/tools/percy-sdk.ts | 2 +- src/tools/sdk-utils/common/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/percy-sdk.ts b/src/tools/percy-sdk.ts index 8b91655..68a4dd5 100644 --- a/src/tools/percy-sdk.ts +++ b/src/tools/percy-sdk.ts @@ -7,7 +7,7 @@ import { setUpPercyHandler } from "./sdk-utils/handler.js"; * Tool description for standalone Percy visual testing */ const SETUP_PERCY_DESCRIPTION = - "Set up Percy visual testing for your project. Do not invoke this tool without clear user intent or codebase inspection. If the user says 'setup Percy' and the codebase contains BrowserStack Automate SDK or any yml related to it, always use 'automate'; otherwise, use 'web'. If the user says 'run Percy automate', always use 'automate'."; + "Set up Percy visual testing for your project. This supports both Percy Web Standalone and Percy Automate."; /** * Registers the standalone Percy setup tool with the MCP server. diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index b6c4f8d..5b42610 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -16,7 +16,7 @@ export const SetUpPercyParamsShape = { integrationType: z .nativeEnum(PercyIntegrationTypeEnum) .describe( - "Set to 'automate' if the user says 'run Percy automate' or if the codebase contains BrowserStack Automate SDK or any related yml file. In all other cases, set to 'web'. This value must be provided explicitly or determined by clear codebase inspection—never inferred automatically." + "User can say 'setup Percy' then if the codebase contains BrowserStack Automate SDK or any related yml file then use 'automate'. In all other cases, set to 'web' which is standalone percy. Set to 'automate' if the user says 'run Percy automate'. This value must be provided explicitly or determined by clear codebase inspection—never inferred automatically." ), }; From 5483db3fae9ae9a3050d6c2a666ce417654151dc Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Fri, 1 Aug 2025 23:06:37 +0530 Subject: [PATCH 19/23] Refactor Percy and BrowserStack integration --- src/tools/sdk-utils/common/schema.ts | 2 +- src/tools/sdk-utils/handler.ts | 44 ++++++++++++++------- src/tools/sdk-utils/percy-bstack/handler.ts | 2 +- src/tools/sdk-utils/percy-bstack/index.ts | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/tools/sdk-utils/common/schema.ts b/src/tools/sdk-utils/common/schema.ts index 5b42610..e80f94a 100644 --- a/src/tools/sdk-utils/common/schema.ts +++ b/src/tools/sdk-utils/common/schema.ts @@ -16,7 +16,7 @@ export const SetUpPercyParamsShape = { integrationType: z .nativeEnum(PercyIntegrationTypeEnum) .describe( - "User can say 'setup Percy' then if the codebase contains BrowserStack Automate SDK or any related yml file then use 'automate'. In all other cases, set to 'web' which is standalone percy. Set to 'automate' if the user says 'run Percy automate'. This value must be provided explicitly or determined by clear codebase inspection—never inferred automatically." + "User can say 'setup Percy' then if the codebase contains BrowserStack Automate SDK or any related yml file then use 'automate'. In all other cases, set to 'web' which is standalone percy. Set to 'automate' if the user says 'run Percy automate'. This value must be provided explicitly or determined by clear codebase inspection—never inferred automatically.", ), }; diff --git a/src/tools/sdk-utils/handler.ts b/src/tools/sdk-utils/handler.ts index 4705826..acf6b0e 100644 --- a/src/tools/sdk-utils/handler.ts +++ b/src/tools/sdk-utils/handler.ts @@ -12,7 +12,7 @@ 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 { runPercyWithSDK } from "./percy-bstack/handler.js"; +import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js"; import { checkPercyIntegrationSupport } from "./common/utils.js"; export async function runTestsOnBrowserStackHandler( @@ -49,7 +49,8 @@ export async function setUpPercyHandler( const percyInput = { projectName: input.projectName, detectedLanguage: input.detectedLanguage, - detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, detectedTestingFramework: input.detectedTestingFramework, integrationType: input.integrationType, }; @@ -73,25 +74,34 @@ export async function setUpPercyHandler( const percyToken = await fetchPercyToken( input.projectName, authorization, - { type: PercyIntegrationTypeEnum.WEB } + { 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 percyWithSDKResult = runPercyWithSDK( + const percyWithBrowserstackSDKResult = runPercyWithBrowserstackSDK( { ...percyInput, desiredPlatforms: [], }, - config + config, ); const hasPercySDKError = - percyWithSDKResult.steps && percyWithSDKResult.steps.some((step) => step.isError); + percyWithBrowserstackSDKResult.steps && + percyWithBrowserstackSDKResult.steps.some((step) => step.isError); if (!hasPercySDKError) { - // Percy with SDK is supported, return those steps - return await formatToolResult(percyWithSDKResult); + // 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({ @@ -116,7 +126,8 @@ export async function setUpPercyHandler( const sdkInput = { projectName: input.projectName, detectedLanguage: input.detectedLanguage, - detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework, + detectedBrowserAutomationFramework: + input.detectedBrowserAutomationFramework, detectedTestingFramework: input.detectedTestingFramework, desiredPlatforms: [], }; @@ -125,16 +136,19 @@ export async function setUpPercyHandler( const percyToken = await fetchPercyToken( input.projectName, authorization, - { type: PercyIntegrationTypeEnum.AUTOMATE } + { type: PercyIntegrationTypeEnum.AUTOMATE }, + ); + const percyAutomateResult = runPercyAutomateOnly( + percyInput, + percyToken, ); - const percyAutomateResult = runPercyAutomateOnly(percyInput, percyToken); // Combine steps: warning, SDK steps, Percy Automate steps const steps = [ { - type: "warning" as const, - content: `Note: Percy with the BrowserStack SDK is not supported for your project's configuration. Falling back to the standalone Percy Automate SDK setup. You must set up both BrowserStack Automate and Percy Automate.`, - title: "Percy Automate Fallback", - isError: false, + 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 || []), diff --git a/src/tools/sdk-utils/percy-bstack/handler.ts b/src/tools/sdk-utils/percy-bstack/handler.ts index 2140d25..e071177 100644 --- a/src/tools/sdk-utils/percy-bstack/handler.ts +++ b/src/tools/sdk-utils/percy-bstack/handler.ts @@ -16,7 +16,7 @@ import { SDKSupportedLanguage, } from "../common/types.js"; -export function runPercyWithSDK( +export function runPercyWithBrowserstackSDK( input: RunTestsOnBrowserStackInput, config: BrowserStackConfig, ): RunTestsInstructionResult { diff --git a/src/tools/sdk-utils/percy-bstack/index.ts b/src/tools/sdk-utils/percy-bstack/index.ts index 15045b9..6338ed2 100644 --- a/src/tools/sdk-utils/percy-bstack/index.ts +++ b/src/tools/sdk-utils/percy-bstack/index.ts @@ -1,5 +1,5 @@ // Percy + BrowserStack SDK utilities -export { runPercyWithSDK } from "./handler.js"; +export { runPercyWithBrowserstackSDK } from "./handler.js"; export { getPercyInstructions, formatPercyInstructions, From da467092e2722c3da02b457c50da805e504d42d5 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Sun, 3 Aug 2025 23:53:14 +0530 Subject: [PATCH 20/23] Enhance Percy integration documentation and support for additional frameworks --- .../sdk-utils/percy-automate/constants.ts | 39 -- .../sdk-utils/percy-automate/frameworks.ts | 11 +- src/tools/sdk-utils/percy-web/constants.ts | 522 ++++++++++++++++++ src/tools/sdk-utils/percy-web/frameworks.ts | 11 + 4 files changed, 536 insertions(+), 47 deletions(-) diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index 5562f82..2a59e7f 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -106,45 +106,6 @@ Run Percy Automate with your tests ${percyAutomateReviewSnapshotsStep} `; -export const testngPercyAutomateInstructions = ` ----STEP--- -Install or upgrade BrowserStack SDK - - Install the BrowserStack SDK using Maven: - mvn archetype:generate -B -DarchetypeGroupId=com.browserstack -DarchetypeArtifactId=browserstack-sdk-archetype-integrate -DgroupId=com.browserstack -DartifactId=browserstack-sdk-archetype-integrate -DBROWSERSTACK_USERNAME=YOUR_USERNAME -DBROWSERSTACK_ACCESS_KEY=YOUR_ACCESS_KEY -DBROWSERSTACK_FRAMEWORK=testng - ----STEP--- -Update your browsersstack.yml config file - 1. Set \`percy: true\` - 2. Set a \`projectName\` - 3. Set \`percyCaptureMode: manual\` - ----STEP--- -Update your TestNG Script -${percyAutomateAggressiveInstruction} - 1. Import the BrowserStack Percy SDK in your test script: - import com.browserstack.PercySDK; - 2. Add the \`PercySDK.screenshot(driver, name)\` method at required points in your test script. - -Example: -\`\`\`java -// ...imports -import com.browserstack.PercySDK; -public class TestNG extends SeleniumTest { - @Test - public void test() throws Exception { - // ... - PercySDK.screenshot(driver, "My Screenshot"); - // ... - } -} -\`\`\` - ----STEP--- -Run your test script - - npx percy exec -- mvn test -P sample-percy-test - -${percyAutomateReviewSnapshotsStep} -`; export const jestPercyAutomateInstructions = ` Install or upgrade the BrowserStack SDK: diff --git a/src/tools/sdk-utils/percy-automate/frameworks.ts b/src/tools/sdk-utils/percy-automate/frameworks.ts index e145441..51addcc 100644 --- a/src/tools/sdk-utils/percy-automate/frameworks.ts +++ b/src/tools/sdk-utils/percy-automate/frameworks.ts @@ -7,18 +7,13 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { instructions: instructions.pythonPytestPercyAutomateInstructions, }, }, - javascript: { + nodejs: { cypress: { instructions: instructions.jsCypressPercyAutomateInstructions }, mocha: { instructions: instructions.mochaPercyAutomateInstructions }, jest: { instructions: instructions.jestPercyAutomateInstructions }, - webdriverio: { - instructions: instructions.webdriverioPercyAutomateInstructions, - }, + webdriverio: { instructions: instructions.webdriverioPercyAutomateInstructions }, testcafe: { instructions: instructions.testcafePercyAutomateInstructions }, - }, - java: { - testng: { instructions: instructions.testngPercyAutomateInstructions }, - }, + } }; /** diff --git a/src/tools/sdk-utils/percy-web/constants.ts b/src/tools/sdk-utils/percy-web/constants.ts index c86ce05..76721e2 100644 --- a/src/tools/sdk-utils/percy-web/constants.ts +++ b/src/tools/sdk-utils/percy-web/constants.ts @@ -121,6 +121,7 @@ Run Percy with your tests ${percyReviewSnapshotsStep} `; + export const rubyInstructions = ` ---STEP--- Install Percy dependencies @@ -156,6 +157,48 @@ Run Percy with your tests ${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 +${percyAutomateAggressiveInstruction} + - 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 + +---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 @@ -284,6 +327,485 @@ Run Percy with your tests ${percyReviewSnapshotsStep} `; +// Percy WebdriverIO instructions for JavaScript +export const jsWebdriverioInstructions = ` +---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 +${percyAutomateAggressiveInstruction} + - 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(); +})(); +\`\`\` + +---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 jsEmberInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Ember SDK: + npm install --save-dev @percy/cli @percy/ember + +---STEP--- +Update your Ember test script +${percyAutomateAggressiveInstruction} + - 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 + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- ember test + +${percyReviewSnapshotsStep} +`; + +// Percy Cypress instructions for JavaScript +export const jsCypressInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Cypress SDK: + npm install --save-dev @percy/cli @percy/cypress + +---STEP--- +Update your Cypress test script +${percyAutomateAggressiveInstruction} + - 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] }); + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- cypress run + +${percyReviewSnapshotsStep} +`; + +// Percy Puppeteer instructions for JavaScript +export const jsPuppeteerInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Puppeteer SDK: + npm install --save-dev @percy/cli @percy/puppeteer + +---STEP--- +Update your Puppeteer test script +${percyAutomateAggressiveInstruction} + - 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] }); + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- mocha + +${percyReviewSnapshotsStep} +`; + +// Percy Nightmare instructions for JavaScript +export const jsNightmareInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Nightmare SDK: + npm install --save-dev @percy/cli @percy/nightmare + +---STEP--- +Update your Nightmare test script +${percyAutomateAggressiveInstruction} + - 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 + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- node script.js + +${percyReviewSnapshotsStep} +`; + +// Percy Nightwatch instructions for JavaScript +export const jsNightwatchInstructions = ` +---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 +${percyAutomateAggressiveInstruction} + - 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 + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- nightwatch + +${percyReviewSnapshotsStep} +`; + +// Percy Protractor instructions for JavaScript +export const jsProtractorInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Protractor SDK: + npm install --save-dev @percy/cli @percy/protractor + +---STEP--- +Update your Protractor test script +${percyAutomateAggressiveInstruction} + - 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 + +---STEP--- +Run Percy with your tests + - Use the following command: + npx percy exec -- protractor conf.js + +${percyReviewSnapshotsStep} +`; + +// Percy TestCafe instructions for JavaScript +export const jsTestcafeInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and TestCafe SDK: + npm install --save-dev @percy/cli @percy/testcafe + +---STEP--- +Update your TestCafe test script +${percyAutomateAggressiveInstruction} + - 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 + +---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 jsGatsbyInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Gatsby plugin: + npm install --save @percy/cli gatsby-plugin-percy + +---STEP--- +Update your Gatsby configuration +${percyAutomateAggressiveInstruction} + - Add the Percy plugin to your gatsby-config.js file: + module.exports = { + plugins: [\`gatsby-plugin-percy\`] + } + +---STEP--- +Run Percy with your Gatsby build + - Use the following command: + npx percy exec -- gatsby build + + - 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() + }] + }] + } + }] +} +\`\`\` + +${percyReviewSnapshotsStep} +`; + +// Percy Storybook instructions for JavaScript +export const jsStorybookInstructions = ` +---STEP--- +Install Percy dependencies + - Install Percy CLI and Storybook SDK: + npm install --save-dev @percy/cli @percy/storybook + +---STEP--- +Update your Storybook stories +${percyAutomateAggressiveInstruction} + - 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. + +---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 diff --git a/src/tools/sdk-utils/percy-web/frameworks.ts b/src/tools/sdk-utils/percy-web/frameworks.ts index a920cb9..f22ba11 100644 --- a/src/tools/sdk-utils/percy-web/frameworks.ts +++ b/src/tools/sdk-utils/percy-web/frameworks.ts @@ -9,6 +9,16 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { javascript: { selenium: { instructions: constants.nodejsInstructions }, playwright: { instructions: constants.jsPlaywrightInstructions }, + webdriverio: { instructions: constants.jsWebdriverioInstructions }, + ember: { instructions: constants.jsEmberInstructions }, + cypress: { instructions: constants.jsCypressInstructions }, + puppeteer: { instructions: constants.jsPuppeteerInstructions }, + nightmare: { instructions: constants.jsNightmareInstructions }, + nightwatch: { instructions: constants.jsNightwatchInstructions }, + protractor: { instructions: constants.jsProtractorInstructions }, + testcafe: { instructions: constants.jsTestcafeInstructions }, + gatsby: { instructions: constants.jsGatsbyInstructions }, + storybook: { instructions: constants.jsStorybookInstructions }, }, java: { selenium: { instructions: constants.javaInstructions }, @@ -16,6 +26,7 @@ export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { }, ruby: { selenium: { instructions: constants.rubyInstructions }, + capybara: { instructions: constants.rubyCapybaraInstructions }, }, csharp: { selenium: { instructions: constants.csharpInstructions }, From 99080dc1ab13b9105aacacd5aee539a393de2a1f Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 4 Aug 2025 10:31:00 +0530 Subject: [PATCH 21/23] Refactor Percy Automate configuration to support multiple drivers and frameworks --- src/tools/sdk-utils/common/utils.ts | 1 + .../sdk-utils/percy-automate/constants.ts | 141 +++++++++++++++++- .../sdk-utils/percy-automate/frameworks.ts | 41 +++-- src/tools/sdk-utils/percy-automate/handler.ts | 7 +- src/tools/sdk-utils/percy-automate/types.ts | 8 +- 5 files changed, 182 insertions(+), 16 deletions(-) diff --git a/src/tools/sdk-utils/common/utils.ts b/src/tools/sdk-utils/common/utils.ts index 23a8b10..b8d356a 100644 --- a/src/tools/sdk-utils/common/utils.ts +++ b/src/tools/sdk-utils/common/utils.ts @@ -22,6 +22,7 @@ export function checkPercyIntegrationSupport(input: { if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) { const isSupported = isPercyAutomateFrameworkSupported( input.detectedLanguage, + input.detectedBrowserAutomationFramework || "", input.detectedTestingFramework || "", ); if (!isSupported) { diff --git a/src/tools/sdk-utils/percy-automate/constants.ts b/src/tools/sdk-utils/percy-automate/constants.ts index 2a59e7f..541707c 100644 --- a/src/tools/sdk-utils/percy-automate/constants.ts +++ b/src/tools/sdk-utils/percy-automate/constants.ts @@ -5,7 +5,7 @@ Review the snapshots - Go to your Percy project on https://percy.io to review snapshots and approve/reject any visual changes. `; -export const pythonPytestPercyAutomateInstructions = ` +export const pythonPytestSeleniumInstructions = ` Install Percy Automate dependencies - Install Percy CLI: npm install --save-dev @percy/cli @@ -47,6 +47,47 @@ Run Percy Automate with your tests ${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 +${percyAutomateAggressiveInstruction} + - 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: @@ -106,6 +147,36 @@ Run Percy Automate with your tests ${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: @@ -230,3 +301,71 @@ Run Percy ${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 index 51addcc..ef6acf9 100644 --- a/src/tools/sdk-utils/percy-automate/frameworks.ts +++ b/src/tools/sdk-utils/percy-automate/frameworks.ts @@ -3,29 +3,50 @@ import * as instructions from "./constants.js"; export const SUPPORTED_CONFIGURATIONS: ConfigMapping = { python: { - pytest: { - instructions: instructions.pythonPytestPercyAutomateInstructions, + selenium: { + pytest: { + instructions: instructions.pythonPytestSeleniumInstructions, + }, }, + playwright: { + pytest: { + instructions: instructions.pythonPytestPlaywrightInstructions, + }, + }, + }, + java: { + playwright:{ + junit: { instructions: instructions.javaPlaywrightJunitInstructions }, + } }, nodejs: { - cypress: { instructions: instructions.jsCypressPercyAutomateInstructions }, - mocha: { instructions: instructions.mochaPercyAutomateInstructions }, - jest: { instructions: instructions.jestPercyAutomateInstructions }, - webdriverio: { instructions: instructions.webdriverioPercyAutomateInstructions }, - testcafe: { instructions: instructions.testcafePercyAutomateInstructions }, - } + 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 and testing framework + * 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; - return !!languageConfig[framework as keyof typeof languageConfig]; + 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 index df0b3ad..b646f05 100644 --- a/src/tools/sdk-utils/percy-automate/handler.ts +++ b/src/tools/sdk-utils/percy-automate/handler.ts @@ -12,10 +12,13 @@ export function runPercyAutomateOnly( // Assume configuration is supported due to guardrails at orchestration layer const languageConfig = SUPPORTED_CONFIGURATIONS[input.detectedLanguage as SDKSupportedLanguage]; - const testingFrameworkConfig = languageConfig[input.detectedTestingFramework]; + const driverConfig = languageConfig[input.detectedBrowserAutomationFramework]; + const testingFrameworkConfig = driverConfig + ? driverConfig[input.detectedTestingFramework] + : undefined; // Generate instructions for the supported configuration with project name - const instructions = testingFrameworkConfig.instructions; + const instructions = testingFrameworkConfig ? testingFrameworkConfig.instructions : ""; // Prepend a step to set the Percy token in the environment steps.push({ diff --git a/src/tools/sdk-utils/percy-automate/types.ts b/src/tools/sdk-utils/percy-automate/types.ts index 9ad18c2..c1860f8 100644 --- a/src/tools/sdk-utils/percy-automate/types.ts +++ b/src/tools/sdk-utils/percy-automate/types.ts @@ -1,11 +1,13 @@ /** * Type for Percy Automate configuration mapping. - * Structure: language -> testingFramework -> { instructions: (projectName: string) => string } + * Structure: language -> driver -> testingFramework -> { instructions: string } */ export type ConfigMapping = { [language: string]: { - [testingFramework: string]: { - instructions: string; + [driver: string]: { + [framework: string]: { + instructions: string; + }; }; }; }; From be274b131124d8d7ba504b10db55b50b7f9faec1 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 4 Aug 2025 11:34:31 +0530 Subject: [PATCH 22/23] Update screenshot method in tests and enhance --- src/tools/sdk-utils/percy-bstack/constants.ts | 4 +--- src/tools/sdk-utils/percy-bstack/frameworks.ts | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tools/sdk-utils/percy-bstack/constants.ts b/src/tools/sdk-utils/percy-bstack/constants.ts index cf06dde..c37171e 100644 --- a/src/tools/sdk-utils/percy-bstack/constants.ts +++ b/src/tools/sdk-utils/percy-bstack/constants.ts @@ -49,7 +49,7 @@ describe("sample Test", () => { test("my test", async () => { // .... - await percy.snapshot(driver, "My Snapshot") + await percy.screenshot(driver, "My Snapshot") // .... }); }) @@ -134,8 +134,6 @@ Here's an example: \`\`\`csharp using BrowserStackSDK.Percy; -using NUnit.Framework; - namespace Tests; public class MyTest diff --git a/src/tools/sdk-utils/percy-bstack/frameworks.ts b/src/tools/sdk-utils/percy-bstack/frameworks.ts index bc985e2..acef4c1 100644 --- a/src/tools/sdk-utils/percy-bstack/frameworks.ts +++ b/src/tools/sdk-utils/percy-bstack/frameworks.ts @@ -8,11 +8,15 @@ export const PERCY_INSTRUCTIONS: ConfigMapping = { 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: { From 164e9932575cc5374bd4c50625b736e72ec15d81 Mon Sep 17 00:00:00 2001 From: tech-sushant Date: Mon, 4 Aug 2025 14:45:48 +0530 Subject: [PATCH 23/23] Add instructions for iterative Percy integration in test files --- src/tools/sdk-utils/common/commonMessages.ts | 2 ++ src/tools/sdk-utils/percy-bstack/frameworks.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tools/sdk-utils/common/commonMessages.ts b/src/tools/sdk-utils/common/commonMessages.ts index c7cea86..e3aa422 100644 --- a/src/tools/sdk-utils/common/commonMessages.ts +++ b/src/tools/sdk-utils/common/commonMessages.ts @@ -54,4 +54,6 @@ You MUST follow these steps with NO EXCEPTIONS: 3. At the top of EVERY relevant file, ensure the necessary import/setup is present (do NOT duplicate if already present). 4. Insert the snapshot call (with a clear, descriptive, and unique name) at EVERY identified point in EVERY relevant file. 5. Double-check your work. If in doubt, add more snapshot calls rather than fewer. DO NOT stop until you have processed ALL relevant files and ALL important points within them. + +Iteratively update every relevant test file in the directory and all subdirectories, adding Percy integration to each, one file at a time, until all are complete. `; diff --git a/src/tools/sdk-utils/percy-bstack/frameworks.ts b/src/tools/sdk-utils/percy-bstack/frameworks.ts index acef4c1..91c08c8 100644 --- a/src/tools/sdk-utils/percy-bstack/frameworks.ts +++ b/src/tools/sdk-utils/percy-bstack/frameworks.ts @@ -26,4 +26,4 @@ export const PERCY_INSTRUCTIONS: ConfigMapping = { webdriverio: { instructions: constants.webdriverioPercyInstructions }, }, }, -}; +}; \ No newline at end of file