Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,036 changes: 661 additions & 2,375 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@modelcontextprotocol/sdk": "^1.29.0",
"@types/form-data": "^2.5.2",
"axios": "^1.13.2",
"browserstack-local": "^1.5.8",
"csv-parse": "^6.1.0",
"dotenv": "^17.2.3",
"axios": "^1.14.0",
"browserstack-local": "^1.5.12",
"csv-parse": "^6.2.1",
"dotenv": "^17.4.0",
"form-data": "^4.0.5",
"pino": "^10.1.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"sharp": "^0.34.5",
"uuid": "^13.0.0",
"webdriverio": "^9.21.0",
"zod": "^4.2.1"
"webdriverio": "^9.27.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
1 change: 1 addition & 0 deletions src/tools/appautomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export default function addAppAutomationTools(
text: `Error during app automation or screenshot capture: ${errorMessage}`,
},
],
isError: true,
};
}
},
Expand Down
13 changes: 12 additions & 1 deletion src/tools/automate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,17 @@ export async function fetchAutomationScreenshotsTool(
};
} catch (error) {
logger.error("Error during fetching screenshots", error);
throw error;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error during fetching screenshots: ${errorMessage}`,
},
],
isError: true,
};
}
}

Expand Down Expand Up @@ -99,6 +109,7 @@ export default function addAutomationTools(
text: `Error during fetching automate screenshots: ${errorMessage}`,
},
],
isError: true,
};
}
},
Expand Down
14 changes: 13 additions & 1 deletion src/tools/rca-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function getBuildIdTool(
text: `Error fetching build ID: ${errorMessage}`,
},
],
isError: true,
};
}
}
Expand Down Expand Up @@ -83,7 +84,17 @@ export async function fetchRCADataTool(
};
} catch (error) {
logger.error("Error fetching RCA data", error);
throw error;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error fetching RCA data: ${errorMessage}`,
},
],
isError: true,
};
}
}

Expand Down Expand Up @@ -120,6 +131,7 @@ export async function listTestIdsTool(
text: `Error listing test IDs: ${errorMessage}`,
},
],
isError: true,
};
}
}
Expand Down
13 changes: 12 additions & 1 deletion src/tools/selfheal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ export async function fetchSelfHealSelectorTool(
};
} catch (error) {
logger.error("Error fetching self-heal selector suggestions", error);
throw error;
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: `Error fetching self-heal selector suggestions: ${errorMessage}`,
},
],
isError: true,
};
}
}

Expand Down Expand Up @@ -67,6 +77,7 @@ export default function addSelfHealTools(
text: `Error during fetching self-heal suggestions: ${errorMessage}`,
},
],
isError: true,
};
}
},
Expand Down
139 changes: 139 additions & 0 deletions tests/tools/accessibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import addAccessibilityTools from "../../src/tools/accessibility";

vi.mock("../../src/tools/accessiblity-utils/accessibility-rag", () => ({
queryAccessibilityRAG: vi.fn().mockResolvedValue({
content: [{ type: "text", text: "WCAG guidelines say..." }],
}),
}));
vi.mock("../../src/tools/accessiblity-utils/scanner", () => ({
AccessibilityScanner: vi.fn().mockImplementation(() => ({
setAuth: vi.fn(),
startScan: vi.fn().mockResolvedValue({ id: "scan-1", scanRunId: "run-1" }),
waitUntilComplete: vi.fn().mockResolvedValue({ status: "completed" }),
})),
}));
vi.mock("../../src/tools/accessiblity-utils/report-fetcher", () => ({
AccessibilityReportFetcher: vi.fn().mockImplementation(() => ({
setAuth: vi.fn(),
getReportLink: vi.fn().mockResolvedValue({
csvReportUrl: "https://example.com/report.csv",
reportUrl: "https://example.com/report",
}),
})),
}));
vi.mock("../../src/tools/accessiblity-utils/report-parser", () => ({
parseAccessibilityReportFromCSV: vi.fn().mockResolvedValue({
records: [{ issue: "Low contrast", severity: "serious" }],
pageLength: 1,
}),
}));
vi.mock("../../src/tools/accessiblity-utils/auth-config", () => {
return {
AccessibilityAuthConfig: class {
setAuth = vi.fn();
createBasicAuthConfig = vi.fn().mockResolvedValue({
data: { id: "auth-1", name: "test" },
});
createFormAuthConfig = vi.fn().mockResolvedValue({
data: { id: "auth-2", name: "test-form" },
});
getAuthConfig = vi.fn().mockResolvedValue({
data: [{ id: "auth-1", name: "test" }],
});
},
};
});
vi.mock("../../src/lib/get-auth", () => ({
getBrowserStackAuth: vi.fn().mockReturnValue("fake-user:fake-key"),
}));
vi.mock("../../src/logger", () => ({
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() },
}));
vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() }));

