Skip to content

Commit 8c01dc8

Browse files
committed
playwright ref locators attempt
1 parent f957409 commit 8c01dc8

File tree

8 files changed

+1231
-1514
lines changed

8 files changed

+1231
-1514
lines changed

browserbase/package-lock.json

+59-428
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browserbase/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
"dependencies": {
2323
"@browserbasehq/sdk": "^2.0.0",
2424
"@modelcontextprotocol/sdk": "^1.10.2",
25-
"playwright-core": "^1.45.3",
25+
"@types/yaml": "^1.9.6",
26+
"playwright": "^1.53.0-alpha-2025-05-05",
2627
"puppeteer-core": "^23.9.0",
28+
"yaml": "^2.7.1",
2729
"zod": "^3.24.3"
2830
},
2931
"devDependencies": {

browserbase/src/context.ts

+437-332
Large diffs are not rendered by default.

browserbase/src/pageSnapshot.ts

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Use types from playwright-core consistent with the project
18+
import type { Page, FrameLocator, Locator } from 'playwright-core';
19+
import yaml from 'yaml';
20+
import { Writable } from 'stream'; // Import Writable for process.stderr
21+
22+
type PageOrFrameLocator = Page | FrameLocator;
23+
24+
export class PageSnapshot {
25+
private _frameLocators: PageOrFrameLocator[] = [];
26+
private _text!: string;
27+
28+
constructor() {
29+
}
30+
31+
static async create(page: Page): Promise<PageSnapshot> {
32+
const snapshot = new PageSnapshot();
33+
await snapshot._build(page);
34+
return snapshot;
35+
}
36+
37+
text(): string {
38+
return this._text;
39+
}
40+
41+
private async _build(page: Page) {
42+
const yamlDocument = await this._snapshotFrame(page);
43+
this._text = [
44+
`- Page Snapshot`,
45+
'```yaml',
46+
yamlDocument.toString({ indentSeq: false }).trim(),
47+
'```',
48+
].join('\n');
49+
}
50+
51+
private async _snapshotFrame(frame: PageOrFrameLocator) {
52+
const frameIndex = this._frameLocators.push(frame) - 1;
53+
const logPrefix = `[PageSnapshot Frame ${frameIndex}] ${new Date().toISOString()}:`; // Added for logging
54+
let snapshotString = '';
55+
try {
56+
// process.stderr.write(`${logPrefix} Attempting frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true })\\n`);
57+
snapshotString = await (frame.locator('body') as any).ariaSnapshot({ ref: true, emitGeneric: true });
58+
// process.stderr.write(`${logPrefix} Raw ariaSnapshot output:\\nSTART>>>\\n${snapshotString}\\n<<<END\\n`);
59+
} catch (e) {
60+
// process.stderr.write(`${logPrefix} ERROR during ariaSnapshot call: ${e}\\n`);
61+
snapshotString = `error: Could not take snapshot. Error: ${e instanceof Error ? e.message : String(e)}`;
62+
}
63+
64+
const snapshot = yaml.parseDocument(snapshotString);
65+
66+
const visit = async (node: any): Promise<unknown> => {
67+
if (yaml.isPair(node)) {
68+
await Promise.all([
69+
visit(node.key).then(k => node.key = k),
70+
visit(node.value).then(v => node.value = v)
71+
]);
72+
} else if (yaml.isSeq(node) || yaml.isMap(node)) {
73+
node.items = await Promise.all(node.items.map(visit));
74+
} else if (yaml.isScalar(node)) {
75+
if (typeof node.value === 'string') {
76+
const value = node.value;
77+
if (frameIndex > 0) {
78+
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
79+
}
80+
if (value.startsWith('iframe ')) {
81+
const refMatch = value.match(/\[ref=(.*)\]/)?.[1]; // Use different var name
82+
if (refMatch) {
83+
try {
84+
// Use the original example's frameLocator logic, adjusted for potential name collision
85+
const childFrameLocator = frame.frameLocator(`[aria-ref="${refMatch}"]`); // Assuming ref is simple ID
86+
const childSnapshot = await this._snapshotFrame(childFrameLocator);
87+
return snapshot.createPair(node.value, childSnapshot);
88+
} catch (error) {
89+
// process.stderr.write(`${logPrefix} ERROR snapshotting iframe ref ${refMatch}: ${error}\\n`);
90+
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
91+
}
92+
}
93+
}
94+
}
95+
}
96+
return node;
97+
};
98+
99+
if (snapshot.contents) {
100+
await visit(snapshot.contents);
101+
} else {
102+
// process.stderr.write(`${logPrefix} WARN - Snapshot resulted in empty contents.\\n`);
103+
const emptyMapDoc = yaml.parseDocument('{}');
104+
snapshot.contents = emptyMapDoc.contents;
105+
}
106+
return snapshot;
107+
}
108+
109+
110+
refLocator(ref: string): Locator {
111+
let frameIndex = 0;
112+
let frame = this._frameLocators[0];
113+
const match = ref.match(/^f(\d+)(.*)/);
114+
if (match) {
115+
frameIndex = parseInt(match[1], 10);
116+
// Add boundary check
117+
if (frameIndex < 0 || frameIndex >= this._frameLocators.length) {
118+
throw new Error(`Validation Error: Frame index ${frameIndex} derived from ref '${ref}' is out of bounds (found ${this._frameLocators.length} frames).`);
119+
}
120+
frame = this._frameLocators[frameIndex];
121+
ref = match[2]; // Use the ref part *after* the frame index
122+
}
123+
124+
if (!frame)
125+
throw new Error(`Frame (index ${frameIndex}) does not exist. Provide ref from the most current snapshot.`);
126+
127+
// Use the final ref part (potentially stripped of frame prefix) for the locator
128+
return frame.locator(`[aria-ref="${ref}"]`);
129+
}
130+
}

browserbase/src/server.ts

+27-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Context } from "./context.js";
1515
import type { Config } from "./config.js";
1616
import { zodToJsonSchema } from "zod-to-json-schema";
1717
import { z } from 'zod'; // Import z
18+
import { Writable } from 'stream'; // Import Writable for process.stderr
1819

