Skip to content

Commit 06790c8

Browse files
sonic16xrocketraccoon
andauthored
feat(mcp-tools): add hover element tool
* feat(mcp-tools): add hover element tool * feat(mcp-tools): add hover element tool # add tests --------- Co-authored-by: rocketraccoon <[email protected]>
1 parent 9399659 commit 06790c8

File tree

4 files changed

+173
-0
lines changed

4 files changed

+173
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ build
33
testplane-mcp-*.tgz
44
tmp
55
.vscode
6+
.idea

src/tools/hover-element.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { ToolDefinition } from "../types.js";
3+
import { contextProvider } from "../context-provider.js";
4+
import { createElementStateResponse, createErrorResponse } from "../responses/index.js";
5+
import { elementSelectorSchema, findElement } from "./utils/element-selector.js";
6+
7+
export const elementHoverSchema = elementSelectorSchema;
8+
9+
const hoverElementCb: ToolCallback<typeof elementHoverSchema> = async args => {
10+
try {
11+
const context = contextProvider.getContext();
12+
const browser = await context.browser.get();
13+
14+
const { element, queryDescription, testplaneCode } = await findElement(browser, args.locator);
15+
16+
await element.moveTo();
17+
18+
console.error(`Successfully hovered element with ${queryDescription}`);
19+
20+
return await createElementStateResponse(element, {
21+
action: `Successfully hovered element found by ${queryDescription}`,
22+
testplaneCode: testplaneCode.startsWith("await")
23+
? `await (${testplaneCode}).moveTo();`
24+
: `await ${testplaneCode}.moveTo();`,
25+
});
26+
} catch (error) {
27+
console.error("Error hover element:", error);
28+
29+
if (error instanceof Error && error.message.includes("Unable to find")) {
30+
return createErrorResponse(
31+
"Element not found. Try using a different query strategy or check if the element exists on the page.",
32+
);
33+
}
34+
35+
return createErrorResponse("Error hover element", error instanceof Error ? error : undefined);
36+
}
37+
};
38+
39+
export const hoverElement: ToolDefinition<typeof elementHoverSchema> = {
40+
name: "hoverElement",
41+
description: "Hover an element on the page.",
42+
schema: elementHoverSchema,
43+
cb: hoverElementCb,
44+
};

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ToolDefinition } from "../types.js";
22
import { navigate } from "./navigate.js";
33
import { closeBrowser } from "./close-browser.js";
44
import { clickOnElement } from "./click-on-element.js";
5+
import { hoverElement } from "./hover-element.js";
56
import { typeIntoElement } from "./type-into-element.js";
67
import { takePageSnapshot } from "./take-page-snapshot.js";
78
import { listTabs } from "./list-tabs.js";
@@ -14,6 +15,7 @@ export const tools = [
1415
navigate,
1516
closeBrowser,
1617
clickOnElement,
18+
hoverElement,
1719
typeIntoElement,
1820
takePageSnapshot,
1921
listTabs,

test/tools/hover-element.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest";
3+
import { startClient } from "../utils";
4+
import { INTEGRATION_TEST_TIMEOUT } from "../constants";
5+
import { PlaygroundServer } from "../test-server";
6+
import { LocatorStrategy } from "../../src/tools/utils/element-selector";
7+
8+
describe(
9+
"tools/hoverElement",
10+
() => {
11+
let client: Client;
12+
let playgroundUrl: string;
13+
let testServer: PlaygroundServer;
14+
15+
beforeAll(async () => {
16+
testServer = new PlaygroundServer();
17+
playgroundUrl = await testServer.start();
18+
}, 20000);
19+
20+
afterAll(async () => {
21+
if (testServer) {
22+
await testServer.stop();
23+
}
24+
});
25+
26+
beforeEach(async () => {
27+
client = await startClient();
28+
await client.callTool({ name: "navigate", arguments: { url: playgroundUrl } });
29+
});
30+
31+
afterEach(async () => {
32+
if (client) {
33+
await client.close();
34+
}
35+
});
36+
37+
describe("tool availability", () => {
38+
it("should list hoverElement tool in available tools", async () => {
39+
const tools = await client.listTools();
40+
41+
const elementHoverTool = tools.tools.find(tool => tool.name === "hoverElement");
42+
43+
expect(elementHoverTool).toBeDefined();
44+
});
45+
});
46+
47+
describe("hover functionality", () => {
48+
it("should hover an element using semantic query", async () => {
49+
const result = await client.callTool({
50+
name: "hoverElement",
51+
arguments: {
52+
locator: {
53+
strategy: LocatorStrategy.TestingLibrary,
54+
queryType: "role",
55+
queryValue: "button",
56+
queryOptions: { name: "Submit Form" },
57+
},
58+
},
59+
});
60+
61+
expect(result.isError).toBe(false);
62+
const content = result.content as Array<{ type: string; text: string }>;
63+
expect(content[0].text).toContain("Successfully hovered element");
64+
expect(content[0].text).toContain("button#submit-btn[@hover]");
65+
});
66+
67+
it("should hover an element using CSS selector", async () => {
68+
const result = await client.callTool({
69+
name: "hoverElement",
70+
arguments: {
71+
locator: {
72+
strategy: LocatorStrategy.Wdio,
73+
selector: "#unique-element",
74+
},
75+
},
76+
});
77+
78+
expect(result.isError).toBe(false);
79+
const content = result.content as Array<{ type: string; text: string }>;
80+
expect(content[0].text).toContain("Successfully hovered element");
81+
expect(content[0].text).toContain("div#unique-element[@hover]");
82+
});
83+
84+
it("should return correct testplane code for hovered element", async () => {
85+
const result = await client.callTool({
86+
name: "hoverElement",
87+
arguments: {
88+
locator: {
89+
strategy: LocatorStrategy.TestingLibrary,
90+
queryType: "role",
91+
queryValue: "button",
92+
queryOptions: { name: "Submit Form" },
93+
},
94+
},
95+
});
96+
97+
expect(result.isError).toBe(false);
98+
const content = result.content as Array<{ type: string; text: string }>;
99+
expect(content[0].text).toContain(
100+
'await (await browser.findByRole("button", {"name":"Submit Form"})).moveTo()',
101+
);
102+
});
103+
});
104+
105+
describe("error handling specific to hover", () => {
106+
it("should provide helpful error messages for hover failures", async () => {
107+
const result = await client.callTool({
108+
name: "hoverElement",
109+
arguments: {
110+
locator: {
111+
strategy: LocatorStrategy.TestingLibrary,
112+
queryType: "role",
113+
queryValue: "button",
114+
queryOptions: { name: "Non-existent Button" },
115+
},
116+
},
117+
});
118+
119+
expect(result.isError).toBe(true);
120+
const content = result.content as Array<{ type: string; text: string }>;
121+
expect(content[0].text).toContain("Element not found");
122+
});
123+
});
124+
},
125+
INTEGRATION_TEST_TIMEOUT,
126+
);

0 commit comments

Comments
 (0)