const mockConfig = {
"browserstack-username": "fake-user",
"browserstack-access-key": "fake-key",
};

describe("Accessibility Tools", () => {
let serverMock: any;
let handlers: Record<string, (...args: any[]) => any>;

beforeEach(() => {
vi.clearAllMocks();
handlers = {};
serverMock = {
tool: vi.fn((name: string, _desc: string, _schema: any, handler: (...args: any[]) => any) => {
handlers[name] = handler;
}),
server: { getClientVersion: vi.fn().mockReturnValue({ version: "1.0" }) },
};
addAccessibilityTools(serverMock, mockConfig);
});

it("registers all 5 accessibility tools", () => {
const toolNames = serverMock.tool.mock.calls.map((c: any[]) => c[0]);
expect(toolNames).toContain("accessibilityExpert");
expect(toolNames).toContain("startAccessibilityScan");
expect(toolNames).toContain("createAccessibilityAuthConfig");
expect(toolNames).toContain("getAccessibilityAuthConfig");
expect(toolNames).toContain("fetchAccessibilityIssues");
});

it("accessibilityExpert — returns a response without crashing", async () => {
const result = await handlers["accessibilityExpert"](
{ query: "What is WCAG?" },
{ sendNotification: vi.fn(), _meta: {} },
);
expect(result).toBeDefined();
expect(result.content).toBeDefined();
expect(result.content.length).toBeGreaterThan(0);
});

it("createAccessibilityAuthConfig — returns a response for basic auth", async () => {
const result = await handlers["createAccessibilityAuthConfig"](
{ type: "basic", name: "test-auth", username: "user", password: "pass", url: "https://example.com/login" },
{ sendNotification: vi.fn(), _meta: {} },
);
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});

it("createAccessibilityAuthConfig — FAIL: form auth without required selectors returns error", async () => {
const result = await handlers["createAccessibilityAuthConfig"](
{ type: "form", name: "test-form", username: "user", password: "pass", url: "https://example.com" },
{ sendNotification: vi.fn(), _meta: {} },
);
// Should return an error because form auth requires selectors
expect(result.isError).toBe(true);
});

it("getAccessibilityAuthConfig — returns a response", async () => {
const result = await handlers["getAccessibilityAuthConfig"](
{ configId: 1 },
{ sendNotification: vi.fn(), _meta: {} },
);
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});

it("startAccessibilityScan — returns a response", async () => {
const result = await handlers["startAccessibilityScan"](
{ name: "test-scan", pageURL: "https://example.com" },
{ sendNotification: vi.fn(), _meta: { progressToken: "tok" } },
);
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});

it("fetchAccessibilityIssues — returns a response", async () => {
const result = await handlers["fetchAccessibilityIssues"](
{ scanId: "scan-1", scanRunId: "run-1" },
{ sendNotification: vi.fn(), _meta: {} },
);
expect(result).toBeDefined();
expect(result.content).toBeDefined();
});
});
64 changes: 64 additions & 0 deletions tests/tools/automate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
import { fetchAutomationScreenshotsTool } from "../../src/tools/automate";
import { fetchAutomationScreenshots } from "../../src/tools/automate-utils/fetch-screenshots";
import { SessionType } from "../../src/lib/constants";

