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
94 changes: 91 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"license": "ISC",
"dependencies": {
"@testplane/testing-library": "^1.0.5",
"@rrweb/replay": "^2.0.0-alpha.18",
"commander": "^14.0.3",
"debug": "^4.3.4",
"html-reporter": "^11.10.0-rc.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/daemon.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("daemon e2e", () => {
expect(r.code).toBe(0);
expect(r.stdout).toContain(`Successfully navigated to ${playgroundUrl}`);

const snapshotPathMatch = r.stdout.match(/Saved to: (\S+\.(?:yml|html))/);
const snapshotPathMatch = r.stdout.match(/The snapshot was saved to: (\S+\.(?:yml|html))/);
expect(snapshotPathMatch, "navigate response should reference a saved snapshot file").not.toBeNull();
const snapshotContent = fs.readFileSync(snapshotPathMatch![1], "utf8");
expect(snapshotContent).toContain("e2e-ok");
Expand All @@ -141,7 +141,7 @@ describe("daemon e2e", () => {
expect(r.code).toBe(0);
expect(r.stdout).toContain('Successfully selected option by value "jp"');

const snapshotPathMatch = r.stdout.match(/Saved to: (\S+\.(?:yml|html))/);
const snapshotPathMatch = r.stdout.match(/The snapshot was saved to: (\S+\.(?:yml|html))/);
expect(snapshotPathMatch, "select response should reference a saved snapshot file").not.toBeNull();
const snapshotContent = fs.readFileSync(snapshotPathMatch![1], "utf8");
expect(snapshotContent).toContain("selected:jp");
Expand Down
1 change: 1 addition & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.2",
"@testplane/testing-library": "^1.0.5",
"@rrweb/replay": "^2.0.0-alpha.18",
"commander": "^13.1.0",
"html-reporter": "^11.10.0-rc.2",
"testplane": "^8.44.1-rc.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp/test/server.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const EXPECTED_TOOL_NAMES = [
// report tools
"test-results",
"inspect-result",
"time-travel-snapshot",
// session tools
"launch",
"attach",
Expand Down Expand Up @@ -61,7 +62,7 @@ describe(
const text = content.map(c => c.text).join("\n");
expect(text).toContain(`Successfully navigated to ${playgroundUrl}`);

const snapshotPathMatch = text.match(/Saved to: (\S+\.(?:yml|html))/);
const snapshotPathMatch = text.match(/(?:Saved to:|The snapshot was saved to:) (\S+\.(?:yml|html))/);
expect(snapshotPathMatch, "navigate response should reference a saved snapshot file").not.toBeNull();
const snapshotContent = fs.readFileSync(snapshotPathMatch![1], "utf8");
expect(snapshotContent).toContain("server-wiring-ok");
Expand Down
3 changes: 3 additions & 0 deletions packages/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"license": "ISC",
"dependencies": {
"@testplane/testing-library": "^1.0.5",
"@rrweb/replay": "^2.0.0-alpha.18",
"@rrweb/types": "^2.0.0-alpha.18",
"fflate": "^0.8.2",
"html-reporter": "^11.10.0-rc.2",
"lodash.escaperegexp": "^4.1.2",
"testplane": "^8.44.1-rc.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { attachToBrowser } from "./tools/attach-to-browser.js";
import { closeBrowser } from "./tools/close-browser.js";
import { inspectResult } from "./tools/inspect-result/index.js";
import { testResults } from "./tools/test-results/index.js";
import { timeTravelSnapshot } from "./tools/time-travel-snapshot/index.js";

// This function just ensures that every item on a type level is a Tool<something>. AFAIK the only way to do this.
const typeCheckedTools = <const T extends readonly unknown[]>(
Expand All @@ -42,6 +43,7 @@ export const tools = typeCheckedTools([
closeBrowser,
testResults,
inspectResult,
timeTravelSnapshot,
]);

export { launchBrowserWithOptions, ToolKind };
Expand Down
39 changes: 29 additions & 10 deletions packages/tools/src/responses/browser-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@ function formatNotesAsComments(notes: string[], fenceLanguage: "yaml" | "html"):

export interface PageSnapshotResult {
content: string;
fenceLanguage: "yaml" | "html";
fenceLanguage: "yaml" | "html" | "diff";
}

export const INLINE_SNAPSHOT_MAX_LENGTH = 32_000;

export function isPageSnapshotTooLargeForInline(snapshot: PageSnapshotResult): boolean {
return snapshot.content.length > INLINE_SNAPSHOT_MAX_LENGTH;
}

export async function getPageSnapshot(
Expand All @@ -152,21 +158,34 @@ export interface SavedPageSnapshot {
filePath: string;
}

export async function savePageSnapshotToFile(
browser: WdioBrowser,
options: CaptureSnapshotOptions = {},
): Promise<SavedPageSnapshot | null> {
const result = await getPageSnapshot(browser, options);
if (!result) return null;

async function savePageSnapshotToFile(snapshot: PageSnapshotResult): Promise<SavedPageSnapshot> {
const dir = path.join(os.tmpdir(), ".testplane", "snapshots");
await fs.mkdir(dir, { recursive: true });

const timestamp = formatTimestamp();
const extension = result.fenceLanguage === "html" ? "html" : "yml";
const extension = snapshot.fenceLanguage === "html" ? "html" : snapshot.fenceLanguage === "diff" ? "diff" : "yml";
const filePath = path.join(dir, `${timestamp}-${randomUUID()}.${extension}`);

await fs.writeFile(filePath, result.content, "utf8");
await fs.writeFile(filePath, snapshot.content, "utf8");

return { filePath };
}

/** Format a snapshot as a response string, optionally saving it to a file if it is too large for inline display. */
export async function convertSnapshotToResponse(
snapshot: PageSnapshotResult,
{ forceSaveToFile = false }: { forceSaveToFile?: boolean } = {},
): Promise<string> {
if (forceSaveToFile) {
const saved = await savePageSnapshotToFile(snapshot);
return `The snapshot was saved to: ${saved.filePath}`;
}

if (!isPageSnapshotTooLargeForInline(snapshot)) {
return "```" + snapshot.fenceLanguage + "\n" + snapshot.content + "\n```";
}

const saved = await savePageSnapshotToFile(snapshot);

return `The snapshot is too large to include inline (${snapshot.content.length} characters; limit ${INLINE_SNAPSHOT_MAX_LENGTH}), so it was saved to: ${saved.filePath}`;
}
28 changes: 13 additions & 15 deletions packages/tools/src/responses/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { WdioBrowser } from "testplane";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { CaptureSnapshotOptions, getPageSnapshot, getBrowserTabs, savePageSnapshotToFile } from "./browser-helpers.js";
import {
CaptureSnapshotOptions,
getPageSnapshot,
getBrowserTabs,
convertSnapshotToResponse,
} from "./browser-helpers.js";

export type ToolResponse = CallToolResult;

Expand Down Expand Up @@ -55,21 +60,14 @@ export async function createBrowserStateResponse(
}

if (options.isSnapshotNeeded !== false) {
if (options.inlineSnapshot) {
const snapshot = await getPageSnapshot(browser, options.snapshotOptions);
if (snapshot) {
sections.push("## Current Tab Snapshot");
sections.push("```" + snapshot.fenceLanguage + "\n" + snapshot.content + "\n```");
}
const snapshot = await getPageSnapshot(browser, options.snapshotOptions);
if (!snapshot) {
sections.push("## Current Tab Snapshot");
sections.push("No snapshot captured");
} else {
const saved = await savePageSnapshotToFile(browser, options.snapshotOptions);
if (saved) {
sections.push("## Current Tab Snapshot");
sections.push(
`Saved to: ${saved.filePath}\n` +
`Note: some tags/attributes may be omitted and text may be truncated — see the comments at the top of the file for details.`,
);
}
const response = await convertSnapshotToResponse(snapshot, { forceSaveToFile: !options.inlineSnapshot });
sections.push("## Current Tab Snapshot");
sections.push(response);
}
}

Expand Down
Loading
Loading