Skip to content
Open
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
32 changes: 31 additions & 1 deletion packages/tools/src/__tests__/browser-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ function createManager() {
role: "button",
name: "Send",
selector: "#send",
xpath: "//*[@id=\"send\"]",
testId: "[data-testid=\"send\"]",
nth: 0,
box: { x: 10, y: 20, width: 30, height: 12 },
},
Expand Down Expand Up @@ -154,7 +156,11 @@ describe("createBrowserTool", () => {

const snapshot = await tool.execute("id", { action: "snapshot" });
expect((snapshot.content[0] as any).text).toContain("[1] button \"Send\"");
expect((snapshot.details as any).refs["1"]).toMatchObject({ selector: "#send" });
expect((snapshot.details as any).refs["1"]).toMatchObject({
selector: "#send",
xpath: "//*[@id=\"send\"]",
testId: "[data-testid=\"send\"]",
});

const ariaSnapshot = await tool.execute("id", { action: "snapshot", format: "aria" });
expect((ariaSnapshot.content[0] as any).text).toContain("ARIA snapshot");
Expand All @@ -168,6 +174,30 @@ describe("createBrowserTool", () => {
expect(manager.close).toHaveBeenCalled();
});

it("stores selector candidates for each click target", async () => {
const { manager } = createManager();
const tool = createBrowserTool(manager as any);

await tool.execute("id", { action: "snapshot" });
const refClick = await tool.execute("id", { action: "click", ref: "1" });
expect(refClick.details).toMatchObject({
action: "click",
targetId: "tab_1",
target: "ref 1 (Send)",
selectors: {
css: "#send",
xpath: "//*[@id=\"send\"]",
testId: "[data-testid=\"send\"]",
},
});

const directClick = await tool.execute("id", { action: "click", selector: "button" });
expect(directClick.details).toMatchObject({
action: "click",
selectors: { css: "button" },
});
});

it("does not crash when evaluate resolves to undefined", async () => {
const { manager, page } = createManager();
(page.evaluate as any).mockResolvedValueOnce(undefined);
Expand Down
53 changes: 51 additions & 2 deletions packages/tools/src/browser/browser-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ interface BrowserSnapshotRef {
value?: string;
description?: string;
selector: string;
xpath?: string;
testId?: string;
nth: number;
box?: { x: number; y: number; width: number; height: number };
}
Expand Down Expand Up @@ -557,14 +559,19 @@ export function createBrowserTool(
element?: string;
required?: boolean;
label?: string;
}): { locator: any; description: string } | undefined => {
}): { locator: any; description: string; selectors: Record<string, string> } | undefined => {
const refValue = params.ref?.trim();
if (refValue) {
const snapRef = getSnapshotRef(page, refValue);
if (snapRef) {
return {
locator: resolveLocatorFromSelector(page, snapRef.selector),
description: `ref ${refValue}${snapRef.name ? ` (${snapRef.name})` : ""}`,
selectors: {
css: snapRef.selector,
...(snapRef.xpath ? { xpath: snapRef.xpath } : {}),
...(snapRef.testId ? { testId: snapRef.testId } : {}),
},
};
}
throw new Error(`Unknown snapshot ref: ${refValue}. Take a new snapshot or pass selector explicitly.`);
Expand All @@ -575,6 +582,7 @@ export function createBrowserTool(
return {
locator: resolveLocatorFromSelector(page, selector),
description: `selector ${selector}`,
selectors: { css: selector },
};
}

Expand Down Expand Up @@ -802,6 +810,38 @@ export function createBrowserTool(
return `body > ${parts.join(" > ")}`;
};

const xpathPath = (element: BrowserDomElementLike): string => {
if (element.id) {
return `//*[@id="${element.id.replace(/"/g, '\\"')}"]`;
}
const parts: string[] = [];
let current: BrowserDomElementLike | null | undefined = element;
while (current) {
const tag = current.tagName.toLowerCase();
const parent: BrowserDomElementLike | null | undefined = current.parentElement;
if (!parent || tag === "html") {
parts.unshift(tag);
break;
}
const siblings = Array.from(parent.children as ArrayLike<BrowserDomElementLike>)
.filter((child) => child.tagName === current?.tagName);
const index = siblings.indexOf(current) + 1;
parts.unshift(siblings.length > 1 ? `${tag}[${Math.max(1, index)}]` : tag);
current = parent;
}
return `/${parts.join("/")}`;
};

const testIdSelector = (element: BrowserDomElementLike): string | undefined => {
for (const attr of ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa"]) {
const value = element.getAttribute(attr)?.trim();
if (value) {
return `[${attr}="${value.replace(/"/g, '\\"')}"]`;
}
}
return undefined;
};

const dedup = new Set<BrowserDomElementLike>();
const elements = Array.from(root.querySelectorAll(candidateSelector))
.filter((element) => visible(element))
Expand Down Expand Up @@ -832,6 +872,8 @@ export function createBrowserTool(
value: value || undefined,
description,
selector: cssPath(element),
xpath: xpathPath(element),
testId: testIdSelector(element),
nth,
box: {
x: Math.max(0, Math.round(rect.left)),
Expand Down Expand Up @@ -1256,6 +1298,8 @@ export function createBrowserTool(
name: node.name,
nth: node.nth,
selector: node.selector,
xpath: node.xpath,
testId: node.testId,
},
])),
stats: snapshot.stats,
Expand Down Expand Up @@ -1315,7 +1359,12 @@ export function createBrowserTool(
label: "selector or ref",
});
const verb = await clickLocator(locatorInfo!.locator, params);
return textResult(`${capitalize(verb)}: ${locatorInfo!.description}`);
return textResult(`${capitalize(verb)}: ${locatorInfo!.description}`, {
action: "click",
targetId: activeBrowserManager.getTabId(page),
target: locatorInfo!.description,
selectors: locatorInfo!.selectors,
});
}

case "type": {
Expand Down