1920
// Remove direct tool imports
2021
// import { navigateTool } from "./tools/navigate.js";
@@ -94,29 +95,45 @@ export function createServer(serverOptions: BrowserbaseServerOptions, config: Co
9495
});
9596

9697
server.setRequestHandler(CallToolRequestSchema, async (request) => {
97-
const errorResult = (...messages: string[]) => ({
98-
content: [{ type: 'text', text: messages.join('\n') }],
99-
isError: true,
100-
});
98+
const logError = (message: string) => {
99+
// Ensure error logs definitely go to stderr
100+
process.stderr.write(`[server.ts Error] ${new Date().toISOString()} ${message}\\n`);
101+
};
102+
103+
const errorResult = (...messages: string[]) => {
104+
const result = {
105+
content: [{ type: 'text', text: messages.join('\\n') }],
106+
isError: true,
107+
};
108+
logError(`Returning error: ${JSON.stringify(result)}`); // Log the error structure
109+
return result;
110+
};
101111

102112
// Use the map built from the passed-in tools
103113
const tool = availableTools.get(request.params.name);
104-
114+
105115
if (!tool) {
106-
console.error(`Tool "${request.params.name}" not found.`);
116+
// Use the explicit error logger
117+
logError(`Tool "${request.params.name}" not found.`);
107118
// Check if it was a placeholder tool that wasn't implemented
108-
// This requires access to the original placeholder definitions,
119+
// This requires access to the original placeholder definitions,
109120
// maybe pass placeholder names/schemas separately or handle in Context?
110121
// For now, just return not found.
111122
return errorResult(`Tool "${request.params.name}" not found`);
112123
}
113124

114125
try {
115126
// Delegate execution to the context
116-
return await context.run(tool, request.params.arguments ?? {});
127+
const result = await context.run(tool, request.params.arguments ?? {});
128+
// Log the successful result structure just before returning
129+
process.stderr.write(`[server.ts Success] ${new Date().toISOString()} Returning result for ${request.params.name}: ${JSON.stringify(result)}\\n`);
130+
return result;
117131
} catch (error) {
118-
console.error(`Error running tool via context: ${error}`);
119-
return errorResult(`Failed to run tool '${request.params.name}': ${error instanceof Error ? error.message : String(error)}`);
132+
// Use the explicit error logger
133+
const errorMessage = error instanceof Error ? error.message : String(error);
134+
logError(`Error running tool ${request.params.name} via context: ${errorMessage}`);
135+
logError(`Original error stack (if available): ${error instanceof Error ? error.stack : 'N/A'}`); // Log stack trace
136+
return errorResult(`Failed to run tool '${request.params.name}': ${errorMessage}`);
120137
}
121138
});
122139

0 commit comments

Comments
 (0)