vi.mock("../../src/tools/automate-utils/fetch-screenshots", () => ({
fetchAutomationScreenshots: vi.fn(),
}));
vi.mock("../../src/logger", () => ({
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn() },
}));
vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() }));

const mockConfig = {
"browserstack-username": "fake-user",
"browserstack-access-key": "fake-key",
};

describe("fetchAutomationScreenshotsTool", () => {
beforeEach(() => vi.clearAllMocks());

it("SUCCESS: returns screenshots as image content", async () => {
(fetchAutomationScreenshots as Mock).mockResolvedValue([
{ base64: "abc123", url: "https://example.com/1.png" },
{ base64: "def456", url: "https://example.com/2.png" },
]);

const result = await fetchAutomationScreenshotsTool(
{ sessionId: "sess-123", sessionType: SessionType.Automate },
mockConfig,
);

expect(result.isError).toBeFalsy();
expect(result.content.length).toBe(3); // 1 text + 2 images
expect(result.content[0].type).toBe("text");
expect(result.content[1].type).toBe("image");
});

it("SUCCESS: returns isError when no screenshots found", async () => {
(fetchAutomationScreenshots as Mock).mockResolvedValue([]);

const result = await fetchAutomationScreenshotsTool(
{ sessionId: "sess-123", sessionType: SessionType.Automate },
mockConfig,
);

expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("No screenshots found");
});

it("FAIL: returns isError on API failure", async () => {
(fetchAutomationScreenshots as Mock).mockRejectedValue(
new Error("API error"),
);

const result = await fetchAutomationScreenshotsTool(
{ sessionId: "sess-123", sessionType: SessionType.Automate },
mockConfig,
);

expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Error");
});
});
69 changes: 69 additions & 0 deletions tests/tools/bstack-sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import addSDKTools from "../../src/tools/bstack-sdk";
import { runTestsOnBrowserStackHandler } from "../../src/tools/sdk-utils/handler";

vi.mock("../../src/tools/sdk-utils/handler", () => ({
runTestsOnBrowserStackHandler: vi.fn(),
}));
vi.mock("../../src/lib/utils", () => ({
handleMCPError: vi.fn().mockReturnValue({
content: [{ type: "text", text: "Error occurred" }],
isError: true,
}),
}));
vi.mock("../../src/logger", () => ({
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn() },
}));
vi.mock("../../src/lib/instrumentation", () => ({ trackMCP: vi.fn() }));

const mockConfig = {
"browserstack-username": "fake-user",
"browserstack-access-key": "fake-key",
};

describe("BStack SDK Tool", () => {
let serverMock: any;

beforeEach(() => {
vi.clearAllMocks();
serverMock = {
tool: vi.fn((name, desc, schema, handler) => {
serverMock.handlers = serverMock.handlers || {};
serverMock.handlers[name] = handler;
}),
server: { getClientVersion: vi.fn().mockReturnValue({ version: "1.0" }) },
};
addSDKTools(serverMock, mockConfig);
});

it("registers setupBrowserStackAutomateTests tool", () => {
expect(serverMock.tool).toHaveBeenCalledWith(
"setupBrowserStackAutomateTests",
expect.any(String),
expect.any(Object),
expect.any(Function),
);
});

it("SUCCESS: returns SDK setup instructions", async () => {
(runTestsOnBrowserStackHandler as any).mockResolvedValue({
content: [{ type: "text", text: "SDK configured successfully" }],
});

const handler = serverMock.handlers["setupBrowserStackAutomateTests"];
const result = await handler({ language: "java", test_framework: "testng", testing_type: "web" });

expect(result.content[0].text).toContain("SDK configured");
});

it("FAIL: returns error via handleMCPError", async () => {
(runTestsOnBrowserStackHandler as any).mockRejectedValue(
new Error("Invalid framework"),
);

const handler = serverMock.handlers["setupBrowserStackAutomateTests"];
const result = await handler({ language: "bad", test_framework: "bad", testing_type: "web" });

expect(result.isError).toBe(true);
});
});
Loading
Loading