Skip to content

Commit 1f09ebc

Browse files
authored
feat: use optimized snapshots provided by testplane (#15)
* feat: use optimized snapshots provided by testplane * chore: update testplane version
1 parent 6ee43e1 commit 1f09ebc

File tree

9 files changed

+144
-41
lines changed

9 files changed

+144
-41
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
build
33
testplane-mcp-*.tgz
4+
tmp
5+
.vscode

package-lock.json

Lines changed: 4 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@modelcontextprotocol/sdk": "^1.11.2",
3131
"@testing-library/webdriverio": "^3.2.1",
3232
"commander": "^13.1.0",
33-
"testplane": "latest",
33+
"testplane": "^8.29.3",
3434
"zod": "^3.22.4"
3535
},
3636
"devDependencies": {

src/responses/browser-helpers.ts

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,83 @@ export async function getBrowserTabs(browser: WdioBrowser): Promise<BrowserTab[]
3434
}
3535
}
3636

37-
export async function getCurrentTabSnapshot(browser: WdioBrowser): Promise<string | null> {
37+
export interface CaptureSnapshotOptions {
38+
includeTags?: string[];
39+
includeAttrs?: string[];
40+
excludeTags?: string[];
41+
excludeAttrs?: string[];
42+
truncateText?: boolean;
43+
maxTextLength?: number;
44+
}
45+
46+
async function captureSnapshot(
47+
browserOrElement: WebdriverIO.Element | WdioBrowser,
48+
options: CaptureSnapshotOptions = {},
49+
context: { type: "browser" | "element"; fallbackMethod?: string },
50+
): Promise<string | null> {
3851
try {
39-
const pageSource = await browser.getPageSource();
52+
const snapshotResult = await (browserOrElement as WdioBrowser).unstable_captureDomSnapshot(options);
53+
54+
const notes: string[] = [];
4055

41-
if (!pageSource || pageSource.trim().length === 0) {
42-
return null;
56+
const omittedParts: string[] = [];
57+
if (snapshotResult.omittedTags.length > 0) {
58+
omittedParts.push(`tags: ${snapshotResult.omittedTags.join(", ")}`);
59+
}
60+
if (snapshotResult.omittedAttributes.length > 0) {
61+
omittedParts.push(`attributes: ${snapshotResult.omittedAttributes.join(", ")}`);
4362
}
4463

45-
return pageSource;
64+
if (omittedParts.length > 0) {
65+
notes.push(
66+
`# Note: ${omittedParts.join(" and ")} were omitted from this ${context.type} snapshot. If you need them, request the ${context.type} snapshot again and explicitly specify them as needed.`,
67+
);
68+
}
69+
70+
if (snapshotResult.textWasTruncated) {
71+
notes.push(
72+
`# Note: some text contents/attribute values were truncated. If you need full text contents, request a snapshot with truncateText: false.`,
73+
);
74+
}
75+
76+
return "```yaml\n" + notes.join("\n") + "\n" + snapshotResult.snapshot + "\n```";
4677
} catch (error) {
47-
console.error("Error getting tab snapshot:", error);
48-
return null;
78+
console.error(`Error getting ${context.type} snapshot:`, error);
79+
80+
if (context.type === "browser" && browserOrElement.getPageSource) {
81+
const pageSource = await browserOrElement.getPageSource();
82+
return (
83+
"```html\n" +
84+
`<!-- Note: failed to get optimized ${context.type} snapshot, below is raw page source as a fallback. The error was: ${(error as Error)?.stack} -->\n\n` +
85+
pageSource +
86+
"\n```"
87+
);
88+
}
89+
90+
if (context.type === "element" && (browserOrElement as WebdriverIO.Element).getHTML) {
91+
const elementHTML = await (browserOrElement as WebdriverIO.Element).getHTML();
92+
return (
93+
"```html\n" +
94+
`<!-- Note: failed to get ${context.type} snapshot, below is element HTML as a fallback. The error was: ${(error as Error)?.message} -->\n\n` +
95+
elementHTML +
96+
"\n```"
97+
);
98+
}
4999
}
100+
101+
return null;
102+
}
103+
104+
export async function getCurrentTabSnapshot(
105+
browser: WdioBrowser,
106+
options: CaptureSnapshotOptions = {},
107+
): Promise<string | null> {
108+
return captureSnapshot(browser, options, { type: "browser" });
109+
}
110+
111+
export async function getElementSnapshot(
112+
element: WebdriverIO.Element,
113+
options: CaptureSnapshotOptions = {},
114+
): Promise<string | null> {
115+
return captureSnapshot(element, options, { type: "element" });
50116
}

src/responses/index.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import { WdioBrowser } from "testplane";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3-
import { getBrowserTabs, getCurrentTabSnapshot } from "./browser-helpers.js";
3+
import {
4+
CaptureSnapshotOptions,
5+
getBrowserTabs,
6+
getCurrentTabSnapshot,
7+
getElementSnapshot,
8+
} from "./browser-helpers.js";
49

510
export type ToolResponse = CallToolResult;
611

712
export interface BrowserResponseOptions {
8-
action: string;
13+
action?: string;
914
testplaneCode?: string;
1015
additionalInfo?: string;
16+
snapshotOptions?: CaptureSnapshotOptions;
17+
}
18+
19+
export interface ElementResponseOptions {
20+
action?: string;
21+
testplaneCode?: string;
22+
additionalInfo?: string;
23+
snapshotOptions?: CaptureSnapshotOptions;
1124
}
1225

1326
export function createSimpleResponse(message: string, isError = false): ToolResponse {
@@ -28,7 +41,9 @@ export async function createBrowserStateResponse(
2841
): Promise<ToolResponse> {
2942
const sections: string[] = [];
3043

31-
sections.push(`✅ ${options.action}`);
44+
if (options.action) {
45+
sections.push(`✅ ${options.action}`);
46+
}
3247

3348
if (options.testplaneCode) {
3449
sections.push("## Testplane Code");
@@ -46,12 +61,10 @@ export async function createBrowserStateResponse(
4661
});
4762
}
4863

49-
const snapshot = await getCurrentTabSnapshot(browser);
64+
const snapshot = await getCurrentTabSnapshot(browser, options.snapshotOptions);
5065
if (snapshot) {
5166
sections.push("## Current Tab Snapshot");
52-
sections.push("```html");
5367
sections.push(snapshot);
54-
sections.push("```");
5568
}
5669

5770
if (options.additionalInfo) {
@@ -75,3 +88,34 @@ export function createErrorResponse(message: string, error?: Error): ToolRespons
7588
isError: true,
7689
};
7790
}
91+
92+
export async function createElementStateResponse(
93+
element: WebdriverIO.Element,
94+
options: ElementResponseOptions,
95+
): Promise<ToolResponse> {
96+
const sections: string[] = [];
97+
98+
if (options.action) {
99+
sections.push(`✅ ${options.action}`);
100+
}
101+
102+
if (options.testplaneCode) {
103+
sections.push("## Testplane Code");
104+
sections.push("```javascript");
105+
sections.push(options.testplaneCode);
106+
sections.push("```");
107+
}
108+
109+
const elementSnapshot = await getElementSnapshot(element, options.snapshotOptions);
110+
if (elementSnapshot) {
111+
sections.push("## Element Snapshot");
112+
sections.push(elementSnapshot);
113+
}
114+
115+
if (options.additionalInfo) {
116+
sections.push("## Additional Information");
117+
sections.push(options.additionalInfo);
118+
}
119+
120+
return createSimpleResponse(sections.join("\n\n"));
121+
}

src/tools/click-on-element.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { ToolDefinition } from "../types.js";
33
import { contextProvider } from "../context-provider.js";
4-
import { createBrowserStateResponse, createErrorResponse } from "../responses/index.js";
4+
import { createElementStateResponse, createErrorResponse } from "../responses/index.js";
55
import { elementSelectorSchema, findElement } from "./utils/element-selector.js";
66

77
export const elementClickSchema = elementSelectorSchema;
@@ -17,21 +17,21 @@ const clickOnElementCb: ToolCallback<typeof elementClickSchema> = async args =>
1717

1818
console.error(`Successfully clicked element with ${queryDescription}`);
1919

20-
return await createBrowserStateResponse(browser, {
20+
return await createElementStateResponse(element, {
2121
action: `Successfully clicked element found by ${queryDescription}`,
2222
testplaneCode,
2323
additionalInfo: `Element selection strategy: ${args.queryType ? `Semantic query (${args.queryType})` : "CSS selector (fallback)"}`,
2424
});
2525
} catch (error) {
2626
console.error("Error clicking element:", error);
27-
let errorMessage = "Error clicking element";
2827

2928
if (error instanceof Error && error.message.includes("Unable to find")) {
30-
errorMessage =
31-
"Element not found. Try using a different query strategy or check if the element exists on the page.";
29+
return createErrorResponse(
30+
"Element not found. Try using a different query strategy or check if the element exists on the page.",
31+
);
3232
}
3333

34-
return createErrorResponse(errorMessage, error instanceof Error ? error : undefined);
34+
return createErrorResponse("Error clicking element", error instanceof Error ? error : undefined);
3535
}
3636
};
3737

src/tools/type-into-element.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { z } from "zod";
22
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { ToolDefinition } from "../types.js";
44
import { contextProvider } from "../context-provider.js";
5-
import { createBrowserStateResponse, createErrorResponse } from "../responses/index.js";
5+
import { createElementStateResponse, createErrorResponse } from "../responses/index.js";
66
import { elementSelectorSchema, findElement } from "./utils/element-selector.js";
77

88
export const typeIntoElementSchema = {
@@ -27,21 +27,21 @@ const typeIntoElementCb: ToolCallback<typeof typeIntoElementSchema> = async args
2727

2828
console.error(`Successfully typed "${text}" into element with ${queryDescription}`);
2929

30-
return await createBrowserStateResponse(browser, {
30+
return await createElementStateResponse(element, {
3131
action: `Successfully typed "${text}" into element found by ${queryDescription}`,
3232
testplaneCode,
3333
additionalInfo: `Element selection strategy: ${selectorArgs.queryType ? `Semantic query (${selectorArgs.queryType})` : "CSS selector (fallback)"}`,
3434
});
3535
} catch (error) {
3636
console.error("Error typing into element:", error);
37-
let errorMessage = "Error typing into element";
3837

3938
if (error instanceof Error && error.message.includes("Unable to find")) {
40-
errorMessage =
41-
"Element not found. Try using a different query strategy or check if the element exists on the page.";
39+
return createErrorResponse(
40+
"Element not found. Try using a different query strategy or check if the element exists on the page.",
41+
);
4242
}
4343

44-
return createErrorResponse(errorMessage, error instanceof Error ? error : undefined);
44+
return createErrorResponse("Error typing into element", error instanceof Error ? error : undefined);
4545
}
4646
};
4747

test/responses/index.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ describe("responses/index", () => {
133133
const responseText = result.content[0].text;
134134

135135
expect(responseText).toContain("## Current Tab Snapshot");
136-
expect(responseText).toContain("```html");
137136
expect(responseText).toContain("<html><body>Test content</body></html>");
138-
expect(responseText).toContain("```");
139137
});
140138

141139
it("should include additional information when provided", async () => {

test/tools/click-on-element.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe(
5757
expect(result.isError).toBe(false);
5858
const content = result.content as Array<{ type: string; text: string }>;
5959
expect(content[0].text).toContain("Successfully clicked element");
60-
expect(content[0].text).toContain("clicked-indicator show");
60+
expect(content[0].text).toContain("span.clicked-indicator.show");
6161
});
6262

6363
it("should click an element using CSS selector", async () => {
@@ -71,7 +71,7 @@ describe(
7171
expect(result.isError).toBe(false);
7272
const content = result.content as Array<{ type: string; text: string }>;
7373
expect(content[0].text).toContain("Successfully clicked element");
74-
expect(content[0].text).toContain("clicked-indicator show");
74+
expect(content[0].text).toContain("span.clicked-indicator.show");
7575
});
7676

7777
it("should return correct testplane code for clicked element", async () => {

0 commit comments

Comments
 (0)