Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 32 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class Context {
private _page: playwright.Page | undefined;
private _console: playwright.ConsoleMessage[] = [];
private _createPagePromise: Promise<playwright.Page> | undefined;
private _lastSnapshotFrames: playwright.FrameLocator[] = [];

constructor(userDataDir: string, launchOptions?: playwright.LaunchOptions) {
this._userDataDir = userDataDir;
Expand Down Expand Up @@ -90,4 +91,35 @@ export class Context {
const [page] = context.pages();
return { page };
}

async allFrameSnapshot() {
const page = this.existingPage();
const visibleFrames = await page.locator('iframe').filter({ visible: true }).all();
this._lastSnapshotFrames = visibleFrames.map(frame => frame.contentFrame());

const snapshots = await Promise.all([
page.locator('html').ariaSnapshot({ ref: true }),
...this._lastSnapshotFrames.map(async (frame, index) => {
const snapshot = await frame.locator('html').ariaSnapshot({ ref: true });
return snapshot.replaceAll('[ref=', `[ref=f${index}`);
})
]);

return snapshots.join('\n');
}

refLocator(ref: string): playwright.Locator {
const page = this.existingPage();
let frame: playwright.Frame | playwright.FrameLocator = page.mainFrame();
const match = ref.match(/^f(\d+)(.*)/);
if (match) {
const frameIndex = parseInt(match[1], 10);
if (!this._lastSnapshotFrames[frameIndex])
throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`);
frame = this._lastSnapshotFrames[frameIndex];
ref = match[2];
}

return frame.locator(`aria-ref=${ref}`);
}
}
2 changes: 1 addition & 1 deletion src/tools/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const navigate: ToolFactory = snapshot => ({
// Cap load event to 5 seconds, the page is operational at this point.
await page.waitForLoadState('load', { timeout: 5000 }).catch(() => {});
if (snapshot)
return captureAriaSnapshot(page);
return captureAriaSnapshot(context);
return {
content: [{
type: 'text',
Expand Down
26 changes: 7 additions & 19 deletions src/tools/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const snapshot: Tool = {
},

handle: async context => {
return await captureAriaSnapshot(context.existingPage());
return await captureAriaSnapshot(context);
},
};

Expand All @@ -48,7 +48,7 @@ export const click: Tool = {

handle: async (context, params) => {
const validatedParams = elementSchema.parse(params);
return runAndWait(context, `"${validatedParams.element}" clicked`, page => refLocator(page, validatedParams.ref).click(), true);
return runAndWait(context, `"${validatedParams.element}" clicked`, page => context.refLocator(validatedParams.ref).click(), true);
},
};

Expand All @@ -69,8 +69,8 @@ export const drag: Tool = {
handle: async (context, params) => {
const validatedParams = dragSchema.parse(params);
return runAndWait(context, `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`, async page => {
const startLocator = refLocator(page, validatedParams.startRef);
const endLocator = refLocator(page, validatedParams.endRef);
const startLocator = context.refLocator(validatedParams.startRef);
const endLocator = context.refLocator(validatedParams.endRef);
await startLocator.dragTo(endLocator);
}, true);
},
Expand All @@ -85,7 +85,7 @@ export const hover: Tool = {

handle: async (context, params) => {
const validatedParams = elementSchema.parse(params);
return runAndWait(context, `Hovered over "${validatedParams.element}"`, page => refLocator(page, validatedParams.ref).hover(), true);
return runAndWait(context, `Hovered over "${validatedParams.element}"`, page => context.refLocator(validatedParams.ref).hover(), true);
},
};

Expand All @@ -104,7 +104,7 @@ export const type: Tool = {
handle: async (context, params) => {
const validatedParams = typeSchema.parse(params);
return await runAndWait(context, `Typed "${validatedParams.text}" into "${validatedParams.element}"`, async page => {
const locator = refLocator(page, validatedParams.ref);
const locator = context.refLocator(validatedParams.ref);
await locator.fill(validatedParams.text);
if (validatedParams.submit)
await locator.press('Enter');
Expand All @@ -126,7 +126,7 @@ export const selectOption: Tool = {
handle: async (context, params) => {
const validatedParams = selectOptionSchema.parse(params);
return await runAndWait(context, `Selected option in "${validatedParams.element}"`, async page => {
const locator = refLocator(page, validatedParams.ref);
const locator = context.refLocator(validatedParams.ref);
await locator.selectOption(validatedParams.values);
}, true);
},
Expand All @@ -153,15 +153,3 @@ export const screenshot: Tool = {
};
},
};

function refLocator(page: playwright.Page, ref: string): playwright.Locator {
let frame = page.frames()[0];
const match = ref.match(/^f(\d+)(.*)/);
if (match) {
const frameIndex = parseInt(match[1], 10);
frame = page.frames()[frameIndex];
ref = match[2];
}

return frame.locator(`aria-ref=${ref}`);
}
17 changes: 4 additions & 13 deletions src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,29 +74,20 @@ async function waitForCompletion<R>(page: playwright.Page, callback: () => Promi
export async function runAndWait(context: Context, status: string, callback: (page: playwright.Page) => Promise<any>, snapshot: boolean = false): Promise<ToolResult> {
const page = context.existingPage();
await waitForCompletion(page, () => callback(page));
return snapshot ? captureAriaSnapshot(page, status) : {
return snapshot ? captureAriaSnapshot(context, status) : {
content: [{ type: 'text', text: status }],
};
}

export async function captureAllFrameSnapshot(page: playwright.Page): Promise<string> {
const snapshots = await Promise.all(page.frames().map(frame => frame.locator('html').ariaSnapshot({ ref: true })));
const scopedSnapshots = snapshots.map((snapshot, frameIndex) => {
if (frameIndex === 0)
return snapshot;
return snapshot.replaceAll('[ref=', `[ref=f${frameIndex}`);
});
return scopedSnapshots.join('\n');
}

export async function captureAriaSnapshot(page: playwright.Page, status: string = ''): Promise<ToolResult> {
export async function captureAriaSnapshot(context: Context, status: string = ''): Promise<ToolResult> {
const page = context.existingPage();
return {
content: [{ type: 'text', text: `${status ? `${status}\n` : ''}
- Page URL: ${page.url()}
- Page Title: ${await page.title()}
- Page Snapshot
\`\`\`yaml
${await captureAllFrameSnapshot(page)}
${await context.allFrameSnapshot()}
\`\`\`
`
}],
Expand Down
8 changes: 4 additions & 4 deletions tests/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ test('stitched aria frames', async ({ server }) => {
params: {
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<h1>Hello</h1><iframe src="data:text/html,<h1>World</h1>"></iframe>',
url: 'data:text/html,<h1>Hello</h1><iframe src="data:text/html,<h1>World</h1>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>',
},
},
});
Expand All @@ -438,14 +438,14 @@ test('stitched aria frames', async ({ server }) => {
content: [{
type: 'text',
text: `
- Page URL: data:text/html,<h1>Hello</h1><iframe src="data:text/html,<h1>World</h1>"></iframe>
- Page URL: data:text/html,<h1>Hello</h1><iframe src="data:text/html,<h1>World</h1>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>
- Page Title:
- Page Snapshot
\`\`\`yaml
- document [ref=s1e2]:
- heading \"Hello\" [level=1] [ref=s1e4]
- document [ref=f1s1e2]:
- heading \"World\" [level=1] [ref=f1s1e4]
- document [ref=f0s1e2]:
- heading \"World\" [level=1] [ref=f0s1e4]
\`\`\`
`,
}],
Expand Down