Skip to content

Commit dede051

Browse files
authored
feat: implement take page snapshot command (#16)
1 parent 1f09ebc commit dede051

File tree

4 files changed

+156
-1
lines changed

4 files changed

+156
-1
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,21 @@ Type text into an input element on the page using semantic queries (`testing-lib
225225
**Note:** Provide either semantic query parameters OR selector, not both.
226226

227227
</details>
228+
229+
<details>
230+
<summary>Page Inspection</summary>
231+
232+
### `takePageSnapshot`
233+
Capture a DOM snapshot of the current page with configurable filtering options.
234+
235+
- **Parameters:**
236+
- `includeTags` (array of strings, optional): HTML tags to include in the snapshot besides defaults
237+
- `includeAttrs` (array of strings, optional): HTML attributes to include in the snapshot besides defaults
238+
- `excludeTags` (array of strings, optional): HTML tags to exclude from the snapshot
239+
- `excludeAttrs` (array of strings, optional): HTML attributes to exclude from the snapshot
240+
- `truncateText` (boolean, optional): Whether to truncate long text content (default: true)
241+
- `maxTextLength` (number, optional): Maximum length of text content before truncation
242+
243+
**Note:** By default, only useful tags and attributes are included in snapshots. The response will indicate what was omitted. Use the filtering options only if you need specific content that's not included by default.
244+
245+
</details>

src/tools/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,12 @@ import { navigate } from "./navigate.js";
33
import { closeBrowser } from "./close-browser.js";
44
import { clickOnElement } from "./click-on-element.js";
55
import { typeIntoElement } from "./type-into-element.js";
6+
import { takePageSnapshot } from "./take-page-snapshot.js";
67

7-
export const tools = [navigate, closeBrowser, clickOnElement, typeIntoElement] as const satisfies ToolDefinition<any>[]; // eslint-disable-line @typescript-eslint/no-explicit-any
8+
export const tools = [
9+
navigate,
10+
closeBrowser,
11+
clickOnElement,
12+
typeIntoElement,
13+
takePageSnapshot,
14+
] as const satisfies ToolDefinition<any>[]; // eslint-disable-line @typescript-eslint/no-explicit-any

src/tools/take-page-snapshot.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { z } from "zod";
2+
import { ToolDefinition } from "../types.js";
3+
import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
4+
import { contextProvider } from "../context-provider.js";
5+
import { createBrowserStateResponse, createErrorResponse } from "../responses/index.js";
6+
7+
export const takePageSnapshotSchema = {
8+
includeTags: z.array(z.string()).optional().describe("HTML tags to include in the snapshot besides defaults"),
9+
includeAttrs: z
10+
.array(z.string())
11+
.optional()
12+
.describe("HTML attributes to include in the snapshot besides defaults"),
13+
excludeTags: z.array(z.string()).optional().describe("HTML tags to exclude from the snapshot"),
14+
excludeAttrs: z.array(z.string()).optional().describe("HTML attributes to exclude from the snapshot"),
15+
truncateText: z.boolean().optional().describe("Whether to truncate long text content (default: true)"),
16+
maxTextLength: z.number().positive().optional().describe("Maximum length of text content before truncation"),
17+
};
18+
19+
const takePageSnapshotCb: ToolCallback<typeof takePageSnapshotSchema> = async args => {
20+
try {
21+
const context = contextProvider.getContext();
22+
const browser = await context.browser.get();
23+
24+
const snapshotOptions = {
25+
includeTags: args.includeTags,
26+
includeAttrs: args.includeAttrs,
27+
excludeTags: args.excludeTags,
28+
excludeAttrs: args.excludeAttrs,
29+
truncateText: args.truncateText,
30+
maxTextLength: args.maxTextLength,
31+
};
32+
33+
const testplaneCode = `const snapshot = await browser.unstable_captureDomSnapshot(${JSON.stringify(snapshotOptions, null, 2)});`;
34+
35+
return await createBrowserStateResponse(browser, {
36+
action: "Page snapshot captured successfully",
37+
testplaneCode,
38+
snapshotOptions,
39+
});
40+
} catch (error) {
41+
console.error("Error taking page snapshot:", error);
42+
return createErrorResponse("Error taking page snapshot", error instanceof Error ? error : undefined);
43+
}
44+
};
45+
46+
export const takePageSnapshot: ToolDefinition<typeof takePageSnapshotSchema> = {
47+
name: "takePageSnapshot",
48+
description:
49+
"Capture a DOM snapshot of the current page. Note: by default, only useful tags and attributes are included. Prefer to use defaults. Response contains info as to what was omitted. If you need more info, request a snapshot with more tags and attributes.",
50+
schema: takePageSnapshotSchema,
51+
cb: takePageSnapshotCb,
52+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
7+
describe(
8+
"tools/takePageSnapshot",
9+
() => {
10+
let client: Client;
11+
let playgroundUrl: string;
12+
let testServer: PlaygroundServer;
13+
14+
beforeAll(async () => {
15+
testServer = new PlaygroundServer();
16+
playgroundUrl = await testServer.start();
17+
}, 20000);
18+
19+
afterAll(async () => {
20+
if (testServer) {
21+
await testServer.stop();
22+
}
23+
});
24+
25+
beforeEach(async () => {
26+
client = await startClient();
27+
});
28+
29+
afterEach(async () => {
30+
if (client) {
31+
await client.close();
32+
}
33+
});
34+
35+
describe("takePageSnapshot tool availability", () => {
36+
it("should list takePageSnapshot tool in available tools", async () => {
37+
const tools = await client.listTools();
38+
39+
const snapshotTool = tools.tools.find(tool => tool.name === "takePageSnapshot");
40+
41+
expect(snapshotTool).toBeDefined();
42+
});
43+
});
44+
45+
describe("takePageSnapshot tool execution", () => {
46+
it("should capture snapshot of playground page with expected content", async () => {
47+
await client.callTool({
48+
name: "navigate",
49+
arguments: { url: playgroundUrl },
50+
});
51+
52+
const result = await client.callTool({
53+
name: "takePageSnapshot",
54+
arguments: {},
55+
});
56+
57+
expect(result.isError).toBe(false);
58+
expect(result.content).toBeDefined();
59+
60+
const content = result.content as Array<{ type: string; text: string }>;
61+
expect(content).toHaveLength(1);
62+
expect(content[0].type).toBe("text");
63+
64+
const responseText = content[0].text;
65+
66+
expect(responseText).toContain("Element Click Test Playground");
67+
expect(responseText).toContain("Role-based Elements");
68+
expect(responseText).toContain("Submit Form");
69+
expect(responseText).toContain("Text-based Elements");
70+
expect(responseText).toContain("Form Elements");
71+
expect(responseText).toContain("Email Address");
72+
expect(responseText).toContain("Placeholder Elements");
73+
expect(responseText).toContain("Enter your name");
74+
});
75+
});
76+
},
77+
INTEGRATION_TEST_TIMEOUT,
78+
);

0 commit comments

Comments
 (0)