Skip to content

Commit bd0fc04

Browse files
Merge pull request #132 from tech-sushant/percy-new-flow
Percy Integration
2 parents ceede72 + c553dec commit bd0fc04

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4534
-664
lines changed

src/lib/inmemory-store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const signedUrlMap = new Map<string, object>();
2+
export const testFilePathsMap = new Map<string, string[]>();

src/server-factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const require = createRequire(import.meta.url);
77
const packageJson = require("../package.json");
88
import logger from "./logger.js";
99
import addSDKTools from "./tools/bstack-sdk.js";
10+
import addPercyTools from "./tools/percy-sdk.js";
1011
import addBrowserLiveTools from "./tools/live.js";
1112
import addAccessibilityTools from "./tools/accessibility.js";
1213
import addTestManagementTools from "./tools/testmanagement.js";
@@ -48,6 +49,7 @@ export class BrowserStackMcpServer {
4849
const toolAdders = [
4950
addAccessibilityTools,
5051
addSDKTools,
52+
addPercyTools,
5153
addAppLiveTools,
5254
addBrowserLiveTools,
5355
addTestManagementTools,

src/tools/add-percy-snapshots.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { testFilePathsMap } from "../lib/inmemory-store.js";
2+
import { updateFileAndStep } from "./percy-snapshot-utils/utils.js";
3+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import { percyWebSetupInstructions } from "../tools/sdk-utils/percy-web/handler.js";
5+
6+
export async function updateTestsWithPercyCommands(args: {
7+
uuid: string;
8+
index: number;
9+
}): Promise<CallToolResult> {
10+
const { uuid, index } = args;
11+
const filePaths = testFilePathsMap.get(uuid);
12+
13+
if (!filePaths) {
14+
throw new Error(`No test files found in memory for UUID: ${uuid}`);
15+
}
16+
17+
if (index < 0 || index >= filePaths.length) {
18+
throw new Error(
19+
`Invalid index: ${index}. There are ${filePaths.length} files for UUID: ${uuid}`,
20+
);
21+
}
22+
const result = await updateFileAndStep(
23+
filePaths[index],
24+
index,
25+
filePaths.length,
26+
percyWebSetupInstructions,
27+
);
28+
29+
return {
30+
content: result,
31+
};
32+
}

src/tools/bstack-sdk.ts

Lines changed: 9 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -1,254 +1,25 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { z } from "zod";
4-
import { trackMCP } from "../lib/instrumentation.js";
5-
import { getSDKPrefixCommand } from "./sdk-utils/commands.js";
6-
7-
import {
8-
SDKSupportedBrowserAutomationFramework,
9-
SDKSupportedLanguage,
10-
SDKSupportedTestingFramework,
11-
SDKSupportedLanguageEnum,
12-
SDKSupportedBrowserAutomationFrameworkEnum,
13-
SDKSupportedTestingFrameworkEnum,
14-
} from "./sdk-utils/types.js";
15-
16-
import {
17-
generateBrowserStackYMLInstructions,
18-
getInstructionsForProjectConfiguration,
19-
formatInstructionsWithNumbers,
20-
} from "./sdk-utils/instructions.js";
21-
22-
import {
23-
formatPercyInstructions,
24-
getPercyInstructions,
25-
} from "./sdk-utils/percy/instructions.js";
26-
import { getBrowserStackAuth } from "../lib/get-auth.js";
272
import { BrowserStackConfig } from "../lib/types.js";
3+
import { RunTestsOnBrowserStackParamsShape } from "./sdk-utils/common/schema.js";
4+
import { runTestsOnBrowserStackHandler } from "./sdk-utils/handler.js";
5+
import { RUN_ON_BROWSERSTACK_DESCRIPTION } from "./sdk-utils/common/constants.js";
286

29-
/**
30-
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
31-
* This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies.
32-
*/
33-
export async function bootstrapProjectWithSDK({
34-
detectedBrowserAutomationFramework,
35-
detectedTestingFramework,
36-
detectedLanguage,
37-
desiredPlatforms,
38-
enablePercy,
39-
config,
40-
}: {
41-
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFramework;
42-
detectedTestingFramework: SDKSupportedTestingFramework;
43-
detectedLanguage: SDKSupportedLanguage;
44-
desiredPlatforms: string[];
45-
enablePercy: boolean;
46-
config: BrowserStackConfig;
47-
}): Promise<CallToolResult> {
48-
// Get credentials from config
49-
const authString = getBrowserStackAuth(config);
50-
const [username, accessKey] = authString.split(":");
51-
52-
// Handle frameworks with unique setup instructions that don't use browserstack.yml
53-
if (
54-
detectedBrowserAutomationFramework === "cypress" ||
55-
detectedTestingFramework === "webdriverio"
56-
) {
57-
let combinedInstructions = getInstructionsForProjectConfiguration(
58-
detectedBrowserAutomationFramework,
59-
detectedTestingFramework,
60-
detectedLanguage,
61-
username,
62-
accessKey,
63-
);
64-
65-
if (enablePercy) {
66-
const percyInstructions = getPercyInstructions(
67-
detectedLanguage,
68-
detectedBrowserAutomationFramework,
69-
detectedTestingFramework,
70-
);
71-
72-
if (percyInstructions) {
73-
combinedInstructions +=
74-
"\n\n" + formatPercyInstructions(percyInstructions);
75-
} else {
76-
throw new Error(
77-
`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.`,
78-
);
79-
}
80-
}
81-
82-
// Apply consistent formatting for all configurations
83-
return formatFinalInstructions(combinedInstructions);
84-
}
85-
86-
// Handle default flow using browserstack.yml
87-
const sdkSetupCommand = getSDKPrefixCommand(
88-
detectedLanguage,
89-
detectedTestingFramework,
90-
username,
91-
accessKey,
92-
);
93-
94-
const ymlInstructions = generateBrowserStackYMLInstructions(
95-
desiredPlatforms,
96-
enablePercy,
97-
);
98-
99-
const instructionsForProjectConfiguration =
100-
getInstructionsForProjectConfiguration(
101-
detectedBrowserAutomationFramework,
102-
detectedTestingFramework,
103-
detectedLanguage,
104-
username,
105-
accessKey,
106-
);
107-
108-
let combinedInstructions = "";
109-
110-
// Step 1: Add SDK setup command
111-
if (sdkSetupCommand) {
112-
combinedInstructions += sdkSetupCommand;
113-
}
114-
115-
// Step 2: Add browserstack.yml setup
116-
if (ymlInstructions) {
117-
combinedInstructions += "\n\n---STEP---\n" + ymlInstructions;
118-
}
119-
120-
// Step 3: Add language/framework-specific setup
121-
if (instructionsForProjectConfiguration) {
122-
combinedInstructions += "\n\n" + instructionsForProjectConfiguration;
123-
}
124-
125-
// Step 4: Add Percy setup if applicable
126-
if (enablePercy) {
127-
const percyInstructions = getPercyInstructions(
128-
detectedLanguage,
129-
detectedBrowserAutomationFramework,
130-
detectedTestingFramework,
131-
);
132-
133-
if (percyInstructions) {
134-
combinedInstructions +=
135-
"\n\n" + formatPercyInstructions(percyInstructions);
136-
} else {
137-
throw new Error(
138-
`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.`,
139-
);
140-
}
141-
}
142-
143-
// Apply consistent formatting for all configurations
144-
return formatFinalInstructions(combinedInstructions);
145-
}
146-
147-
// Helper function to apply consistent formatting to all instruction types
148-
function formatFinalInstructions(combinedInstructions: string): CallToolResult {
149-
const fullInstructions = `⚠️ IMPORTANT: DO NOT SKIP ANY STEP
150-
All the setup steps described in this file MUST be executed regardless of any existing configuration or setup.
151-
This ensures proper BrowserStack SDK setup.
152-
153-
${formatInstructionsWithNumbers(combinedInstructions)}`;
154-
155-
return {
156-
content: [
157-
{
158-
type: "text",
159-
text: fullInstructions,
160-
isError: false,
161-
},
162-
],
163-
};
164-
}
165-
166-
export default function addSDKTools(
7+
export function registerRunBrowserStackTestsTool(
1678
server: McpServer,
1689
config: BrowserStackConfig,
16910
) {
17011
const tools: Record<string, any> = {};
17112

17213
tools.setupBrowserStackAutomateTests = server.tool(
17314
"setupBrowserStackAutomateTests",
174-
"Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use for functional or integration tests on BrowserStack, with optional Percy visual testing for supported frameworks. Example prompts: run this test on browserstack; run this test on browserstack with Percy; set up this project for browserstack with Percy. Integrate BrowserStack SDK into your project",
175-
{
176-
detectedBrowserAutomationFramework: z
177-
.nativeEnum(SDKSupportedBrowserAutomationFrameworkEnum)
178-
.describe(
179-
"The automation framework configured in the project. Example: 'playwright', 'selenium'",
180-
),
181-
182-
detectedTestingFramework: z
183-
.nativeEnum(SDKSupportedTestingFrameworkEnum)
184-
.describe(
185-
"The testing framework used in the project. Be precise with framework selection Example: 'webdriverio', 'jest', 'pytest', 'junit4', 'junit5', 'mocha'",
186-
),
187-
188-
detectedLanguage: z
189-
.nativeEnum(SDKSupportedLanguageEnum)
190-
.describe(
191-
"The programming language used in the project. Example: 'nodejs', 'python', 'java', 'csharp'",
192-
),
193-
194-
desiredPlatforms: z
195-
.array(z.enum(["windows", "macos", "android", "ios"]))
196-
.describe(
197-
"The platforms the user wants to test on. Always ask this to the user, do not try to infer this.",
198-
),
199-
200-
enablePercy: z
201-
.boolean()
202-
.optional()
203-
.default(false)
204-
.describe(
205-
"Set to true if the user wants to enable Percy for visual testing. Defaults to false.",
206-
),
207-
},
208-
15+
RUN_ON_BROWSERSTACK_DESCRIPTION,
16+
RunTestsOnBrowserStackParamsShape,
20917
async (args) => {
210-
try {
211-
trackMCP(
212-
"runTestsOnBrowserStack",
213-
server.server.getClientVersion()!,
214-
undefined,
215-
config,
216-
);
217-
218-
return await bootstrapProjectWithSDK({
219-
detectedBrowserAutomationFramework:
220-
args.detectedBrowserAutomationFramework as SDKSupportedBrowserAutomationFramework,
221-
222-
detectedTestingFramework:
223-
args.detectedTestingFramework as SDKSupportedTestingFramework,
224-
225-
detectedLanguage: args.detectedLanguage as SDKSupportedLanguage,
226-
227-
desiredPlatforms: args.desiredPlatforms,
228-
enablePercy: args.enablePercy,
229-
config,
230-
});
231-
} catch (error) {
232-
trackMCP(
233-
"runTestsOnBrowserStack",
234-
server.server.getClientVersion()!,
235-
error,
236-
config,
237-
);
238-
239-
return {
240-
content: [
241-
{
242-
type: "text",
243-
text: `Failed to bootstrap project with BrowserStack SDK. Error: ${error}. Please open an issue on GitHub if the problem persists`,
244-
isError: true,
245-
},
246-
],
247-
isError: true,
248-
};
249-
}
18+
return runTestsOnBrowserStackHandler(args, config);
25019
},
25120
);
25221

25322
return tools;
25423
}
24+
25+
export default registerRunBrowserStackTestsTool;

src/tools/list-test-files.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { listTestFiles } from "./percy-snapshot-utils/detect-test-files.js";
2+
import { testFilePathsMap } from "../lib/inmemory-store.js";
3+
import crypto from "crypto";
4+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
6+
export async function addListTestFiles(args: any): Promise<CallToolResult> {
7+
const { dirs, language, framework } = args;
8+
let testFiles: string[] = [];
9+
10+
for (const dir of dirs) {
11+
const files = await listTestFiles({
12+
language,
13+
framework,
14+
baseDir: dir,
15+
});
16+
testFiles = testFiles.concat(files);
17+
}
18+
19+
if (testFiles.length === 0) {
20+
throw new Error("No test files found");
21+
}
22+
23+
// Generate a UUID and store the test files in memory
24+
const uuid = crypto.randomUUID();
25+
testFilePathsMap.set(uuid, testFiles);
26+
27+
return {
28+
content: [
29+
{
30+
type: "text",
31+
text: `The Test files are stored in memory with id ${uuid} and the total number of tests files found is ${testFiles.length}. You can use this UUID to retrieve the tests file paths later.`,
32+
},
33+
{
34+
type: "text",
35+
text: `You can now use the tool addPercySnapshotCommands to update the test file with Percy commands for visual testing with the UUID ${uuid}`,
36+
},
37+
],
38+
};
39+
}

0 commit comments

Comments
 (0)