Skip to content

Commit 939347d

Browse files
feat: implement create LCA steps functionality and polling mechanism for BrowserStack Test Management
1 parent eee8f17 commit 939347d

File tree

5 files changed

+573
-1
lines changed

5 files changed

+573
-1
lines changed

src/tools/testmanagement-utils/TCG-utils/api.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,61 @@ export async function projectIdentifierToId(
361361
}
362362
throw new Error(`Project with identifier ${projectId} not found.`);
363363
}
364+
365+
export async function testCaseIdentifierToDetails(
366+
projectId: string,
367+
testCaseIdentifier: string,
368+
): Promise<{ testCaseId: string; folderId: string }> {
369+
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;
370+
371+
const response = await axios.get(url, {
372+
headers: {
373+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
374+
accept: "application/json, text/plain, */*",
375+
},
376+
});
377+
378+
if (response.data.success !== true) {
379+
throw new Error(
380+
`Failed to fetch test case details: ${response.statusText}`,
381+
);
382+
}
383+
384+
// Check if test_cases array exists and has items
385+
if (
386+
!response.data.test_cases ||
387+
!Array.isArray(response.data.test_cases) ||
388+
response.data.test_cases.length === 0
389+
) {
390+
throw new Error(
391+
`No test cases found in response for identifier ${testCaseIdentifier}`,
392+
);
393+
}
394+
395+
for (const testCase of response.data.test_cases) {
396+
if (testCase.identifier === testCaseIdentifier) {
397+
// Extract folder ID from the links.folder URL
398+
// URL format: "/api/v1/projects/1930314/folder/10193436/test-cases"
399+
let folderId = "";
400+
if (testCase.links && testCase.links.folder) {
401+
const folderMatch = testCase.links.folder.match(/\/folder\/(\d+)\//);
402+
if (folderMatch && folderMatch[1]) {
403+
folderId = folderMatch[1];
404+
}
405+
}
406+
407+
if (!folderId) {
408+
throw new Error(
409+
`Could not extract folder ID for test case ${testCaseIdentifier}`,
410+
);
411+
}
412+
413+
return {
414+
testCaseId: testCase.id.toString(),
415+
folderId: folderId,
416+
};
417+
}
418+
}
419+
420+
throw new Error(`Test case with identifier ${testCaseIdentifier} not found.`);
421+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import axios from "axios";
4+
import config from "../../config.js";
5+
import { formatAxiosError } from "../../lib/error.js";
6+
import {
7+
projectIdentifierToId,
8+
testCaseIdentifierToDetails,
9+
} from "./TCG-utils/api.js";
10+
import { pollLCAStatus } from "./poll-lca-status.js";
11+
12+
/**
13+
* Schema for creating LCA steps for a test case
14+
*/
15+
export const CreateLCAStepsSchema = z.object({
16+
project_identifier: z
17+
.string()
18+
.describe("ID of the project (Starts with 'PR-')"),
19+
test_case_identifier: z
20+
.string()
21+
.describe("Identifier of the test case (e.g., 'TC-12345')"),
22+
base_url: z.string().describe("Base URL for the test (e.g., 'google.com')"),
23+
credentials: z
24+
.object({
25+
username: z.string().describe("Username for authentication"),
26+
password: z.string().describe("Password for authentication"),
27+
})
28+
.optional()
29+
.describe("Optional credentials for authentication"),
30+
local_enabled: z
31+
.boolean()
32+
.optional()
33+
.default(false)
34+
.describe("Whether local testing is enabled"),
35+
test_name: z.string().describe("Name of the test"),
36+
test_case_details: z
37+
.object({
38+
name: z.string().describe("Name of the test case"),
39+
description: z.string().describe("Description of the test case"),
40+
preconditions: z.string().describe("Preconditions for the test case"),
41+
test_case_steps: z
42+
.array(
43+
z.object({
44+
step: z.string().describe("Test step description"),
45+
result: z.string().describe("Expected result"),
46+
}),
47+
)
48+
.describe("Array of test case steps with expected results"),
49+
})
50+
.describe("Test case details including steps"),
51+
wait_for_completion: z
52+
.boolean()
53+
.optional()
54+
.default(true)
55+
.describe("Whether to wait for LCA build completion (default: true)"),
56+
});
57+
58+
export type CreateLCAStepsArgs = z.infer<typeof CreateLCAStepsSchema>;
59+
60+
/**
61+
* Creates LCA (Low Code Automation) steps for a test case in BrowserStack Test Management
62+
*/
63+
export async function createLCASteps(
64+
args: CreateLCAStepsArgs,
65+
context: any,
66+
): Promise<CallToolResult> {
67+
try {
68+
// Get the project ID from identifier
69+
const projectId = await projectIdentifierToId(args.project_identifier);
70+
71+
// Get the test case ID and folder ID from identifier
72+
const { testCaseId, folderId } = await testCaseIdentifierToDetails(
73+
projectId,
74+
args.test_case_identifier,
75+
);
76+
77+
const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;
78+
79+
const payload = {
80+
base_url: args.base_url,
81+
credentials: args.credentials,
82+
local_enabled: args.local_enabled,
83+
test_name: args.test_name,
84+
test_case_details: args.test_case_details,
85+
version: "v2",
86+
webhook_path: `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
87+
};
88+
89+
const response = await axios.post(url, payload, {
90+
headers: {
91+
"API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
92+
accept: "application/json, text/plain, */*",
93+
"Content-Type": "application/json",
94+
},
95+
});
96+
97+
if (response.status >= 200 && response.status < 300) {
98+
// Check if user wants to wait for completion
99+
if (!args.wait_for_completion) {
100+
return {
101+
content: [
102+
{
103+
type: "text",
104+
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
105+
},
106+
{
107+
type: "text",
108+
text: "LCA build started. Check the BrowserStack Lowcode Automation UI for completion status.",
109+
},
110+
],
111+
};
112+
}
113+
114+
// Start polling for LCA build completion
115+
try {
116+
const max_wait_minutes = 10; // Maximum wait time in minutes
117+
const maxWaitMs = max_wait_minutes * 60 * 1000;
118+
const lcaResult = await pollLCAStatus(
119+
projectId,
120+
folderId,
121+
testCaseId,
122+
context,
123+
maxWaitMs, // max wait time
124+
2 * 60 * 1000, // 2 minutes initial wait
125+
10 * 1000, // 10 seconds interval
126+
);
127+
128+
if (lcaResult && lcaResult.status === "done") {
129+
return {
130+
content: [
131+
{
132+
type: "text",
133+
text: `Successfully created LCA steps for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
134+
},
135+
{
136+
type: "text",
137+
text: `LCA build completed! Resource URL: ${lcaResult.resource_path}`,
138+
},
139+
],
140+
};
141+
} else {
142+
return {
143+
content: [
144+
{
145+
type: "text",
146+
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
147+
},
148+
{
149+
type: "text",
150+
text: `Warning: LCA build did not complete within ${max_wait_minutes} minutes. You can check the status later in the BrowserStack Test Management UI.`,
151+
},
152+
],
153+
};
154+
}
155+
} catch (pollError) {
156+
console.error("Error during LCA polling:", pollError);
157+
return {
158+
content: [
159+
{
160+
type: "text",
161+
text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
162+
},
163+
{
164+
type: "text",
165+
text: "Warning: Error occurred while polling for LCA build completion. Check the BrowserStack Test Management UI for status.",
166+
},
167+
],
168+
};
169+
}
170+
} else {
171+
throw new Error(`Unexpected response: ${JSON.stringify(response.data)}`);
172+
}
173+
} catch (error) {
174+
// Add more specific error handling
175+
if (error instanceof Error) {
176+
if (error.message.includes("not found")) {
177+
return {
178+
content: [
179+
{
180+
type: "text",
181+
text: `Error: ${error.message}. Please verify that the project identifier "${args.project_identifier}" and test case identifier "${args.test_case_identifier}" are correct.`,
182+
isError: true,
183+
},
184+
],
185+
isError: true,
186+
};
187+
}
188+
}
189+
return formatAxiosError(error, "Failed to create LCA steps");
190+
}
191+
}

0 commit comments

Comments
 (0)