@browserstack/mcp-server 1.2.4 → 1.2.6-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/apiClient.d.ts +8 -5
- package/dist/lib/apiClient.js +77 -15
- package/dist/lib/device-cache.d.ts +3 -1
- package/dist/lib/device-cache.js +4 -0
- package/dist/lib/inmemory-store.d.ts +5 -1
- package/dist/lib/inmemory-store.js +10 -1
- package/dist/lib/instrumentation.js +6 -3
- package/dist/lib/utils.d.ts +75 -2
- package/dist/lib/utils.js +20 -0
- package/dist/lib/version-resolver.js +30 -14
- package/dist/tools/add-percy-snapshots.d.ts +0 -1
- package/dist/tools/add-percy-snapshots.js +11 -6
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +7 -1
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +46 -26
- package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -1
- package/dist/tools/appautomate-utils/appium-sdk/constants.js +24 -3
- package/dist/tools/appautomate-utils/appium-sdk/handler.js +16 -2
- package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +2 -0
- package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +63 -29
- package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -1
- package/dist/tools/appautomate-utils/appium-sdk/types.js +10 -1
- package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
- package/dist/tools/appautomate-utils/native-execution/constants.js +24 -2
- package/dist/tools/appautomate.js +15 -2
- package/dist/tools/automate-utils/fetch-screenshots.js +4 -1
- package/dist/tools/list-test-files.d.ts +1 -1
- package/dist/tools/list-test-files.js +43 -19
- package/dist/tools/live-utils/start-session.js +1 -1
- package/dist/tools/percy-sdk.js +33 -6
- package/dist/tools/percy-snapshot-utils/constants.d.ts +0 -6
- package/dist/tools/percy-snapshot-utils/constants.js +0 -15
- package/dist/tools/rca-agent-utils/constants.d.ts +1 -1
- package/dist/tools/rca-agent-utils/constants.js +2 -2
- package/dist/tools/rca-agent-utils/rca-data.d.ts +1 -1
- package/dist/tools/rca-agent-utils/rca-data.js +2 -2
- package/dist/tools/rca-agent-utils/types.d.ts +3 -3
- package/dist/tools/rca-agent.d.ts +1 -1
- package/dist/tools/run-percy-scan.js +51 -10
- package/dist/tools/sdk-utils/bstack/configUtils.d.ts +8 -4
- package/dist/tools/sdk-utils/bstack/configUtils.js +74 -20
- package/dist/tools/sdk-utils/bstack/constants.d.ts +1 -1
- package/dist/tools/sdk-utils/bstack/constants.js +7 -9
- package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +1 -1
- package/dist/tools/sdk-utils/bstack/sdkHandler.js +19 -9
- package/dist/tools/sdk-utils/common/constants.d.ts +6 -5
- package/dist/tools/sdk-utils/common/constants.js +8 -7
- package/dist/tools/sdk-utils/common/device-validator.d.ts +25 -0
- package/dist/tools/sdk-utils/common/device-validator.js +375 -0
- package/dist/tools/sdk-utils/common/schema.d.ts +32 -8
- package/dist/tools/sdk-utils/common/schema.js +62 -3
- package/dist/tools/sdk-utils/common/utils.d.ts +1 -1
- package/dist/tools/sdk-utils/common/utils.js +14 -2
- package/dist/tools/sdk-utils/handler.d.ts +1 -0
- package/dist/tools/sdk-utils/handler.js +59 -14
- package/dist/tools/sdk-utils/percy-automate/constants.d.ts +4 -4
- package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -4
- package/dist/tools/sdk-utils/percy-bstack/handler.js +5 -1
- package/dist/tools/sdk-utils/percy-web/constants.d.ts +22 -20
- package/dist/tools/sdk-utils/percy-web/constants.js +39 -0
- package/dist/tools/sdk-utils/percy-web/handler.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../../lib/device-cache.js";
|
|
2
|
+
import { resolveVersion } from "../../../lib/version-resolver.js";
|
|
3
|
+
import { customFuzzySearch } from "../../../lib/fuzzy.js";
|
|
4
|
+
import { SDKSupportedBrowserAutomationFrameworkEnum } from "./types.js";
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
windows: { browser: "chrome" },
|
|
7
|
+
macos: { browser: "safari" },
|
|
8
|
+
android: { device: "Samsung Galaxy S24", browser: "chrome" },
|
|
9
|
+
ios: { device: "iPhone 15", browser: "safari" },
|
|
10
|
+
};
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// AUTOMATE SECTION (Desktop + Mobile for BrowserStack SDK)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Helper functions to build device entries and eliminate duplication
|
|
15
|
+
function buildDesktopEntries(automateData) {
|
|
16
|
+
if (!automateData.desktop) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return automateData.desktop.flatMap((platform) => platform.browsers.map((browser) => ({
|
|
20
|
+
os: platform.os,
|
|
21
|
+
os_version: platform.os_version,
|
|
22
|
+
browser: browser.browser,
|
|
23
|
+
browser_version: browser.browser_version,
|
|
24
|
+
})));
|
|
25
|
+
}
|
|
26
|
+
function buildMobileEntries(appAutomateData, platform) {
|
|
27
|
+
if (!appAutomateData.mobile) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return appAutomateData.mobile
|
|
31
|
+
.filter((group) => group.os === platform)
|
|
32
|
+
.flatMap((group) => group.devices.map((device) => ({
|
|
33
|
+
os: group.os,
|
|
34
|
+
os_version: device.os_version,
|
|
35
|
+
display_name: device.display_name,
|
|
36
|
+
browsers: device.browsers || [
|
|
37
|
+
{
|
|
38
|
+
browser: device.browser || (platform === "android" ? "chrome" : "safari"),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
43
|
+
// Performance optimization: Create indexed maps for faster lookups
|
|
44
|
+
function createDesktopIndex(entries) {
|
|
45
|
+
const byOS = new Map();
|
|
46
|
+
const byOSVersion = new Map();
|
|
47
|
+
const byBrowser = new Map();
|
|
48
|
+
const nested = new Map();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
// Index by OS
|
|
51
|
+
if (!byOS.has(entry.os)) {
|
|
52
|
+
byOS.set(entry.os, []);
|
|
53
|
+
}
|
|
54
|
+
byOS.get(entry.os).push(entry);
|
|
55
|
+
// Index by OS version
|
|
56
|
+
if (!byOSVersion.has(entry.os_version)) {
|
|
57
|
+
byOSVersion.set(entry.os_version, []);
|
|
58
|
+
}
|
|
59
|
+
byOSVersion.get(entry.os_version).push(entry);
|
|
60
|
+
// Index by browser
|
|
61
|
+
if (!byBrowser.has(entry.browser)) {
|
|
62
|
+
byBrowser.set(entry.browser, []);
|
|
63
|
+
}
|
|
64
|
+
byBrowser.get(entry.browser).push(entry);
|
|
65
|
+
// Build nested index: Map<os, Map<os_version, Map<browser, DesktopBrowserEntry[]>>>
|
|
66
|
+
if (!nested.has(entry.os)) {
|
|
67
|
+
nested.set(entry.os, new Map());
|
|
68
|
+
}
|
|
69
|
+
const osMap = nested.get(entry.os);
|
|
70
|
+
if (!osMap.has(entry.os_version)) {
|
|
71
|
+
osMap.set(entry.os_version, new Map());
|
|
72
|
+
}
|
|
73
|
+
const osVersionMap = osMap.get(entry.os_version);
|
|
74
|
+
if (!osVersionMap.has(entry.browser)) {
|
|
75
|
+
osVersionMap.set(entry.browser, []);
|
|
76
|
+
}
|
|
77
|
+
osVersionMap.get(entry.browser).push(entry);
|
|
78
|
+
}
|
|
79
|
+
return { byOS, byOSVersion, byBrowser, nested };
|
|
80
|
+
}
|
|
81
|
+
function createMobileIndex(entries) {
|
|
82
|
+
const byPlatform = new Map();
|
|
83
|
+
const byDeviceName = new Map();
|
|
84
|
+
const byOSVersion = new Map();
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
// Index by platform
|
|
87
|
+
if (!byPlatform.has(entry.os)) {
|
|
88
|
+
byPlatform.set(entry.os, []);
|
|
89
|
+
}
|
|
90
|
+
byPlatform.get(entry.os).push(entry);
|
|
91
|
+
// Index by device name (case-insensitive)
|
|
92
|
+
const deviceKey = entry.display_name.toLowerCase();
|
|
93
|
+
if (!byDeviceName.has(deviceKey)) {
|
|
94
|
+
byDeviceName.set(deviceKey, []);
|
|
95
|
+
}
|
|
96
|
+
byDeviceName.get(deviceKey).push(entry);
|
|
97
|
+
// Index by OS version
|
|
98
|
+
if (!byOSVersion.has(entry.os_version)) {
|
|
99
|
+
byOSVersion.set(entry.os_version, []);
|
|
100
|
+
}
|
|
101
|
+
byOSVersion.get(entry.os_version).push(entry);
|
|
102
|
+
}
|
|
103
|
+
return { byPlatform, byDeviceName, byOSVersion };
|
|
104
|
+
}
|
|
105
|
+
export async function validateDevices(devices, framework) {
|
|
106
|
+
const validatedEnvironments = [];
|
|
107
|
+
if (!devices || devices.length === 0) {
|
|
108
|
+
// Use centralized default fallback
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
platform: "windows",
|
|
112
|
+
osVersion: "11",
|
|
113
|
+
browser: DEFAULTS.windows.browser,
|
|
114
|
+
browserVersion: "latest",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
// Determine what data we need to fetch
|
|
119
|
+
const needsDesktop = devices.some((env) => ["windows", "macos"].includes((env[0] || "").toLowerCase()));
|
|
120
|
+
const needsMobile = devices.some((env) => ["android", "ios"].includes((env[0] || "").toLowerCase()));
|
|
121
|
+
// Fetch data using framework-specific endpoint for both desktop and mobile
|
|
122
|
+
let deviceData = null;
|
|
123
|
+
try {
|
|
124
|
+
if (needsDesktop || needsMobile) {
|
|
125
|
+
if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) {
|
|
126
|
+
deviceData = (await getDevicesAndBrowsers(BrowserStackProducts.PLAYWRIGHT_AUTOMATE));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
deviceData = (await getDevicesAndBrowsers(BrowserStackProducts.SELENIUM_AUTOMATE));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new Error(`Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
// Preprocess data into indexed maps for better performance
|
|
137
|
+
let desktopIndex = null;
|
|
138
|
+
let androidIndex = null;
|
|
139
|
+
let iosIndex = null;
|
|
140
|
+
if (needsDesktop && deviceData) {
|
|
141
|
+
const desktopEntries = buildDesktopEntries(deviceData);
|
|
142
|
+
desktopIndex = createDesktopIndex(desktopEntries);
|
|
143
|
+
}
|
|
144
|
+
if (needsMobile && deviceData) {
|
|
145
|
+
const androidEntries = buildMobileEntries(deviceData, "android");
|
|
146
|
+
const iosEntries = buildMobileEntries(deviceData, "ios");
|
|
147
|
+
androidIndex = createMobileIndex(androidEntries);
|
|
148
|
+
iosIndex = createMobileIndex(iosEntries);
|
|
149
|
+
}
|
|
150
|
+
for (const env of devices) {
|
|
151
|
+
const discriminator = (env[0] || "").toLowerCase();
|
|
152
|
+
let validatedEnv;
|
|
153
|
+
if (discriminator === "windows") {
|
|
154
|
+
validatedEnv = validateDesktopEnvironment(env, desktopIndex, "windows", DEFAULTS.windows.browser);
|
|
155
|
+
}
|
|
156
|
+
else if (discriminator === "macos") {
|
|
157
|
+
validatedEnv = validateDesktopEnvironment(env, desktopIndex, "macos", DEFAULTS.macos.browser);
|
|
158
|
+
}
|
|
159
|
+
else if (discriminator === "android") {
|
|
160
|
+
validatedEnv = validateMobileEnvironment(env, androidIndex, "android", DEFAULTS.android.device, DEFAULTS.android.browser);
|
|
161
|
+
}
|
|
162
|
+
else if (discriminator === "ios") {
|
|
163
|
+
validatedEnv = validateMobileEnvironment(env, iosIndex, "ios", DEFAULTS.ios.device, DEFAULTS.ios.browser);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
throw new Error(`Unsupported platform: ${discriminator}`);
|
|
167
|
+
}
|
|
168
|
+
validatedEnvironments.push(validatedEnv);
|
|
169
|
+
}
|
|
170
|
+
if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) {
|
|
171
|
+
validatedEnvironments.forEach((env) => {
|
|
172
|
+
if (env.browser) {
|
|
173
|
+
env.browser = env.browser.toLowerCase();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return validatedEnvironments;
|
|
178
|
+
}
|
|
179
|
+
// Optimized desktop validation using nested indexed maps for O(1) lookups
|
|
180
|
+
function validateDesktopEnvironment(env, index, platform, defaultBrowser) {
|
|
181
|
+
const [, osVersion, browser, browserVersion] = env;
|
|
182
|
+
const osKey = platform === "windows" ? "Windows" : "OS X";
|
|
183
|
+
// Use nested index for O(1) lookup instead of filtering
|
|
184
|
+
const osMap = index.nested.get(osKey);
|
|
185
|
+
if (!osMap) {
|
|
186
|
+
throw new Error(`No ${platform} devices available`);
|
|
187
|
+
}
|
|
188
|
+
// Get available OS versions for this platform
|
|
189
|
+
const availableOSVersions = Array.from(osMap.keys());
|
|
190
|
+
const validatedOSVersion = resolveVersion(osVersion || "latest", availableOSVersions);
|
|
191
|
+
// Use nested index for O(1) lookup
|
|
192
|
+
const osVersionMap = osMap.get(validatedOSVersion);
|
|
193
|
+
if (!osVersionMap) {
|
|
194
|
+
throw new Error(`OS version "${validatedOSVersion}" not available for ${platform}`);
|
|
195
|
+
}
|
|
196
|
+
// Get available browsers for this OS version
|
|
197
|
+
const availableBrowsers = Array.from(osVersionMap.keys());
|
|
198
|
+
const validatedBrowser = validateBrowserExact(browser || defaultBrowser, availableBrowsers);
|
|
199
|
+
// Use nested index for O(1) lookup
|
|
200
|
+
const browserEntries = osVersionMap.get(validatedBrowser);
|
|
201
|
+
if (!browserEntries || browserEntries.length === 0) {
|
|
202
|
+
throw new Error(`Browser "${validatedBrowser}" not available for ${platform} ${validatedOSVersion}`);
|
|
203
|
+
}
|
|
204
|
+
const availableBrowserVersions = [
|
|
205
|
+
...new Set(browserEntries.map((e) => e.browser_version)),
|
|
206
|
+
];
|
|
207
|
+
const validatedBrowserVersion = resolveVersion(browserVersion || "latest", availableBrowserVersions);
|
|
208
|
+
return {
|
|
209
|
+
platform,
|
|
210
|
+
osVersion: validatedOSVersion,
|
|
211
|
+
browser: validatedBrowser,
|
|
212
|
+
browserVersion: validatedBrowserVersion,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// Optimized mobile validation using indexed maps
|
|
216
|
+
function validateMobileEnvironment(env, index, platform, defaultDevice, defaultBrowser) {
|
|
217
|
+
const [, deviceName, osVersion, browser] = env;
|
|
218
|
+
const platformEntries = index.byPlatform.get(platform) || [];
|
|
219
|
+
if (platformEntries.length === 0) {
|
|
220
|
+
throw new Error(`No ${platform} devices available`);
|
|
221
|
+
}
|
|
222
|
+
// Use fuzzy search only for device names (as suggested in feedback)
|
|
223
|
+
const deviceMatches = customFuzzySearch(platformEntries, ["display_name"], deviceName || defaultDevice, 5);
|
|
224
|
+
if (deviceMatches.length === 0) {
|
|
225
|
+
throw new Error(`No ${platform} devices matching "${deviceName}". Available devices: ${platformEntries
|
|
226
|
+
.map((d) => d.display_name || "unknown")
|
|
227
|
+
.slice(0, 5)
|
|
228
|
+
.join(", ")}`);
|
|
229
|
+
}
|
|
230
|
+
// Try to find exact match first
|
|
231
|
+
const exactMatch = deviceMatches.find((m) => m.display_name.toLowerCase() === (deviceName || "").toLowerCase());
|
|
232
|
+
// If no exact match, throw error instead of using fuzzy match
|
|
233
|
+
if (!exactMatch) {
|
|
234
|
+
const suggestions = deviceMatches.map((m) => m.display_name).join(", ");
|
|
235
|
+
throw new Error(`Device "${deviceName}" not found exactly for ${platform}. Available similar devices: ${suggestions}. Please use the exact device name.`);
|
|
236
|
+
}
|
|
237
|
+
// Use index for faster filtering
|
|
238
|
+
const deviceKey = exactMatch.display_name.toLowerCase();
|
|
239
|
+
const deviceFiltered = index.byDeviceName.get(deviceKey) || [];
|
|
240
|
+
const availableOSVersions = [
|
|
241
|
+
...new Set(deviceFiltered.map((d) => d.os_version)),
|
|
242
|
+
];
|
|
243
|
+
const validatedOSVersion = resolveVersion(osVersion || "latest", availableOSVersions);
|
|
244
|
+
// Use index for faster filtering
|
|
245
|
+
const osVersionEntries = index.byOSVersion.get(validatedOSVersion) || [];
|
|
246
|
+
const osFiltered = osVersionEntries.filter((d) => d.display_name.toLowerCase() === deviceKey);
|
|
247
|
+
// Validate browser if provided - use exact matching for browsers
|
|
248
|
+
let validatedBrowser = browser || defaultBrowser;
|
|
249
|
+
if (browser && osFiltered.length > 0) {
|
|
250
|
+
// Extract browsers more carefully - handle different possible structures
|
|
251
|
+
const availableBrowsers = [
|
|
252
|
+
...new Set(osFiltered.flatMap((d) => {
|
|
253
|
+
if (d.browsers && Array.isArray(d.browsers)) {
|
|
254
|
+
// If browsers is an array of objects with browser property
|
|
255
|
+
return d.browsers
|
|
256
|
+
.map((b) => {
|
|
257
|
+
// Use display_name for user-friendly browser names, fallback to browser field
|
|
258
|
+
return b.display_name || b.browser;
|
|
259
|
+
})
|
|
260
|
+
.filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
// For mobile devices, provide default browsers if none found
|
|
263
|
+
return platform === "android" ? ["chrome"] : ["safari"];
|
|
264
|
+
})),
|
|
265
|
+
].filter(Boolean);
|
|
266
|
+
if (availableBrowsers.length > 0) {
|
|
267
|
+
try {
|
|
268
|
+
validatedBrowser = validateBrowserExact(browser, availableBrowsers);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// Add more context to browser validation errors
|
|
272
|
+
throw new Error(`Failed to validate browser "${browser}" for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// For mobile, if no specific browsers found, just use the requested browser
|
|
277
|
+
// as most mobile devices support standard browsers
|
|
278
|
+
validatedBrowser = browser || defaultBrowser;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
platform,
|
|
283
|
+
osVersion: validatedOSVersion,
|
|
284
|
+
deviceName: exactMatch.display_name,
|
|
285
|
+
browser: validatedBrowser,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// APP AUTOMATE SECTION (Mobile devices for App Automate)
|
|
290
|
+
// ============================================================================
|
|
291
|
+
export async function validateAppAutomateDevices(devices) {
|
|
292
|
+
const validatedDevices = [];
|
|
293
|
+
if (!devices || devices.length === 0) {
|
|
294
|
+
// Use centralized default fallback
|
|
295
|
+
return [
|
|
296
|
+
{
|
|
297
|
+
platform: "android",
|
|
298
|
+
osVersion: "latest",
|
|
299
|
+
deviceName: DEFAULTS.android.device,
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
let appAutomateData;
|
|
304
|
+
try {
|
|
305
|
+
// Fetch app automate device data
|
|
306
|
+
appAutomateData = (await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE));
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
// Only wrap fetch-related errors
|
|
310
|
+
throw new Error(`Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`);
|
|
311
|
+
}
|
|
312
|
+
for (const device of devices) {
|
|
313
|
+
// Parse device array in format ["android", "Device Name", "OS Version"]
|
|
314
|
+
const [platform, deviceName, osVersion] = device;
|
|
315
|
+
// Find matching device in the data
|
|
316
|
+
let validatedDevice = null;
|
|
317
|
+
if (!appAutomateData.mobile) {
|
|
318
|
+
throw new Error("No mobile device data available");
|
|
319
|
+
}
|
|
320
|
+
// Filter by platform first
|
|
321
|
+
const platformGroup = appAutomateData.mobile.find((group) => group.os === platform.toLowerCase());
|
|
322
|
+
if (!platformGroup) {
|
|
323
|
+
throw new Error(`Platform "${platform}" not supported for App Automate`);
|
|
324
|
+
}
|
|
325
|
+
const platformDevices = platformGroup.devices;
|
|
326
|
+
// Find exact device name match (case-insensitive)
|
|
327
|
+
const exactMatch = platformDevices.find((d) => d.display_name.toLowerCase() === deviceName.toLowerCase());
|
|
328
|
+
if (exactMatch) {
|
|
329
|
+
// Check if the OS version is available for this device
|
|
330
|
+
const deviceVersions = platformDevices
|
|
331
|
+
.filter((d) => d.display_name === exactMatch.display_name)
|
|
332
|
+
.map((d) => d.os_version);
|
|
333
|
+
const validatedOSVersion = resolveVersion(osVersion || "latest", deviceVersions);
|
|
334
|
+
validatedDevice = {
|
|
335
|
+
platform: platformGroup.os,
|
|
336
|
+
osVersion: validatedOSVersion,
|
|
337
|
+
deviceName: exactMatch.display_name,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (!validatedDevice) {
|
|
341
|
+
// If no exact match found, suggest similar devices from the SAME platform only
|
|
342
|
+
const platformDevicesForSearch = platformDevices.map((d) => ({
|
|
343
|
+
...d,
|
|
344
|
+
platform: platformGroup.os,
|
|
345
|
+
}));
|
|
346
|
+
// Try fuzzy search with a more lenient threshold
|
|
347
|
+
const deviceMatches = customFuzzySearch(platformDevicesForSearch, ["display_name"], deviceName, 5, 0.8);
|
|
348
|
+
const suggestions = deviceMatches
|
|
349
|
+
.map((m) => `${m.display_name}`)
|
|
350
|
+
.join(", ");
|
|
351
|
+
// If no fuzzy matches, show some available devices as fallback
|
|
352
|
+
const fallbackDevices = platformDevicesForSearch
|
|
353
|
+
.slice(0, 5)
|
|
354
|
+
.map((d) => d.display_name)
|
|
355
|
+
.join(", ");
|
|
356
|
+
const errorMessage = suggestions
|
|
357
|
+
? `Device "${deviceName}" not found for platform "${platform}".\nAvailable similar devices: ${suggestions}`
|
|
358
|
+
: `Device "${deviceName}" not found for platform "${platform}".\nAvailable devices: ${fallbackDevices}`;
|
|
359
|
+
throw new Error(errorMessage);
|
|
360
|
+
}
|
|
361
|
+
validatedDevices.push(validatedDevice);
|
|
362
|
+
}
|
|
363
|
+
return validatedDevices;
|
|
364
|
+
}
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// SHARED UTILITY FUNCTIONS
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Exact browser validation (preferred for structured fields)
|
|
369
|
+
function validateBrowserExact(requestedBrowser, availableBrowsers) {
|
|
370
|
+
const exactMatch = availableBrowsers.find((b) => b.toLowerCase() === requestedBrowser.toLowerCase());
|
|
371
|
+
if (exactMatch) {
|
|
372
|
+
return exactMatch;
|
|
373
|
+
}
|
|
374
|
+
throw new Error(`Browser "${requestedBrowser}" not found. Available options: ${availableBrowsers.join(", ")}`);
|
|
375
|
+
}
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PercyIntegrationTypeEnum } from "./types.js";
|
|
3
3
|
import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum } from "./types.js";
|
|
4
|
+
export declare const PlatformEnum: {
|
|
5
|
+
readonly WINDOWS: "windows";
|
|
6
|
+
readonly MACOS: "macos";
|
|
7
|
+
readonly ANDROID: "android";
|
|
8
|
+
readonly IOS: "ios";
|
|
9
|
+
};
|
|
10
|
+
export declare const WindowsPlatformEnum: {
|
|
11
|
+
readonly WINDOWS: "windows";
|
|
12
|
+
};
|
|
13
|
+
export declare const MacOSPlatformEnum: {
|
|
14
|
+
readonly MACOS: "macos";
|
|
15
|
+
};
|
|
4
16
|
export declare const SetUpPercyParamsShape: {
|
|
5
17
|
projectName: z.ZodString;
|
|
6
18
|
detectedLanguage: z.ZodNativeEnum<typeof SDKSupportedLanguageEnum>;
|
|
7
19
|
detectedBrowserAutomationFramework: z.ZodNativeEnum<typeof SDKSupportedBrowserAutomationFrameworkEnum>;
|
|
8
20
|
detectedTestingFramework: z.ZodNativeEnum<typeof SDKSupportedTestingFrameworkEnum>;
|
|
9
21
|
integrationType: z.ZodNativeEnum<typeof PercyIntegrationTypeEnum>;
|
|
10
|
-
folderPaths: z.ZodArray<z.ZodString, "many"
|
|
22
|
+
folderPaths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
+
filePaths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
11
24
|
};
|
|
12
25
|
export declare const RunTestsOnBrowserStackParamsShape: {
|
|
13
26
|
projectName: z.ZodString;
|
|
14
27
|
detectedLanguage: z.ZodNativeEnum<typeof SDKSupportedLanguageEnum>;
|
|
15
28
|
detectedBrowserAutomationFramework: z.ZodNativeEnum<typeof SDKSupportedBrowserAutomationFrameworkEnum>;
|
|
16
29
|
detectedTestingFramework: z.ZodNativeEnum<typeof SDKSupportedTestingFrameworkEnum>;
|
|
17
|
-
|
|
30
|
+
devices: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodTuple<[z.ZodNativeEnum<{
|
|
31
|
+
readonly WINDOWS: "windows";
|
|
32
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"android">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"ios">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodNativeEnum<{
|
|
33
|
+
readonly MACOS: "macos";
|
|
34
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>]>, "many">>;
|
|
18
35
|
};
|
|
19
36
|
export declare const SetUpPercySchema: z.ZodObject<{
|
|
20
37
|
projectName: z.ZodString;
|
|
@@ -22,40 +39,47 @@ export declare const SetUpPercySchema: z.ZodObject<{
|
|
|
22
39
|
detectedBrowserAutomationFramework: z.ZodNativeEnum<typeof SDKSupportedBrowserAutomationFrameworkEnum>;
|
|
23
40
|
detectedTestingFramework: z.ZodNativeEnum<typeof SDKSupportedTestingFrameworkEnum>;
|
|
24
41
|
integrationType: z.ZodNativeEnum<typeof PercyIntegrationTypeEnum>;
|
|
25
|
-
folderPaths: z.ZodArray<z.ZodString, "many"
|
|
42
|
+
folderPaths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
43
|
+
filePaths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
26
44
|
}, "strip", z.ZodTypeAny, {
|
|
27
45
|
projectName: string;
|
|
28
46
|
detectedLanguage: SDKSupportedLanguageEnum;
|
|
29
47
|
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFrameworkEnum;
|
|
30
48
|
detectedTestingFramework: SDKSupportedTestingFrameworkEnum;
|
|
31
49
|
integrationType: PercyIntegrationTypeEnum;
|
|
32
|
-
folderPaths
|
|
50
|
+
folderPaths?: string[] | undefined;
|
|
51
|
+
filePaths?: string[] | undefined;
|
|
33
52
|
}, {
|
|
34
53
|
projectName: string;
|
|
35
54
|
detectedLanguage: SDKSupportedLanguageEnum;
|
|
36
55
|
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFrameworkEnum;
|
|
37
56
|
detectedTestingFramework: SDKSupportedTestingFrameworkEnum;
|
|
38
57
|
integrationType: PercyIntegrationTypeEnum;
|
|
39
|
-
folderPaths
|
|
58
|
+
folderPaths?: string[] | undefined;
|
|
59
|
+
filePaths?: string[] | undefined;
|
|
40
60
|
}>;
|
|
41
61
|
export declare const RunTestsOnBrowserStackSchema: z.ZodObject<{
|
|
42
62
|
projectName: z.ZodString;
|
|
43
63
|
detectedLanguage: z.ZodNativeEnum<typeof SDKSupportedLanguageEnum>;
|
|
44
64
|
detectedBrowserAutomationFramework: z.ZodNativeEnum<typeof SDKSupportedBrowserAutomationFrameworkEnum>;
|
|
45
65
|
detectedTestingFramework: z.ZodNativeEnum<typeof SDKSupportedTestingFrameworkEnum>;
|
|
46
|
-
|
|
66
|
+
devices: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodTuple<[z.ZodNativeEnum<{
|
|
67
|
+
readonly WINDOWS: "windows";
|
|
68
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"android">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"ios">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodNativeEnum<{
|
|
69
|
+
readonly MACOS: "macos";
|
|
70
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>]>, "many">>;
|
|
47
71
|
}, "strip", z.ZodTypeAny, {
|
|
48
72
|
projectName: string;
|
|
49
73
|
detectedLanguage: SDKSupportedLanguageEnum;
|
|
50
74
|
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFrameworkEnum;
|
|
51
75
|
detectedTestingFramework: SDKSupportedTestingFrameworkEnum;
|
|
52
|
-
|
|
76
|
+
devices: (["windows", string, string, string] | ["android", string, string, string] | ["ios", string, string, string] | ["macos", string, string, string])[];
|
|
53
77
|
}, {
|
|
54
78
|
projectName: string;
|
|
55
79
|
detectedLanguage: SDKSupportedLanguageEnum;
|
|
56
80
|
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFrameworkEnum;
|
|
57
81
|
detectedTestingFramework: SDKSupportedTestingFrameworkEnum;
|
|
58
|
-
|
|
82
|
+
devices?: (["windows", string, string, string] | ["android", string, string, string] | ["ios", string, string, string] | ["macos", string, string, string])[] | undefined;
|
|
59
83
|
}>;
|
|
60
84
|
export type SetUpPercyInput = z.infer<typeof SetUpPercySchema>;
|
|
61
85
|
export type RunTestsOnBrowserStackInput = z.infer<typeof RunTestsOnBrowserStackSchema>;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PercyIntegrationTypeEnum } from "./types.js";
|
|
3
3
|
import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum, } from "./types.js";
|
|
4
|
+
// Platform enums for better validation
|
|
5
|
+
export const PlatformEnum = {
|
|
6
|
+
WINDOWS: "windows",
|
|
7
|
+
MACOS: "macos",
|
|
8
|
+
ANDROID: "android",
|
|
9
|
+
IOS: "ios",
|
|
10
|
+
};
|
|
11
|
+
export const WindowsPlatformEnum = {
|
|
12
|
+
WINDOWS: "windows",
|
|
13
|
+
};
|
|
14
|
+
export const MacOSPlatformEnum = {
|
|
15
|
+
MACOS: "macos",
|
|
16
|
+
};
|
|
4
17
|
export const SetUpPercyParamsShape = {
|
|
5
18
|
projectName: z.string().describe("A unique name for your Percy project."),
|
|
6
19
|
detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum),
|
|
@@ -11,7 +24,12 @@ export const SetUpPercyParamsShape = {
|
|
|
11
24
|
.describe("Specify the Percy integration type: web (Percy Web) or automate (Percy Automate). If not provided, always prompt the user with: 'Please specify the Percy integration type.' Do not proceed without an explicit selection. Never use a default."),
|
|
12
25
|
folderPaths: z
|
|
13
26
|
.array(z.string())
|
|
27
|
+
.optional()
|
|
14
28
|
.describe("An array of absolute folder paths containing UI test files. If not provided, analyze codebase for UI test folders by scanning for test patterns which contain UI test cases as per framework. Return empty array if none found."),
|
|
29
|
+
filePaths: z
|
|
30
|
+
.array(z.string())
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("An array of absolute file paths to specific UI test files. Use this when you want to target specific test files rather than entire folders. If not provided, will use folderPaths instead."),
|
|
15
33
|
};
|
|
16
34
|
export const RunTestsOnBrowserStackParamsShape = {
|
|
17
35
|
projectName: z
|
|
@@ -20,9 +38,50 @@ export const RunTestsOnBrowserStackParamsShape = {
|
|
|
20
38
|
detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum),
|
|
21
39
|
detectedBrowserAutomationFramework: z.nativeEnum(SDKSupportedBrowserAutomationFrameworkEnum),
|
|
22
40
|
detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum),
|
|
23
|
-
|
|
24
|
-
.array(z.
|
|
25
|
-
|
|
41
|
+
devices: z
|
|
42
|
+
.array(z.union([
|
|
43
|
+
// Windows: [windows, osVersion, browser, browserVersion]
|
|
44
|
+
z.tuple([
|
|
45
|
+
z
|
|
46
|
+
.nativeEnum(WindowsPlatformEnum)
|
|
47
|
+
.describe("Platform identifier: 'windows'"),
|
|
48
|
+
z.string().describe("Windows version, e.g. '10', '11'"),
|
|
49
|
+
z.string().describe("Browser name, e.g. 'chrome', 'firefox', 'edge'"),
|
|
50
|
+
z
|
|
51
|
+
.string()
|
|
52
|
+
.describe("Browser version, e.g. '132', 'latest', 'oldest'"),
|
|
53
|
+
]),
|
|
54
|
+
// Android: [android, name, model, osVersion, browser]
|
|
55
|
+
z.tuple([
|
|
56
|
+
z
|
|
57
|
+
.literal(PlatformEnum.ANDROID)
|
|
58
|
+
.describe("Platform identifier: 'android'"),
|
|
59
|
+
z
|
|
60
|
+
.string()
|
|
61
|
+
.describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
|
|
62
|
+
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
|
|
63
|
+
z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'"),
|
|
64
|
+
]),
|
|
65
|
+
// iOS: [ios, name, model, osVersion, browser]
|
|
66
|
+
z.tuple([
|
|
67
|
+
z.literal(PlatformEnum.IOS).describe("Platform identifier: 'ios'"),
|
|
68
|
+
z.string().describe("Device name, e.g. 'iPhone 12 Pro'"),
|
|
69
|
+
z.string().describe("iOS version, e.g. '17', 'latest'"),
|
|
70
|
+
z.string().describe("Browser name, typically 'safari'"),
|
|
71
|
+
]),
|
|
72
|
+
// macOS: [mac|macos, name, model, browser, browserVersion]
|
|
73
|
+
z.tuple([
|
|
74
|
+
z
|
|
75
|
+
.nativeEnum(MacOSPlatformEnum)
|
|
76
|
+
.describe("Platform identifier: 'mac' or 'macos'"),
|
|
77
|
+
z.string().describe("macOS version name, e.g. 'Sequoia', 'Ventura'"),
|
|
78
|
+
z.string().describe("Browser name, e.g. 'safari', 'chrome'"),
|
|
79
|
+
z.string().describe("Browser version, e.g. 'latest'"),
|
|
80
|
+
]),
|
|
81
|
+
]))
|
|
82
|
+
.max(3)
|
|
83
|
+
.default([])
|
|
84
|
+
.describe("Preferred tuples of target devices.Add device only when user asks explicitly for it. Defaults to [] . Example: [['windows', '11', 'chrome', 'latest']]"),
|
|
26
85
|
};
|
|
27
86
|
export const SetUpPercySchema = z.object(SetUpPercyParamsShape);
|
|
28
87
|
export const RunTestsOnBrowserStackSchema = z.object(RunTestsOnBrowserStackParamsShape);
|
|
@@ -18,8 +18,8 @@ export declare function getPercyAutomateNotImplementedMessage(type: PercyAutomat
|
|
|
18
18
|
export declare function getBootstrapFailedMessage(error: unknown, context: {
|
|
19
19
|
config: unknown;
|
|
20
20
|
percyMode?: string;
|
|
21
|
-
sdkVersion?: string;
|
|
22
21
|
}): string;
|
|
23
22
|
export declare function percyUnsupportedResult(integrationType: PercyIntegrationTypeEnum, supportCheck?: {
|
|
24
23
|
errorMessage?: string;
|
|
25
24
|
}): CallToolResult;
|
|
25
|
+
export declare function validatePercyPathandFolders(input: any): void;
|
|
@@ -4,6 +4,7 @@ import { isPercyWebFrameworkSupported } from "../percy-web/frameworks.js";
|
|
|
4
4
|
import { formatInstructionsWithNumbers, generateVerificationMessage, } from "./formatUtils.js";
|
|
5
5
|
import { PercyAutomateNotImplementedType, } from "./types.js";
|
|
6
6
|
import { IMPORTANT_SETUP_WARNING } from "./index.js";
|
|
7
|
+
import { PackageJsonVersion } from "../../../index.js";
|
|
7
8
|
export function checkPercyIntegrationSupport(input) {
|
|
8
9
|
if (input.integrationType === PercyIntegrationTypeEnum.AUTOMATE) {
|
|
9
10
|
const isSupported = isPercyAutomateFrameworkSupported(input.detectedLanguage, input.detectedBrowserAutomationFramework || "", input.detectedTestingFramework || "");
|
|
@@ -69,10 +70,11 @@ export function getPercyAutomateNotImplementedMessage(type, input, supported) {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
export function getBootstrapFailedMessage(error, context) {
|
|
73
|
+
const error_message = error instanceof Error ? error.message : "unknown error";
|
|
72
74
|
return `Failed to bootstrap project with BrowserStack SDK.
|
|
73
|
-
Error: ${
|
|
75
|
+
Error: ${error_message}
|
|
74
76
|
Percy Mode: ${context.percyMode ?? "automate"}
|
|
75
|
-
|
|
77
|
+
MCP Version: ${PackageJsonVersion}
|
|
76
78
|
Please open an issue on GitHub if the problem persists.`;
|
|
77
79
|
}
|
|
78
80
|
export function percyUnsupportedResult(integrationType, supportCheck) {
|
|
@@ -88,3 +90,13 @@ export function percyUnsupportedResult(integrationType, supportCheck) {
|
|
|
88
90
|
shouldSkipFormatting: true,
|
|
89
91
|
};
|
|
90
92
|
}
|
|
93
|
+
export function validatePercyPathandFolders(input) {
|
|
94
|
+
const hasFolderPaths = input.folderPaths && input.folderPaths.length > 0;
|
|
95
|
+
const hasFilePaths = input.filePaths && input.filePaths.length > 0;
|
|
96
|
+
if (!hasFolderPaths && !hasFilePaths) {
|
|
97
|
+
throw new Error("Please provide either:\n" +
|
|
98
|
+
"• folderPaths: Array of directory paths containing test files\n" +
|
|
99
|
+
"• filePaths: Array of specific test file paths\n\n" +
|
|
100
|
+
"Example: { filePaths: ['/path/to/test.spec.js'] }");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -2,3 +2,4 @@ import { BrowserStackConfig } from "../../lib/types.js";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
export declare function runTestsOnBrowserStackHandler(rawInput: unknown, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
4
4
|
export declare function setUpPercyHandler(rawInput: unknown, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
5
|
+
export declare function simulatePercyChangeHandler(rawInput: unknown, config: BrowserStackConfig): Promise<CallToolResult>;
|