@browserstack/mcp-server 1.0.12 → 1.0.14
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 +22 -0
- package/dist/config.js +2 -6
- package/dist/index.js +31 -30
- package/dist/lib/api.js +3 -57
- package/dist/lib/constants.js +14 -0
- package/dist/lib/device-cache.js +32 -33
- package/dist/lib/error.js +3 -6
- package/dist/lib/fuzzy.js +1 -4
- package/dist/lib/inmemory-store.js +1 -0
- package/dist/lib/instrumentation.js +12 -18
- package/dist/lib/local.js +27 -35
- package/dist/lib/utils.js +29 -4
- package/dist/logger.js +4 -9
- package/dist/tools/accessibility.js +9 -12
- package/dist/tools/accessiblity-utils/accessibility.js +14 -22
- package/dist/tools/appautomate-utils/appautomate.js +95 -0
- package/dist/tools/appautomate.js +116 -0
- package/dist/tools/applive-utils/fuzzy-search.js +3 -6
- package/dist/tools/applive-utils/start-session.js +14 -20
- package/dist/tools/applive-utils/upload-app.js +12 -51
- package/dist/tools/applive.js +18 -25
- package/dist/tools/automate-utils/fetch-screenshots.js +59 -0
- package/dist/tools/automate.js +44 -37
- package/dist/tools/bstack-sdk.js +14 -18
- package/dist/tools/failurelogs-utils/app-automate.js +88 -0
- package/dist/tools/failurelogs-utils/automate.js +97 -0
- package/dist/tools/getFailureLogs.js +173 -0
- package/dist/tools/live-utils/desktop-filter.js +8 -11
- package/dist/tools/live-utils/mobile-filter.js +7 -10
- package/dist/tools/live-utils/start-session.js +17 -23
- package/dist/tools/live-utils/types.js +2 -5
- package/dist/tools/live-utils/version-resolver.js +1 -4
- package/dist/tools/live.js +23 -29
- package/dist/tools/observability.js +12 -19
- package/dist/tools/sdk-utils/constants.js +3 -9
- package/dist/tools/sdk-utils/instructions.js +4 -9
- package/dist/tools/sdk-utils/types.js +1 -2
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +259 -0
- package/dist/tools/testmanagement-utils/TCG-utils/config.js +5 -0
- package/dist/tools/testmanagement-utils/TCG-utils/helpers.js +53 -0
- package/dist/tools/testmanagement-utils/TCG-utils/types.js +8 -0
- package/dist/tools/testmanagement-utils/add-test-result.js +61 -0
- package/dist/tools/testmanagement-utils/create-project-folder.js +21 -28
- package/dist/tools/testmanagement-utils/create-testcase.js +30 -38
- package/dist/tools/testmanagement-utils/create-testrun.js +23 -30
- package/dist/tools/testmanagement-utils/list-testcases.js +16 -23
- package/dist/tools/testmanagement-utils/list-testruns.js +13 -20
- package/dist/tools/testmanagement-utils/testcase-from-file.js +42 -0
- package/dist/tools/testmanagement-utils/update-testrun.js +16 -23
- package/dist/tools/testmanagement-utils/upload-file.js +101 -0
- package/dist/tools/testmanagement.js +115 -46
- package/package.json +10 -6
|
@@ -1,37 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const logger_1 = __importDefault(require("../../logger"));
|
|
8
|
-
const child_process_1 = __importDefault(require("child_process"));
|
|
9
|
-
const desktop_filter_1 = require("./desktop-filter");
|
|
10
|
-
const mobile_filter_1 = require("./mobile-filter");
|
|
11
|
-
const types_1 = require("./types");
|
|
12
|
-
const local_1 = require("../../lib/local");
|
|
1
|
+
import logger from "../../logger.js";
|
|
2
|
+
import childProcess from "child_process";
|
|
3
|
+
import { filterDesktop } from "./desktop-filter.js";
|
|
4
|
+
import { filterMobile } from "./mobile-filter.js";
|
|
5
|
+
import { PlatformType, } from "./types.js";
|
|
6
|
+
import { isLocalURL, ensureLocalBinarySetup, killExistingBrowserStackLocalProcesses, } from "../../lib/local.js";
|
|
13
7
|
/**
|
|
14
8
|
* Prepares local tunnel setup based on URL type
|
|
15
9
|
*/
|
|
16
10
|
async function prepareLocalTunnel(url) {
|
|
17
|
-
const isLocal =
|
|
11
|
+
const isLocal = isLocalURL(url);
|
|
18
12
|
if (isLocal) {
|
|
19
|
-
await
|
|
13
|
+
await ensureLocalBinarySetup();
|
|
20
14
|
}
|
|
21
15
|
else {
|
|
22
|
-
await
|
|
16
|
+
await killExistingBrowserStackLocalProcesses();
|
|
23
17
|
}
|
|
24
18
|
return isLocal;
|
|
25
19
|
}
|
|
26
20
|
/**
|
|
27
21
|
* Entrypoint: detects platformType & delegates.
|
|
28
22
|
*/
|
|
29
|
-
async function startBrowserSession(args) {
|
|
30
|
-
const entry = args.platformType ===
|
|
31
|
-
? await
|
|
32
|
-
: await
|
|
23
|
+
export async function startBrowserSession(args) {
|
|
24
|
+
const entry = args.platformType === PlatformType.DESKTOP
|
|
25
|
+
? await filterDesktop(args)
|
|
26
|
+
: await filterMobile(args);
|
|
33
27
|
const isLocal = await prepareLocalTunnel(args.url);
|
|
34
|
-
const url = args.platformType ===
|
|
28
|
+
const url = args.platformType === PlatformType.DESKTOP
|
|
35
29
|
? buildDesktopUrl(args, entry, isLocal)
|
|
36
30
|
: buildMobileUrl(args, entry, isLocal);
|
|
37
31
|
openBrowser(url);
|
|
@@ -81,14 +75,14 @@ function openBrowser(launchUrl) {
|
|
|
81
75
|
? ["cmd", "/c", "start", launchUrl]
|
|
82
76
|
: ["xdg-open", launchUrl];
|
|
83
77
|
// nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
|
|
84
|
-
const child =
|
|
78
|
+
const child = childProcess.spawn(command[0], command.slice(1), {
|
|
85
79
|
stdio: "ignore",
|
|
86
80
|
detached: true,
|
|
87
81
|
});
|
|
88
|
-
child.on("error", (err) =>
|
|
82
|
+
child.on("error", (err) => logger.error(`Failed to open browser: ${err}. URL: ${launchUrl}`));
|
|
89
83
|
child.unref();
|
|
90
84
|
}
|
|
91
85
|
catch (err) {
|
|
92
|
-
|
|
86
|
+
logger.error(`Failed to launch browser: ${err}. URL: ${launchUrl}`);
|
|
93
87
|
}
|
|
94
88
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PlatformType = void 0;
|
|
4
|
-
var PlatformType;
|
|
1
|
+
export var PlatformType;
|
|
5
2
|
(function (PlatformType) {
|
|
6
3
|
PlatformType["DESKTOP"] = "desktop";
|
|
7
4
|
PlatformType["MOBILE"] = "mobile";
|
|
8
|
-
})(PlatformType || (
|
|
5
|
+
})(PlatformType || (PlatformType = {}));
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveVersion = resolveVersion;
|
|
4
1
|
/**
|
|
5
2
|
* If req === "latest" or "oldest", returns max/min numeric (or lex)
|
|
6
3
|
* Else if exact match, returns that
|
|
7
4
|
* Else picks the numerically closest (or first)
|
|
8
5
|
*/
|
|
9
|
-
function resolveVersion(requested, available) {
|
|
6
|
+
export function resolveVersion(requested, available) {
|
|
10
7
|
// strip duplicates & sort
|
|
11
8
|
const uniq = Array.from(new Set(available));
|
|
12
9
|
// pick min/max
|
package/dist/tools/live.js
CHANGED
|
@@ -1,36 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
exports.default = addBrowserLiveTools;
|
|
7
|
-
const zod_1 = require("zod");
|
|
8
|
-
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
-
const start_session_1 = require("./live-utils/start-session");
|
|
10
|
-
const types_1 = require("./live-utils/types");
|
|
11
|
-
const instrumentation_1 = require("../lib/instrumentation");
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import logger from "../logger.js";
|
|
3
|
+
import { startBrowserSession } from "./live-utils/start-session.js";
|
|
4
|
+
import { PlatformType } from "./live-utils/types.js";
|
|
5
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
12
6
|
// Define the schema shape
|
|
13
7
|
const LiveArgsShape = {
|
|
14
|
-
platformType:
|
|
15
|
-
.nativeEnum(
|
|
8
|
+
platformType: z
|
|
9
|
+
.nativeEnum(PlatformType)
|
|
16
10
|
.describe("Must be 'desktop' or 'mobile'"),
|
|
17
|
-
desiredURL:
|
|
18
|
-
desiredOS:
|
|
11
|
+
desiredURL: z.string().url().describe("The URL to test"),
|
|
12
|
+
desiredOS: z
|
|
19
13
|
.enum(["Windows", "OS X", "android", "ios", "winphone"])
|
|
20
14
|
.describe("Desktop OS ('Windows' or 'OS X') or mobile OS ('android','ios','winphone')"),
|
|
21
|
-
desiredOSVersion:
|
|
15
|
+
desiredOSVersion: z
|
|
22
16
|
.string()
|
|
23
17
|
.describe("The OS version must be specified as a version number (e.g., '10', '14.0') or as a keyword such as 'latest' or 'oldest'. Normalize variations like 'newest' or 'most recent' to 'latest', and terms like 'earliest' or 'first' to 'oldest'. For macOS, version names (e.g., 'Sequoia') must be used instead of numeric versions."),
|
|
24
|
-
desiredBrowser:
|
|
18
|
+
desiredBrowser: z
|
|
25
19
|
.enum(["chrome", "firefox", "safari", "edge", "ie"])
|
|
26
20
|
.describe("Browser for desktop (Chrome, IE, Firefox, Safari, Edge)"),
|
|
27
|
-
desiredBrowserVersion:
|
|
21
|
+
desiredBrowserVersion: z
|
|
28
22
|
.string()
|
|
29
23
|
.optional()
|
|
30
24
|
.describe("Browser version for desktop (e.g. '133.2', 'latest'). If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'."),
|
|
31
|
-
desiredDevice:
|
|
25
|
+
desiredDevice: z.string().optional().describe("Device name for mobile"),
|
|
32
26
|
};
|
|
33
|
-
const LiveArgsSchema =
|
|
27
|
+
const LiveArgsSchema = z.object(LiveArgsShape);
|
|
34
28
|
/**
|
|
35
29
|
* Launches a desktop browser session
|
|
36
30
|
*/
|
|
@@ -39,8 +33,8 @@ async function launchDesktopSession(args) {
|
|
|
39
33
|
throw new Error("You must provide a desiredBrowser");
|
|
40
34
|
if (!args.desiredBrowserVersion)
|
|
41
35
|
throw new Error("You must provide a desiredBrowserVersion");
|
|
42
|
-
return
|
|
43
|
-
platformType:
|
|
36
|
+
return startBrowserSession({
|
|
37
|
+
platformType: PlatformType.DESKTOP,
|
|
44
38
|
url: args.desiredURL,
|
|
45
39
|
os: args.desiredOS,
|
|
46
40
|
osVersion: args.desiredOSVersion,
|
|
@@ -54,8 +48,8 @@ async function launchDesktopSession(args) {
|
|
|
54
48
|
async function launchMobileSession(args) {
|
|
55
49
|
if (!args.desiredDevice)
|
|
56
50
|
throw new Error("You must provide a desiredDevice");
|
|
57
|
-
return
|
|
58
|
-
platformType:
|
|
51
|
+
return startBrowserSession({
|
|
52
|
+
platformType: PlatformType.MOBILE,
|
|
59
53
|
browser: args.desiredBrowser,
|
|
60
54
|
url: args.desiredURL,
|
|
61
55
|
os: args.desiredOS,
|
|
@@ -70,7 +64,7 @@ async function runBrowserSession(rawArgs) {
|
|
|
70
64
|
// Validate and narrow
|
|
71
65
|
const args = LiveArgsSchema.parse(rawArgs);
|
|
72
66
|
// Branch desktop vs mobile and delegate
|
|
73
|
-
const launchUrl = args.platformType ===
|
|
67
|
+
const launchUrl = args.platformType === PlatformType.DESKTOP
|
|
74
68
|
? await launchDesktopSession(args)
|
|
75
69
|
: await launchMobileSession(args);
|
|
76
70
|
return {
|
|
@@ -82,15 +76,15 @@ async function runBrowserSession(rawArgs) {
|
|
|
82
76
|
],
|
|
83
77
|
};
|
|
84
78
|
}
|
|
85
|
-
function addBrowserLiveTools(server) {
|
|
79
|
+
export default function addBrowserLiveTools(server) {
|
|
86
80
|
server.tool("runBrowserLiveSession", "Launch a BrowserStack Live session (desktop or mobile).", LiveArgsShape, async (args) => {
|
|
87
81
|
try {
|
|
88
|
-
|
|
82
|
+
trackMCP("runBrowserLiveSession", server.server.getClientVersion());
|
|
89
83
|
return await runBrowserSession(args);
|
|
90
84
|
}
|
|
91
85
|
catch (error) {
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
logger.error("Live session failed: %s", error);
|
|
87
|
+
trackMCP("runBrowserLiveSession", server.server.getClientVersion(), error);
|
|
94
88
|
return {
|
|
95
89
|
content: [
|
|
96
90
|
{
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
exports.default = addObservabilityTools;
|
|
8
|
-
const zod_1 = require("zod");
|
|
9
|
-
const api_1 = require("../lib/api");
|
|
10
|
-
const instrumentation_1 = require("../lib/instrumentation");
|
|
11
|
-
const logger_1 = __importDefault(require("../logger"));
|
|
12
|
-
async function getFailuresInLastRun(buildName, projectName) {
|
|
13
|
-
const buildsData = await (0, api_1.getLatestO11YBuildInfo)(buildName, projectName);
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getLatestO11YBuildInfo } from "../lib/api.js";
|
|
3
|
+
import { trackMCP } from "../lib/instrumentation.js";
|
|
4
|
+
import logger from "../logger.js";
|
|
5
|
+
export async function getFailuresInLastRun(buildName, projectName) {
|
|
6
|
+
const buildsData = await getLatestO11YBuildInfo(buildName, projectName);
|
|
14
7
|
const observabilityUrl = buildsData.observability_url;
|
|
15
8
|
if (!observabilityUrl) {
|
|
16
9
|
throw new Error("No observability URL found in build data, this is likely because the build is not yet available on BrowserStack Observability.");
|
|
@@ -35,22 +28,22 @@ async function getFailuresInLastRun(buildName, projectName) {
|
|
|
35
28
|
],
|
|
36
29
|
};
|
|
37
30
|
}
|
|
38
|
-
function addObservabilityTools(server) {
|
|
31
|
+
export default function addObservabilityTools(server) {
|
|
39
32
|
server.tool("getFailuresInLastRun", "Use this tool to debug failures in the last run of the test suite on BrowserStack. Use only when browserstack.yml file is present in the project root.", {
|
|
40
|
-
buildName:
|
|
33
|
+
buildName: z
|
|
41
34
|
.string()
|
|
42
35
|
.describe("Name of the build to get failures for. This is the 'build' key in the browserstack.yml file. If not sure, ask the user for the build name."),
|
|
43
|
-
projectName:
|
|
36
|
+
projectName: z
|
|
44
37
|
.string()
|
|
45
38
|
.describe("Name of the project to get failures for. This is the 'projectName' key in the browserstack.yml file. If not sure, ask the user for the project name."),
|
|
46
39
|
}, async (args) => {
|
|
47
40
|
try {
|
|
48
|
-
|
|
41
|
+
trackMCP("getFailuresInLastRun", server.server.getClientVersion());
|
|
49
42
|
return await getFailuresInLastRun(args.buildName, args.projectName);
|
|
50
43
|
}
|
|
51
44
|
catch (error) {
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
logger.error("Failed to get failures in the last run: %s", error);
|
|
46
|
+
trackMCP("getFailuresInLastRun", server.server.getClientVersion(), error);
|
|
54
47
|
return {
|
|
55
48
|
content: [
|
|
56
49
|
{
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SUPPORTED_CONFIGURATIONS = void 0;
|
|
7
|
-
const config_1 = __importDefault(require("../../config"));
|
|
1
|
+
import config from "../../config.js";
|
|
8
2
|
const nodejsInstructions = `
|
|
9
3
|
- Ensure that \`browserstack-node-sdk\` is present in package.json, use the latest version.
|
|
10
4
|
- Add new scripts to package.json for running tests on BrowserStack (use \`npx\` to trigger the sdk):
|
|
@@ -27,7 +21,7 @@ python3 -m pip install browserstack-sdk
|
|
|
27
21
|
|
|
28
22
|
Run the following command to setup the browserstack-sdk:
|
|
29
23
|
\`\`\`bash
|
|
30
|
-
browserstack-sdk setup --username "${
|
|
24
|
+
browserstack-sdk setup --username "${config.browserstackUsername}" --key "${config.browserstackAccessKey}"
|
|
31
25
|
\`\`\`
|
|
32
26
|
|
|
33
27
|
In order to run tests on BrowserStack, run the following command:
|
|
@@ -35,7 +29,7 @@ In order to run tests on BrowserStack, run the following command:
|
|
|
35
29
|
browserstack-sdk python <path-to-test-file>
|
|
36
30
|
\`\`\`
|
|
37
31
|
`;
|
|
38
|
-
|
|
32
|
+
export const SUPPORTED_CONFIGURATIONS = {
|
|
39
33
|
nodejs: {
|
|
40
34
|
playwright: {
|
|
41
35
|
jest: { instructions: nodejsInstructions },
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getInstructionsForProjectConfiguration = void 0;
|
|
4
|
-
exports.generateBrowserStackYMLInstructions = generateBrowserStackYMLInstructions;
|
|
5
|
-
const constants_1 = require("./constants");
|
|
1
|
+
import { SUPPORTED_CONFIGURATIONS } from "./constants.js";
|
|
6
2
|
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";
|
|
7
|
-
const getInstructionsForProjectConfiguration = (detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage) => {
|
|
8
|
-
const configuration =
|
|
3
|
+
export const getInstructionsForProjectConfiguration = (detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage) => {
|
|
4
|
+
const configuration = SUPPORTED_CONFIGURATIONS[detectedLanguage];
|
|
9
5
|
if (!configuration) {
|
|
10
6
|
throw new Error(`BrowserStack MCP Server currently does not support ${detectedLanguage}, ${errorMessageSuffix}`);
|
|
11
7
|
}
|
|
@@ -17,8 +13,7 @@ const getInstructionsForProjectConfiguration = (detectedBrowserAutomationFramewo
|
|
|
17
13
|
}
|
|
18
14
|
return configuration[detectedBrowserAutomationFramework][detectedTestingFramework].instructions;
|
|
19
15
|
};
|
|
20
|
-
|
|
21
|
-
function generateBrowserStackYMLInstructions(desiredPlatforms) {
|
|
16
|
+
export function generateBrowserStackYMLInstructions(desiredPlatforms) {
|
|
22
17
|
return `
|
|
23
18
|
Create a browserstack.yml file in the project root. The file should be in the following format:
|
|
24
19
|
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { TCG_TRIGGER_URL, TCG_POLL_URL, FETCH_DETAILS_URL, FORM_FIELDS_URL, BULK_CREATE_URL, } from "./config.js";
|
|
3
|
+
import { createTestCasePayload } from "./helpers.js";
|
|
4
|
+
import config from "../../../config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Fetch default and custom form fields for a project.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchFormFields(projectId) {
|
|
9
|
+
const res = await axios.get(FORM_FIELDS_URL(projectId), {
|
|
10
|
+
headers: {
|
|
11
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
return res.data;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Trigger AI-based test case generation for a document.
|
|
18
|
+
*/
|
|
19
|
+
export async function triggerTestCaseGeneration(document, documentId, folderId, projectId, source) {
|
|
20
|
+
const res = await axios.post(TCG_TRIGGER_URL, {
|
|
21
|
+
document,
|
|
22
|
+
documentId,
|
|
23
|
+
folderId,
|
|
24
|
+
projectId,
|
|
25
|
+
source,
|
|
26
|
+
webhookUrl: `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/webhooks/tcg`,
|
|
27
|
+
}, {
|
|
28
|
+
headers: {
|
|
29
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"request-source": source,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (res.status !== 200) {
|
|
35
|
+
throw new Error(`Trigger failed: ${res.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
return res.data["x-bstack-traceRequestId"];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Initiate a fetch for test-case details; returns the traceRequestId for polling.
|
|
41
|
+
*/
|
|
42
|
+
export async function fetchTestCaseDetails(documentId, folderId, projectId, testCaseIds, source) {
|
|
43
|
+
if (testCaseIds.length === 0) {
|
|
44
|
+
throw new Error("No testCaseIds provided to fetchTestCaseDetails");
|
|
45
|
+
}
|
|
46
|
+
const res = await axios.post(FETCH_DETAILS_URL, {
|
|
47
|
+
document_id: documentId,
|
|
48
|
+
folder_id: folderId,
|
|
49
|
+
project_id: projectId,
|
|
50
|
+
test_case_ids: testCaseIds,
|
|
51
|
+
}, {
|
|
52
|
+
headers: {
|
|
53
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
54
|
+
"request-source": source,
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (res.data.data.success !== true) {
|
|
59
|
+
throw new Error(`Fetch details failed: ${res.data.data.message}`);
|
|
60
|
+
}
|
|
61
|
+
return res.data.request_trace_id;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Poll for a given traceRequestId until all test-case details are returned.
|
|
65
|
+
*/
|
|
66
|
+
export async function pollTestCaseDetails(traceRequestId) {
|
|
67
|
+
const detailMap = {};
|
|
68
|
+
let done = false;
|
|
69
|
+
while (!done) {
|
|
70
|
+
// add a bit of jitter to avoid synchronized polling storms
|
|
71
|
+
await new Promise((r) => setTimeout(r, 10000 + Math.random() * 5000));
|
|
72
|
+
const poll = await axios.post(`${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`, {}, {
|
|
73
|
+
headers: {
|
|
74
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
if (!poll.data.data.success) {
|
|
78
|
+
throw new Error(`Polling failed: ${poll.data.data.message}`);
|
|
79
|
+
}
|
|
80
|
+
for (const msg of poll.data.data.message) {
|
|
81
|
+
if (msg.type === "termination") {
|
|
82
|
+
done = true;
|
|
83
|
+
}
|
|
84
|
+
if (msg.type === "testcase_details") {
|
|
85
|
+
for (const test of msg.data.testcase_details) {
|
|
86
|
+
detailMap[test.id] = {
|
|
87
|
+
steps: test.steps,
|
|
88
|
+
preconditions: test.preconditions,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return detailMap;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Poll for scenarios & testcases, trigger detail fetches, then poll all details in parallel.
|
|
98
|
+
*/
|
|
99
|
+
export async function pollScenariosTestDetails(args, traceId, context, documentId, source) {
|
|
100
|
+
const { folderId, projectReferenceId } = args;
|
|
101
|
+
const scenariosMap = {};
|
|
102
|
+
const detailPromises = [];
|
|
103
|
+
let iteratorCount = 0;
|
|
104
|
+
// Promisify interval-style polling using a wrapper
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
const intervalId = setInterval(async () => {
|
|
107
|
+
try {
|
|
108
|
+
const poll = await axios.post(`${TCG_POLL_URL}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`, {}, {
|
|
109
|
+
headers: {
|
|
110
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (poll.status !== 200) {
|
|
114
|
+
clearInterval(intervalId);
|
|
115
|
+
reject(new Error(`Polling error: ${poll.statusText}`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const msg of poll.data.data.message) {
|
|
119
|
+
if (msg.type === "scenario") {
|
|
120
|
+
msg.data.scenarios.forEach((sc) => {
|
|
121
|
+
scenariosMap[sc.id] = { id: sc.id, name: sc.name, testcases: [] };
|
|
122
|
+
});
|
|
123
|
+
const count = Object.keys(scenariosMap).length;
|
|
124
|
+
await context.sendNotification({
|
|
125
|
+
method: "notifications/progress",
|
|
126
|
+
params: {
|
|
127
|
+
progressToken: context._meta?.progressToken ?? traceId,
|
|
128
|
+
progress: count,
|
|
129
|
+
total: count,
|
|
130
|
+
message: `Fetched ${count} scenarios`,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (msg.type === "testcase") {
|
|
135
|
+
const sc = msg.data.scenario;
|
|
136
|
+
if (sc) {
|
|
137
|
+
const array = Array.isArray(msg.data.testcases)
|
|
138
|
+
? msg.data.testcases
|
|
139
|
+
: msg.data.testcases
|
|
140
|
+
? [msg.data.testcases]
|
|
141
|
+
: [];
|
|
142
|
+
const ids = array.map((tc) => tc.id || tc.test_case_id);
|
|
143
|
+
const reqId = await fetchTestCaseDetails(documentId, folderId, projectReferenceId, ids, source);
|
|
144
|
+
detailPromises.push(pollTestCaseDetails(reqId));
|
|
145
|
+
scenariosMap[sc.id] ||= {
|
|
146
|
+
id: sc.id,
|
|
147
|
+
name: sc.name,
|
|
148
|
+
testcases: [],
|
|
149
|
+
traceId,
|
|
150
|
+
};
|
|
151
|
+
scenariosMap[sc.id].testcases.push(...array);
|
|
152
|
+
iteratorCount++;
|
|
153
|
+
const total = Object.keys(scenariosMap).length;
|
|
154
|
+
await context.sendNotification({
|
|
155
|
+
method: "notifications/progress",
|
|
156
|
+
params: {
|
|
157
|
+
progressToken: context._meta?.progressToken ?? traceId,
|
|
158
|
+
progress: iteratorCount,
|
|
159
|
+
total,
|
|
160
|
+
message: `Fetched ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (msg.type === "termination") {
|
|
166
|
+
clearInterval(intervalId);
|
|
167
|
+
resolve();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
clearInterval(intervalId);
|
|
173
|
+
reject(err);
|
|
174
|
+
}
|
|
175
|
+
}, 10000); // 10 second interval
|
|
176
|
+
});
|
|
177
|
+
// once all detail fetches are triggered, wait for them to complete
|
|
178
|
+
const detailsList = await Promise.all(detailPromises);
|
|
179
|
+
const allDetails = detailsList.reduce((acc, cur) => ({ ...acc, ...cur }), {});
|
|
180
|
+
// attach the fetched detail objects back to each testcase
|
|
181
|
+
for (const scenario of Object.values(scenariosMap)) {
|
|
182
|
+
scenario.testcases = scenario.testcases.map((tc) => ({
|
|
183
|
+
...tc,
|
|
184
|
+
...(allDetails[tc.id || tc.test_case_id] ?? {}),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
return scenariosMap;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Bulk-create generated test cases in BrowserStack.
|
|
191
|
+
*/
|
|
192
|
+
export async function bulkCreateTestCases(scenariosMap, projectId, folderId, fieldMaps, booleanFieldId, traceId, context, documentId) {
|
|
193
|
+
const results = {};
|
|
194
|
+
const total = Object.keys(scenariosMap).length;
|
|
195
|
+
let doneCount = 0;
|
|
196
|
+
let testCaseCount = 0;
|
|
197
|
+
for (const { id, testcases } of Object.values(scenariosMap)) {
|
|
198
|
+
const testCaseLength = testcases.length;
|
|
199
|
+
testCaseCount += testCaseLength;
|
|
200
|
+
if (testCaseLength === 0)
|
|
201
|
+
continue;
|
|
202
|
+
const payload = {
|
|
203
|
+
test_cases: testcases.map((tc) => createTestCasePayload(tc, id, folderId, fieldMaps, documentId, booleanFieldId, traceId)),
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
const resp = await axios.post(BULK_CREATE_URL(projectId, folderId), payload, {
|
|
207
|
+
headers: {
|
|
208
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
results[id] = resp.data;
|
|
213
|
+
await context.sendNotification({
|
|
214
|
+
method: "notifications/progress",
|
|
215
|
+
params: {
|
|
216
|
+
progressToken: context._meta?.progressToken ?? "bulk-create",
|
|
217
|
+
message: `Bulk create done for scenario ${doneCount} of ${total}`,
|
|
218
|
+
total,
|
|
219
|
+
progress: doneCount,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
//send notification
|
|
225
|
+
await context.sendNotification({
|
|
226
|
+
method: "notifications/progress",
|
|
227
|
+
params: {
|
|
228
|
+
progressToken: context._meta?.progressToken ?? traceId,
|
|
229
|
+
message: `Bulk create failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
230
|
+
total,
|
|
231
|
+
progress: doneCount,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
//continue to next scenario
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
doneCount++;
|
|
238
|
+
}
|
|
239
|
+
const resultString = `Total of ${testCaseCount} test cases created in ${total} scenarios.`;
|
|
240
|
+
return resultString;
|
|
241
|
+
}
|
|
242
|
+
export async function projectIdentifierToId(projectId) {
|
|
243
|
+
const url = `https://test-management.browserstack.com/api/v1/projects/?q=${projectId}`;
|
|
244
|
+
const response = await axios.get(url, {
|
|
245
|
+
headers: {
|
|
246
|
+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
|
|
247
|
+
accept: "application/json, text/plain, */*",
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
if (response.data.success !== true) {
|
|
251
|
+
throw new Error(`Failed to fetch project ID: ${response.statusText}`);
|
|
252
|
+
}
|
|
253
|
+
for (const project of response.data.projects) {
|
|
254
|
+
if (project.identifier === projectId) {
|
|
255
|
+
return project.id;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Project with identifier ${projectId} not found.`);
|
|
259
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const TCG_TRIGGER_URL = "https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/suggest-test-cases";
|
|
2
|
+
export const TCG_POLL_URL = "https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/test-cases-polling";
|
|
3
|
+
export const FETCH_DETAILS_URL = "https://test-management.browserstack.com/api/v1/integration/tcg/test-generation/fetch-test-case-details";
|
|
4
|
+
export const FORM_FIELDS_URL = (projectId) => `https://test-management.browserstack.com/api/v1/projects/${projectId}/form-fields-v2`;
|
|
5
|
+
export const BULK_CREATE_URL = (projectId, folderId) => `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/bulk-test-cases`;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build mappings for default fields for priority, status, and case type.
|
|
3
|
+
*/
|
|
4
|
+
export function buildDefaultFieldMaps(defaultFields) {
|
|
5
|
+
const priority = Object.fromEntries(defaultFields.priority.values.map((v) => [
|
|
6
|
+
v.name.toLowerCase(),
|
|
7
|
+
v.value,
|
|
8
|
+
]));
|
|
9
|
+
const status = Object.fromEntries(defaultFields.status.values.map((v) => [v.internal_name, v.value]));
|
|
10
|
+
const caseType = Object.fromEntries(defaultFields.case_type.values.map((v) => [v.internal_name, v.value]));
|
|
11
|
+
return { priority, status, caseType };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Find a boolean custom field ID if present.
|
|
15
|
+
*/
|
|
16
|
+
export function findBooleanFieldId(customFields) {
|
|
17
|
+
const boolField = customFields.find((f) => f.field_type === "field_boolean");
|
|
18
|
+
return boolField?.id;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Construct payload for creating a single test case in bulk.
|
|
22
|
+
*/
|
|
23
|
+
export function createTestCasePayload(tc, scenarioId, folderId, fieldMaps, documentId, booleanFieldId, traceId) {
|
|
24
|
+
const pri = tc.priority ?? "Medium";
|
|
25
|
+
const stat = fieldMaps.status["active"];
|
|
26
|
+
const ct = fieldMaps.caseType["functional"];
|
|
27
|
+
return {
|
|
28
|
+
attachments: [documentId],
|
|
29
|
+
name: tc.name,
|
|
30
|
+
description: tc.description,
|
|
31
|
+
test_case_folder_id: folderId,
|
|
32
|
+
priority: pri,
|
|
33
|
+
status: stat,
|
|
34
|
+
case_type: ct,
|
|
35
|
+
automation_status: "not_automated",
|
|
36
|
+
fetch_ai_test_case_details: true,
|
|
37
|
+
template: "test_case_steps",
|
|
38
|
+
metadata: JSON.stringify({
|
|
39
|
+
ai_prompt: {
|
|
40
|
+
attachment_id: documentId,
|
|
41
|
+
rich_text_id: null,
|
|
42
|
+
scenario: scenarioId,
|
|
43
|
+
test_case_count: tc.test_case_count || 1,
|
|
44
|
+
uuid: tc.uuid || crypto.randomUUID?.() || "unknown-uuid",
|
|
45
|
+
"x-bstack-traceRequestId": traceId,
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
tags: ["AI Generated", "MCP Generated"],
|
|
49
|
+
custom_fields: booleanFieldId ? { [booleanFieldId]: false } : undefined,
|
|
50
|
+
test_case_steps: tc.steps,
|
|
51
|
+
preconditions: tc.preconditions,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const CreateTestCasesFromFileSchema = z.object({
|
|
3
|
+
documentId: z.string().describe("Internal document identifier"),
|
|
4
|
+
folderId: z.string().describe("BrowserStack folder ID"),
|
|
5
|
+
projectReferenceId: z
|
|
6
|
+
.string()
|
|
7
|
+
.describe("The BrowserStack project reference ID is a unique identifier found in the project URL within the BrowserStack Test Management Platform. This ID is also returned by the Upload Document tool."),
|
|
8
|
+
});
|