@@ -346,11 +346,11 @@ export class StagehandActHandler {
346
346
347
347
throw new PlaywrightCommandException ( e . message ) ;
348
348
}
349
- } else if ( typeof locator [ method as keyof typeof locator ] === "function " ) {
350
- // Log current URL before action
349
+ } else if ( method === "click " ) {
350
+ // Log the URL before clicking
351
351
this . logger ( {
352
352
category : "action" ,
353
- message : "page URL before action " ,
353
+ message : "page URL before click " ,
354
354
level : 2 ,
355
355
auxiliary : {
356
356
url : {
@@ -360,17 +360,59 @@ export class StagehandActHandler {
360
360
} ,
361
361
} ) ;
362
362
363
- // Perform the action
363
+ // if the element is a radio button, we should try to click the label instead
364
364
try {
365
- await (
366
- locator [ method as keyof Locator ] as unknown as (
367
- ...args : string [ ]
368
- ) => Promise < void >
369
- ) ( ...args . map ( ( arg ) => arg ?. toString ( ) || "" ) ) ;
365
+ const isRadio = await locator . evaluate ( ( el ) => {
366
+ return el instanceof HTMLInputElement && el . type === "radio" ;
367
+ } ) ;
368
+
369
+ const clickArg = args . length ? args [ 0 ] : undefined ;
370
+
371
+ if ( isRadio ) {
372
+ // if it’s a radio button, try to find a label to click
373
+ const inputId = await locator . evaluate ( ( el ) => el . id ) ;
374
+ let labelLocator ;
375
+
376
+ if ( inputId ) {
377
+ // if the radio button has an ID, try label[for="thatId"]
378
+ labelLocator = this . stagehandPage . page . locator (
379
+ `label[for="${ inputId } "]` ,
380
+ ) ;
381
+ }
382
+ if ( ! labelLocator || ( await labelLocator . count ( ) ) < 1 ) {
383
+ // if no label was found or the label doesn’t exist, check if
384
+ // there is an ancestor <label>
385
+ labelLocator = this . stagehandPage . page
386
+ . locator ( `xpath=${ xpath } /ancestor::label` )
387
+ . first ( ) ;
388
+ }
389
+ if ( ( await labelLocator . count ( ) ) < 1 ) {
390
+ // if still no label, try checking for a following-sibling or preceding-sibling label
391
+ labelLocator = locator
392
+ . locator ( `xpath=following-sibling::label` )
393
+ . first ( ) ;
394
+ if ( ( await labelLocator . count ( ) ) < 1 ) {
395
+ labelLocator = locator
396
+ . locator ( `xpath=preceding-sibling::label` )
397
+ . first ( ) ;
398
+ }
399
+ }
400
+ if ( ( await labelLocator . count ( ) ) > 0 ) {
401
+ // if we found a label, click it
402
+ await labelLocator . click ( clickArg ) ;
403
+ } else {
404
+ // otherwise, just click the radio button itself
405
+ await locator . click ( clickArg ) ;
406
+ }
407
+ } else {
408
+ // here we just do a normal click if it's not a radio input
409
+ const clickArg = args . length ? args [ 0 ] : undefined ;
410
+ await locator . click ( clickArg ) ;
411
+ }
370
412
} catch ( e ) {
371
413
this . logger ( {
372
414
category : "action" ,
373
- message : "error performing method " ,
415
+ message : "error performing click " ,
374
416
level : 1 ,
375
417
auxiliary : {
376
418
error : {
@@ -400,106 +442,150 @@ export class StagehandActHandler {
400
442
}
401
443
402
444
// Handle navigation if a new page is opened
403
- if ( method === "click" ) {
445
+ this . logger ( {
446
+ category : "action" ,
447
+ message : "clicking element, checking for page navigation" ,
448
+ level : 1 ,
449
+ auxiliary : {
450
+ xpath : {
451
+ value : xpath ,
452
+ type : "string" ,
453
+ } ,
454
+ } ,
455
+ } ) ;
456
+
457
+ // NAVIDNOTE: Should this happen before we wait for locator[method]?
458
+ const newOpenedTab = await Promise . race ( [
459
+ new Promise < Page | null > ( ( resolve ) => {
460
+ // TODO: This is a hack to get the new page
461
+ // We should find a better way to do this
462
+ this . stagehandPage . context . once ( "page" , ( page ) => resolve ( page ) ) ;
463
+ setTimeout ( ( ) => resolve ( null ) , 1_500 ) ;
464
+ } ) ,
465
+ ] ) ;
466
+
467
+ this . logger ( {
468
+ category : "action" ,
469
+ message : "clicked element" ,
470
+ level : 1 ,
471
+ auxiliary : {
472
+ newOpenedTab : {
473
+ value : newOpenedTab ? "opened a new tab" : "no new tabs opened" ,
474
+ type : "string" ,
475
+ } ,
476
+ } ,
477
+ } ) ;
478
+
479
+ if ( newOpenedTab ) {
404
480
this . logger ( {
405
481
category : "action" ,
406
- message : "clicking element, checking for page navigation " ,
482
+ message : "new page detected (new tab) with URL " ,
407
483
level : 1 ,
408
484
auxiliary : {
409
- xpath : {
410
- value : xpath ,
485
+ url : {
486
+ value : newOpenedTab . url ( ) ,
411
487
type : "string" ,
412
488
} ,
413
489
} ,
414
490
} ) ;
415
- // try {
416
- // // Force-click here
417
- // await locator.click({ force: true });
418
- // } catch (e) {
419
- // // handle/log exception
420
- // throw new PlaywrightCommandException(e.message);
421
- // }
422
-
423
- // NAVIDNOTE: Should this happen before we wait for locator[method]?
424
- const newOpenedTab = await Promise . race ( [
425
- new Promise < Page | null > ( ( resolve ) => {
426
- // TODO: This is a hack to get the new page
427
- // We should find a better way to do this
428
- this . stagehandPage . context . once ( "page" , ( page ) => resolve ( page ) ) ;
429
- setTimeout ( ( ) => resolve ( null ) , 1_500 ) ;
430
- } ) ,
431
- ] ) ;
491
+ await newOpenedTab . close ( ) ;
492
+ await this . stagehandPage . page . goto ( newOpenedTab . url ( ) ) ;
493
+ await this . stagehandPage . page . waitForLoadState ( "domcontentloaded" ) ;
494
+ await this . stagehandPage . _waitForSettledDom ( domSettleTimeoutMs ) ;
495
+ }
432
496
497
+ await Promise . race ( [
498
+ this . stagehandPage . page . waitForLoadState ( "networkidle" ) ,
499
+ new Promise ( ( resolve ) => setTimeout ( resolve , 5_000 ) ) ,
500
+ ] ) . catch ( ( e ) => {
433
501
this . logger ( {
434
502
category : "action" ,
435
- message : "clicked element " ,
503
+ message : "network idle timeout hit " ,
436
504
level : 1 ,
437
505
auxiliary : {
438
- newOpenedTab : {
439
- value : newOpenedTab ? "opened a new tab" : "no new tabs opened" ,
506
+ trace : {
507
+ value : e . stack ,
508
+ type : "string" ,
509
+ } ,
510
+ message : {
511
+ value : e . message ,
440
512
type : "string" ,
441
513
} ,
442
514
} ,
443
515
} ) ;
516
+ } ) ;
444
517
445
- if ( newOpenedTab ) {
446
- this . logger ( {
447
- category : "action" ,
448
- message : "new page detected (new tab) with URL" ,
449
- level : 1 ,
450
- auxiliary : {
451
- url : {
452
- value : newOpenedTab . url ( ) ,
453
- type : "string" ,
454
- } ,
455
- } ,
456
- } ) ;
457
- await newOpenedTab . close ( ) ;
458
- await this . stagehandPage . page . goto ( newOpenedTab . url ( ) ) ;
459
- await this . stagehandPage . page . waitForLoadState ( "domcontentloaded" ) ;
460
- await this . stagehandPage . _waitForSettledDom ( domSettleTimeoutMs ) ;
461
- }
518
+ this . logger ( {
519
+ category : "action" ,
520
+ message : "finished waiting for (possible) page navigation" ,
521
+ level : 1 ,
522
+ } ) ;
462
523
463
- await Promise . race ( [
464
- this . stagehandPage . page . waitForLoadState ( "networkidle" ) ,
465
- new Promise ( ( resolve ) => setTimeout ( resolve , 5_000 ) ) ,
466
- ] ) . catch ( ( e ) => {
467
- this . logger ( {
468
- category : "action" ,
469
- message : "network idle timeout hit" ,
470
- level : 1 ,
471
- auxiliary : {
472
- trace : {
473
- value : e . stack ,
474
- type : "string" ,
475
- } ,
476
- message : {
477
- value : e . message ,
478
- type : "string" ,
479
- } ,
524
+ if ( this . stagehandPage . page . url ( ) !== initialUrl ) {
525
+ this . logger ( {
526
+ category : "action" ,
527
+ message : "new page detected with URL" ,
528
+ level : 1 ,
529
+ auxiliary : {
530
+ url : {
531
+ value : this . stagehandPage . page . url ( ) ,
532
+ type : "string" ,
480
533
} ,
481
- } ) ;
534
+ } ,
482
535
} ) ;
536
+ }
537
+ } else if ( typeof locator [ method as keyof typeof locator ] === "function" ) {
538
+ // Fallback: any other locator method
539
+ // Log current URL before action
540
+ this . logger ( {
541
+ category : "action" ,
542
+ message : "page URL before action" ,
543
+ level : 2 ,
544
+ auxiliary : {
545
+ url : {
546
+ value : this . stagehandPage . page . url ( ) ,
547
+ type : "string" ,
548
+ } ,
549
+ } ,
550
+ } ) ;
483
551
552
+ // Perform the action
553
+ try {
554
+ await (
555
+ locator [ method as keyof Locator ] as unknown as (
556
+ ...args : string [ ]
557
+ ) => Promise < void >
558
+ ) ( ...args . map ( ( arg ) => arg ?. toString ( ) || "" ) ) ;
559
+ } catch ( e ) {
484
560
this . logger ( {
485
561
category : "action" ,
486
- message : "finished waiting for (possible) page navigation " ,
562
+ message : "error performing method " ,
487
563
level : 1 ,
564
+ auxiliary : {
565
+ error : {
566
+ value : e . message ,
567
+ type : "string" ,
568
+ } ,
569
+ trace : {
570
+ value : e . stack ,
571
+ type : "string" ,
572
+ } ,
573
+ xpath : {
574
+ value : xpath ,
575
+ type : "string" ,
576
+ } ,
577
+ method : {
578
+ value : method ,
579
+ type : "string" ,
580
+ } ,
581
+ args : {
582
+ value : JSON . stringify ( args ) ,
583
+ type : "object" ,
584
+ } ,
585
+ } ,
488
586
} ) ;
489
587
490
- if ( this . stagehandPage . page . url ( ) !== initialUrl ) {
491
- this . logger ( {
492
- category : "action" ,
493
- message : "new page detected with URL" ,
494
- level : 1 ,
495
- auxiliary : {
496
- url : {
497
- value : this . stagehandPage . page . url ( ) ,
498
- type : "string" ,
499
- } ,
500
- } ,
501
- } ) ;
502
- }
588
+ throw new PlaywrightCommandException ( e . message ) ;
503
589
}
504
590
} else {
505
591
this . logger ( {
@@ -547,47 +633,6 @@ export class StagehandActHandler {
547
633
} ) ;
548
634
549
635
const outerHtml = clone . outerHTML ;
550
-
551
- // const variables = {
552
- // // Replace with your actual variables and their values
553
- // // Example:
554
- // username: "JohnDoe",
555
-
556
- // };
557
-
558
- // // Function to replace variable values with variable names
559
- // const replaceVariables = (element: Element) => {
560
- // if (element instanceof HTMLElement) {
561
- // for (const [key, value] of Object.entries(variables)) {
562
- // if (value) {
563
- // element.innerText = element.innerText.replace(
564
- // new RegExp(value, "g"),
565
- // key,
566
- // );
567
- // }
568
- // }
569
- // }
570
-
571
- // if (
572
- // element instanceof HTMLInputElement ||
573
- // element instanceof HTMLTextAreaElement
574
- // ) {
575
- // for (const [key, value] of Object.entries(variables)) {
576
- // if (value) {
577
- // element.value = element.value.replace(
578
- // new RegExp(value, "g"),
579
- // key,
580
- // );
581
- // }
582
- // }
583
- // }
584
- // };
585
-
586
- // // Replace variables in the cloned element
587
- // replaceVariables(clone);
588
-
589
- // // Replace variables in all child elements
590
- // clone.querySelectorAll("*").forEach(replaceVariables);
591
636
return outerHtml . trim ( ) . replace ( / \s + / g, " " ) ;
592
637
} ) ;
593
638
}
0 commit comments