Skip to content

Commit 77440a6

Browse files
updated the snapshot
1 parent 270046e commit 77440a6

File tree

3 files changed

+197
-34
lines changed

3 files changed

+197
-34
lines changed

package-lock.json

Lines changed: 15 additions & 2 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@playwright/test": "1.52.0-alpha-1743011787000",
5656
"dotenv": "^16.4.7",
5757
"playwright": "1.52.0-alpha-1743011787000",
58+
"yaml": "^2.7.1",
5859
"zod": "^3.24.2"
5960
},
6061
"devDependencies": {

src/tools/utils.ts

Lines changed: 181 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
import type * as playwright from 'playwright';
1818
import type { ToolResult } from './tool';
1919
import type { Context } from '../browser/context';
20+
import yaml from 'yaml';
21+
22+
type PageOrFrameLocator = playwright.Page | playwright.FrameLocator;
2023

2124
async function waitForCompletion<R>(
2225
page: playwright.Page,
2326
callback: () => Promise<R>,
2427
): Promise<R> {
2528
const requests = new Set<playwright.Request>();
2629
let frameNavigated = false;
27-
let waitCallback: () => void = () => {};
30+
let waitCallback: () => void = () => { };
2831
const waitBarrier = new Promise<void>((f) => {
2932
waitCallback = f;
3033
});
@@ -63,59 +66,205 @@ async function waitForCompletion<R>(
6366
clearTimeout(timeout);
6467
};
6568

66-
try {
69+
try
70+
{
6771
const result = await callback();
6872
if (!requests.size && !frameNavigated) waitCallback();
6973
await waitBarrier;
7074
await page.evaluate(() => new Promise((f) => setTimeout(f, 1000)));
7175
return result;
72-
} finally {
76+
} finally
77+
{
7378
dispose();
7479
}
7580
}
7681

82+
export async function run(
83+
context: Context,
84+
options: {
85+
callback: (page: playwright.Page) => Promise<any>;
86+
status?: string;
87+
captureSnapshot?: boolean;
88+
waitForCompletion?: boolean;
89+
noClearFileChooser?: boolean;
90+
}
91+
): Promise<ToolResult> {
92+
const page = context.existingPage();
93+
const dismissFileChooser = !options.noClearFileChooser && context.hasFileChooser();
94+
95+
try
96+
{
97+
if (options.waitForCompletion)
98+
{
99+
await waitForCompletion(page, () => options.callback(page));
100+
} else
101+
{
102+
await options.callback(page);
103+
}
104+
} finally
105+
{
106+
if (dismissFileChooser) context.clearFileChooser();
107+
}
108+
109+
const result: ToolResult = options.captureSnapshot
110+
? await captureAriaSnapshot(context, options.status)
111+
: {
112+
content: [{ type: 'text', text: options.status || '' }],
113+
};
114+
return result;
115+
}
116+
77117
export async function runAndWait(
78118
context: Context,
79119
status: string,
80120
callback: (page: playwright.Page) => Promise<any>,
81121
snapshot: boolean = false,
82122
): Promise<ToolResult> {
83-
const page = context.existingPage();
84-
const dismissFileChooser = context.hasFileChooser();
85-
await waitForCompletion(page, () => callback(page));
86-
if (dismissFileChooser) context.clearFileChooser();
87-
const result: ToolResult = snapshot
88-
? await captureAriaSnapshot(context, status)
89-
: {
90-
content: [{ type: 'text', text: status }],
91-
};
92-
return result;
123+
return run(context, {
124+
callback,
125+
status,
126+
captureSnapshot: snapshot,
127+
waitForCompletion: true,
128+
});
129+
}
130+
131+
export async function runAndWaitWithSnapshot(
132+
context: Context,
133+
options: {
134+
callback: (page: playwright.Page) => Promise<any>;
135+
status?: string;
136+
noClearFileChooser?: boolean;
137+
}
138+
): Promise<ToolResult> {
139+
return run(context, {
140+
...options,
141+
captureSnapshot: true,
142+
waitForCompletion: true,
143+
});
144+
}
145+
146+
class PageSnapshot {
147+
private _frameLocators: PageOrFrameLocator[] = [];
148+
private _text!: string;
149+
150+
constructor() {
151+
}
152+
153+
static async create(page: playwright.Page): Promise<PageSnapshot> {
154+
const snapshot = new PageSnapshot();
155+
await snapshot._build(page);
156+
return snapshot;
157+
}
158+
159+
text(options?: { status?: string, hasFileChooser?: boolean; }): string {
160+
const results: string[] = [];
161+
if (options?.status)
162+
{
163+
results.push(options.status);
164+
results.push('');
165+
}
166+
if (options?.hasFileChooser)
167+
{
168+
results.push('- There is a file chooser visible that requires browser_choose_file to be called');
169+
results.push('');
170+
}
171+
results.push(this._text);
172+
return results.join('\n');
173+
}
174+
175+
private async _build(page: playwright.Page) {
176+
const yamlDocument = await this._snapshotFrame(page);
177+
const lines = [];
178+
lines.push(
179+
`- Page URL: ${page.url()}`,
180+
`- Page Title: ${await page.title()}`
181+
);
182+
lines.push(
183+
`- Page Snapshot`,
184+
'```yaml',
185+
yamlDocument.toString().trim(),
186+
'```',
187+
''
188+
);
189+
this._text = lines.join('\n');
190+
}
191+
192+
private async _snapshotFrame(frame: playwright.Page | playwright.FrameLocator) {
193+
const frameIndex = this._frameLocators.push(frame) - 1;
194+
const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true });
195+
const snapshot = yaml.parseDocument(snapshotString);
196+
197+
const visit = async (node: any): Promise<unknown> => {
198+
if (yaml.isPair(node))
199+
{
200+
await Promise.all([
201+
visit(node.key).then(k => node.key = k),
202+
visit(node.value).then(v => node.value = v)
203+
]);
204+
} else if (yaml.isSeq(node) || yaml.isMap(node))
205+
{
206+
node.items = await Promise.all(node.items.map(visit));
207+
} else if (yaml.isScalar(node))
208+
{
209+
if (typeof node.value === 'string')
210+
{
211+
const value = node.value;
212+
if (frameIndex > 0)
213+
node.value = value.replace('[ref=', `[ref=f${frameIndex}`);
214+
if (value.startsWith('iframe '))
215+
{
216+
const ref = value.match(/\[ref=(.*)\]/)?.[1];
217+
if (ref)
218+
{
219+
try
220+
{
221+
const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`));
222+
return snapshot.createPair(node.value, childSnapshot);
223+
} catch (error)
224+
{
225+
return snapshot.createPair(node.value, '<could not take iframe snapshot>');
226+
}
227+
}
228+
}
229+
}
230+
}
231+
232+
return node;
233+
};
234+
await visit(snapshot.contents);
235+
return snapshot;
236+
}
237+
238+
refLocator(ref: string): playwright.Locator {
239+
let frame = this._frameLocators[0];
240+
const match = ref.match(/^f(\d+)(.*)/);
241+
if (match)
242+
{
243+
const frameIndex = parseInt(match[1], 10);
244+
frame = this._frameLocators[frameIndex];
245+
ref = match[2];
246+
}
247+
248+
if (!frame)
249+
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
250+
251+
return frame.locator(`aria-ref=${ref}`);
252+
}
93253
}
94254

95255
export async function captureAriaSnapshot(
96256
context: Context,
97257
status: string = '',
98258
): Promise<ToolResult> {
99259
const page = context.existingPage();
100-
const lines = [];
101-
if (status) lines.push(`${status}`);
102-
lines.push(
103-
'',
104-
`- Page URL: ${page.url()}`,
105-
`- Page Title: ${await page.title()}`,
106-
);
107-
if (context.hasFileChooser())
108-
lines.push(
109-
`- There is a file chooser visible that requires browser_choose_file to be called`,
110-
);
111-
lines.push(
112-
`- Page Snapshot`,
113-
'```yaml',
114-
await context.allFramesSnapshot(),
115-
'```',
116-
'',
117-
);
260+
const snapshot = await PageSnapshot.create(page);
118261
return {
119-
content: [{ type: 'text', text: lines.join('\n') }],
262+
content: [{
263+
type: 'text',
264+
text: snapshot.text({
265+
status,
266+
hasFileChooser: context.hasFileChooser()
267+
})
268+
}],
120269
};
121270
}

0 commit comments

Comments
 (0)