Skip to content

Commit 8f0f97b

Browse files
use js click instead of playwright (#683)
* use js click * rm unused imports * rm newline * changeset * radio button eval * checkboxes eval * rm comment
1 parent edd6d3f commit 8f0f97b

File tree

6 files changed

+93
-81
lines changed

6 files changed

+93
-81
lines changed

Diff for: .changeset/green-signs-live.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
use javsacript click instead of playwright

Diff for: evals/evals.config.json

+8
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@
317317
{
318318
"name": "extract_single_link",
319319
"categories": ["extract"]
320+
},
321+
{
322+
"name": "radio_btn",
323+
"categories": ["act"]
324+
},
325+
{
326+
"name": "checkboxes",
327+
"categories": ["act"]
320328
}
321329
]
322330
}

Diff for: evals/tasks/checkboxes.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const checkboxes: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
await stagehand.page.goto(
10+
"https://browserbase.github.io/stagehand-eval-sites/sites/checkboxes/",
11+
);
12+
13+
await stagehand.page.act({
14+
action: "click the 'baseball' option",
15+
});
16+
17+
await stagehand.page.act({
18+
action: "click the 'netball' option",
19+
});
20+
21+
const baseballChecked = await stagehand.page
22+
.locator('input[type="checkbox"][name="sports"][value="baseball"]')
23+
.isChecked();
24+
25+
const netballChecked = await stagehand.page
26+
.locator('input[type="checkbox"][name="sports"][value="netball"]')
27+
.isChecked();
28+
29+
await stagehand.close();
30+
31+
return {
32+
_success: baseballChecked && netballChecked,
33+
debugUrl,
34+
sessionUrl,
35+
logs: logger.getLogs(),
36+
};
37+
};

Diff for: evals/tasks/radio_btn.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const radio_btn: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
await stagehand.page.goto(
10+
"https://browserbase.github.io/stagehand-eval-sites/sites/paneer-pizza/",
11+
);
12+
13+
await stagehand.page.act({
14+
action: "click the 'medium' option",
15+
});
16+
17+
// confirm that the Medium radio is now checked
18+
const radioBtnClicked = await stagehand.page
19+
.locator('input[type="radio"][name="Pizza"][value="Medium"]')
20+
.isChecked();
21+
22+
await stagehand.close();
23+
24+
return {
25+
_success: radioBtnClicked,
26+
debugUrl,
27+
sessionUrl,
28+
logs: logger.getLogs(),
29+
};
30+
};

Diff for: lib/handlers/handlerUtils/actHandlerUtils.ts

+5-81
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Page, Locator, errors as PlaywrightErrors } from "@playwright/test";
1+
import { Page, Locator } from "@playwright/test";
22
import { PlaywrightCommandException } from "../../../types/playwright";
33
import { StagehandPage } from "../../StagehandPage";
44
import { getNodeFromXpath } from "@/lib/dom/utils";
55
import { Logger } from "../../../types/log";
66
import { MethodHandlerContext } from "@/types/act";
7+
import { StagehandClickError } from "@/types/stagehandErrors";
78

89
/**
910
* A mapping of playwright methods that may be chosen by the LLM to their
@@ -343,83 +344,9 @@ export async function clickElement(ctx: MethodHandlerContext) {
343344
});
344345

345346
try {
346-
// If it's a radio input, try to click its label
347-
const isRadio = await locator.evaluate((el) => {
348-
return el instanceof HTMLInputElement && el.type === "radio";
347+
await locator.evaluate((el) => {
348+
(el as HTMLElement).click();
349349
});
350-
351-
// Extract the click options (if any) from args[0]
352-
const clickArg = (args[0] ?? {}) as Record<string, unknown>;
353-
354-
// Decide which locator we actually want to click (for radio inputs, prefer label if present)
355-
let finalLocator = locator;
356-
if (isRadio) {
357-
const inputId = await locator.evaluate(
358-
(el) => (el as HTMLInputElement).id,
359-
);
360-
let labelLocator = null;
361-
362-
if (inputId) {
363-
labelLocator = stagehandPage.page.locator(`label[for="${inputId}"]`);
364-
}
365-
if (!labelLocator || (await labelLocator.count()) < 1) {
366-
// Check ancestor <label>
367-
labelLocator = stagehandPage.page
368-
.locator(`xpath=${xpath}/ancestor::label`)
369-
.first();
370-
}
371-
if ((await labelLocator.count()) < 1) {
372-
// Check sibling <label>
373-
labelLocator = locator
374-
.locator("xpath=following-sibling::label")
375-
.first();
376-
if ((await labelLocator.count()) < 1) {
377-
labelLocator = locator
378-
.locator("xpath=preceding-sibling::label")
379-
.first();
380-
}
381-
}
382-
383-
if ((await labelLocator.count()) > 0) {
384-
finalLocator = labelLocator;
385-
}
386-
}
387-
388-
// Try clicking with a short (5s) timeout
389-
try {
390-
await finalLocator.click({
391-
...clickArg,
392-
timeout: 5000,
393-
});
394-
} catch (error) {
395-
// If it's a TimeoutError, retry with force: true
396-
if (error instanceof PlaywrightErrors.TimeoutError) {
397-
logger({
398-
category: "action",
399-
message: "First click attempt timed out, retrying with force...",
400-
level: 2,
401-
});
402-
try {
403-
await finalLocator.click({
404-
...clickArg,
405-
force: true,
406-
});
407-
} catch (forceError) {
408-
// If forced click also fails, throw a more descriptive error
409-
throw new PlaywrightCommandException(
410-
`Failed to click element at [${xpath}]. ` +
411-
`Timeout after 5s, then force-click also failed. ` +
412-
`Original timeout error: ${error.message}, ` +
413-
`Force-click error: ${forceError.message}`,
414-
);
415-
}
416-
} else {
417-
// Non-timeout error on the first click
418-
throw new PlaywrightCommandException(
419-
`Failed to click element at [${xpath}]. ` + `Error: ${error.message}`,
420-
);
421-
}
422-
}
423350
} catch (e) {
424351
logger({
425352
category: "action",
@@ -433,10 +360,7 @@ export async function clickElement(ctx: MethodHandlerContext) {
433360
args: { value: JSON.stringify(args), type: "object" },
434361
},
435362
});
436-
437-
throw new PlaywrightCommandException(
438-
`Could not complete click action at [${xpath}]. Reason: ${e.message}`,
439-
);
363+
throw new StagehandClickError(xpath, e.message);
440364
}
441365

442366
await handlePossiblePageNavigation(

Diff for: types/stagehandErrors.ts

+8
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,11 @@ export class StagehandDomProcessError extends StagehandError {
147147
super(`Error Processing Dom: ${message}`);
148148
}
149149
}
150+
151+
export class StagehandClickError extends StagehandError {
152+
constructor(message: string, selector: string) {
153+
super(
154+
`Error Clicking Element with selector: ${selector} Reason: ${message}`,
155+
);
156+
}
157+
}

0 commit comments

Comments
 (0)