browser-pilot 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.cjs +264 -17
  2. package/dist/cli.mjs +264 -17
  3. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -3487,13 +3487,18 @@ var INPUT_DEBOUNCE_MS = 300;
3487
3487
  var NAVIGATION_DEBOUNCE_MS = 500;
3488
3488
  function selectBestSelectors(candidates) {
3489
3489
  const qualityOrder = {
3490
- "stable-attr": 0,
3491
- id: 1,
3492
- "css-path": 2
3490
+ "role-name": 0,
3491
+ text: 1,
3492
+ "aria-label": 2,
3493
+ testid: 3,
3494
+ "stable-attr": 4,
3495
+ id: 5,
3496
+ "name-attr": 6,
3497
+ "css-path": 7
3493
3498
  };
3494
3499
  const sorted = [...candidates].sort((a, b) => {
3495
- const aOrder = qualityOrder[a.quality] ?? 3;
3496
- const bOrder = qualityOrder[b.quality] ?? 3;
3500
+ const aOrder = qualityOrder[a.quality] ?? 8;
3501
+ const bOrder = qualityOrder[b.quality] ?? 8;
3497
3502
  return aOrder - bOrder;
3498
3503
  });
3499
3504
  const seen = /* @__PURE__ */ new Set();
@@ -3506,6 +3511,67 @@ function selectBestSelectors(candidates) {
3506
3511
  }
3507
3512
  return result;
3508
3513
  }
3514
+ function generateAnnotation(event) {
3515
+ const { kind, element, url } = event;
3516
+ const name = element?.accessibleName || element?.text || element?.ariaLabel;
3517
+ const role = element?.computedRole || element?.role || element?.tag || "";
3518
+ switch (kind) {
3519
+ case "click":
3520
+ if (name && role) {
3521
+ return `Clicked '${name}' ${role}`;
3522
+ } else if (name) {
3523
+ return `Clicked '${name}'`;
3524
+ } else if (role) {
3525
+ return `Clicked ${role}`;
3526
+ }
3527
+ return "Clicked element";
3528
+ case "dblclick":
3529
+ if (name && role) {
3530
+ return `Double-clicked '${name}' ${role}`;
3531
+ }
3532
+ return "Double-clicked element";
3533
+ case "input":
3534
+ if (name) {
3535
+ return `Filled '${name}' with value`;
3536
+ }
3537
+ return "Filled input with value";
3538
+ case "change":
3539
+ if (element?.type === "checkbox" || element?.type === "radio") {
3540
+ const action = event.checked ? "Checked" : "Unchecked";
3541
+ if (name) {
3542
+ return `${action} '${name}' ${element.type}`;
3543
+ }
3544
+ return `${action} ${element.type}`;
3545
+ }
3546
+ if (element?.tag === "select") {
3547
+ if (name) {
3548
+ return `Selected option in '${name}'`;
3549
+ }
3550
+ return "Selected option";
3551
+ }
3552
+ if (name) {
3553
+ return `Changed '${name}'`;
3554
+ }
3555
+ return "Changed element";
3556
+ case "submit":
3557
+ if (name) {
3558
+ return `Submitted '${name}' form`;
3559
+ }
3560
+ return "Submitted form";
3561
+ case "keydown":
3562
+ if (event.key === "Enter") {
3563
+ return "Pressed Enter";
3564
+ }
3565
+ return `Pressed ${event.key}`;
3566
+ case "navigation":
3567
+ return `Navigated to ${url}`;
3568
+ default:
3569
+ if (name) {
3570
+ return `${kind} on '${name}'`;
3571
+ }
3572
+ return `${kind} on element`;
3573
+ }
3574
+ }
3509
3575
  function debounceInputEvents(events) {
3510
3576
  const result = [];
3511
3577
  for (let i = 0; i < events.length; i++) {
@@ -3584,22 +3650,37 @@ function insertNavigationSteps(events, startUrl) {
3584
3650
  }
3585
3651
  return result;
3586
3652
  }
3653
+ function buildElementMeta(event) {
3654
+ const el = event.element;
3655
+ if (!el) return void 0;
3656
+ return {
3657
+ role: el.computedRole || el.role,
3658
+ name: el.accessibleName || el.text || el.ariaLabel,
3659
+ tag: el.tag
3660
+ };
3661
+ }
3587
3662
  function eventToStep(event) {
3588
3663
  const selectors = selectBestSelectors(event.selectors);
3664
+ const elementMeta = buildElementMeta(event);
3665
+ const annotation = generateAnnotation(event);
3589
3666
  switch (event.kind) {
3590
3667
  case "click":
3591
3668
  case "dblclick":
3592
3669
  if (selectors.length === 0) return null;
3593
3670
  return {
3594
3671
  action: "click",
3595
- selector: selectors.length === 1 ? selectors[0] : selectors
3672
+ selector: selectors.length === 1 ? selectors[0] : selectors,
3673
+ element: elementMeta,
3674
+ annotation
3596
3675
  };
3597
3676
  case "input":
3598
3677
  if (selectors.length === 0) return null;
3599
3678
  return {
3600
3679
  action: "fill",
3601
3680
  selector: selectors.length === 1 ? selectors[0] : selectors,
3602
- value: event.value ?? ""
3681
+ value: event.value ?? "",
3682
+ element: elementMeta,
3683
+ annotation
3603
3684
  };
3604
3685
  case "change": {
3605
3686
  if (selectors.length === 0) return null;
@@ -3610,19 +3691,25 @@ function eventToStep(event) {
3610
3691
  return {
3611
3692
  action: "select",
3612
3693
  selector: selectors.length === 1 ? selectors[0] : selectors,
3613
- value: event.value ?? ""
3694
+ value: event.value ?? "",
3695
+ element: elementMeta,
3696
+ annotation
3614
3697
  };
3615
3698
  }
3616
3699
  if (type === "checkbox" || type === "radio") {
3617
3700
  return {
3618
3701
  action: event.checked ? "check" : "uncheck",
3619
- selector: selectors.length === 1 ? selectors[0] : selectors
3702
+ selector: selectors.length === 1 ? selectors[0] : selectors,
3703
+ element: elementMeta,
3704
+ annotation
3620
3705
  };
3621
3706
  }
3622
3707
  return {
3623
3708
  action: "fill",
3624
3709
  selector: selectors.length === 1 ? selectors[0] : selectors,
3625
- value: event.value ?? ""
3710
+ value: event.value ?? "",
3711
+ element: elementMeta,
3712
+ annotation
3626
3713
  };
3627
3714
  }
3628
3715
  case "keydown":
@@ -3631,7 +3718,9 @@ function eventToStep(event) {
3631
3718
  return {
3632
3719
  action: "submit",
3633
3720
  selector: selectors.length === 1 ? selectors[0] : selectors,
3634
- method: "enter"
3721
+ method: "enter",
3722
+ element: elementMeta,
3723
+ annotation
3635
3724
  };
3636
3725
  }
3637
3726
  return null;
@@ -3639,12 +3728,15 @@ function eventToStep(event) {
3639
3728
  if (selectors.length === 0) return null;
3640
3729
  return {
3641
3730
  action: "submit",
3642
- selector: selectors.length === 1 ? selectors[0] : selectors
3731
+ selector: selectors.length === 1 ? selectors[0] : selectors,
3732
+ element: elementMeta,
3733
+ annotation
3643
3734
  };
3644
3735
  case "navigation":
3645
3736
  return {
3646
3737
  action: "goto",
3647
- url: event.url
3738
+ url: event.url,
3739
+ annotation
3648
3740
  };
3649
3741
  default:
3650
3742
  return null;
@@ -3787,19 +3879,50 @@ var RECORDER_SCRIPT = `(function() {
3787
3879
  function getSelectorCandidates(el) {
3788
3880
  const candidates = [];
3789
3881
 
3790
- // 1. Stable attributes (highest quality)
3882
+ // Get semantic info for role-based selectors
3883
+ const role = getRole(el);
3884
+ const name = getAccessibleName(el);
3885
+
3886
+ // 1. Role + name selector (highest priority for semantic elements)
3887
+ if (role && name) {
3888
+ const escapedName = name.replace(/'/g, "\\\\'");
3889
+ candidates.push({
3890
+ selector: "role=" + role + "[name='" + escapedName + "']",
3891
+ quality: 'role-name'
3892
+ });
3893
+ }
3894
+
3895
+ // 2. Text-based selector (for buttons, links, menuitems)
3896
+ if (name && ['button', 'link', 'menuitem'].includes(role)) {
3897
+ candidates.push({
3898
+ selector: "text=" + name,
3899
+ quality: 'text'
3900
+ });
3901
+ }
3902
+
3903
+ // 3. aria-label attribute selector
3904
+ const ariaLabel = el.getAttribute('aria-label');
3905
+ if (ariaLabel) {
3906
+ const escaped = ariaLabel.replace(/"/g, '\\\\"');
3907
+ candidates.push({
3908
+ selector: '[aria-label="' + escaped + '"]',
3909
+ quality: 'aria-label'
3910
+ });
3911
+ }
3912
+
3913
+ // 4. Stable attributes (testid, name)
3791
3914
  const stableAttr = getStableAttrSelector(el);
3792
3915
  if (stableAttr) {
3793
3916
  candidates.push({ selector: stableAttr, quality: 'stable-attr' });
3794
3917
  }
3795
3918
 
3796
- // 2. ID selector
3919
+ // 5. ID selector
3797
3920
  const idSel = getIdSelector(el);
3798
3921
  if (idSel) {
3799
3922
  candidates.push({ selector: idSel, quality: 'id' });
3800
3923
  }
3801
3924
 
3802
- // 3. CSS path (fallback)
3925
+ // 6. CSS path (fallback)
3803
3926
  const cssPath = buildCssPath(el);
3804
3927
  if (cssPath) {
3805
3928
  candidates.push({ selector: cssPath, quality: 'css-path' });
@@ -3808,6 +3931,128 @@ var RECORDER_SCRIPT = `(function() {
3808
3931
  return candidates;
3809
3932
  }
3810
3933
 
3934
+ // Compute accessible name per W3C AccName spec
3935
+ // Priority: aria-labelledby > aria-label > label > title > content > alt > placeholder
3936
+ function getAccessibleName(el) {
3937
+ if (!el || el.nodeType !== 1) return null;
3938
+
3939
+ // 1. aria-labelledby
3940
+ const labelledBy = el.getAttribute('aria-labelledby');
3941
+ if (labelledBy) {
3942
+ const labels = labelledBy.split(/\\s+/)
3943
+ .map(function(id) {
3944
+ const ref = document.getElementById(id);
3945
+ return ref ? ref.textContent : null;
3946
+ })
3947
+ .filter(Boolean);
3948
+ if (labels.length) return labels.join(' ').trim().slice(0, 100);
3949
+ }
3950
+
3951
+ // 2. aria-label
3952
+ const ariaLabel = el.getAttribute('aria-label');
3953
+ if (ariaLabel) return ariaLabel.trim().slice(0, 100);
3954
+
3955
+ // 3. Native <label> for form elements
3956
+ if (el.labels && el.labels.length) {
3957
+ const labelTexts = Array.from(el.labels)
3958
+ .map(function(l) { return l.textContent; })
3959
+ .filter(Boolean);
3960
+ if (labelTexts.length) return labelTexts.join(' ').trim().slice(0, 100);
3961
+ }
3962
+
3963
+ // 4. title attribute
3964
+ const title = el.getAttribute('title');
3965
+ if (title) return title.trim().slice(0, 100);
3966
+
3967
+ // 5. Content for buttons, links, summary
3968
+ const tag = el.tagName.toLowerCase();
3969
+ const role = el.getAttribute('role');
3970
+ if (['button', 'a', 'summary'].includes(tag) || role === 'button' || role === 'link' || role === 'menuitem') {
3971
+ const text = (el.textContent || '').trim();
3972
+ if (text) return text.slice(0, 100);
3973
+ }
3974
+
3975
+ // 6. alt for images
3976
+ if (tag === 'img') {
3977
+ const alt = el.getAttribute('alt');
3978
+ if (alt) return alt.trim().slice(0, 100);
3979
+ }
3980
+
3981
+ // 7. placeholder for inputs
3982
+ if (['input', 'textarea'].includes(tag)) {
3983
+ const placeholder = el.getAttribute('placeholder');
3984
+ if (placeholder) return placeholder.trim().slice(0, 100);
3985
+ }
3986
+
3987
+ return null;
3988
+ }
3989
+
3990
+ // Get explicit ARIA role or implicit role from HTML tag
3991
+ function getRole(el) {
3992
+ if (!el || el.nodeType !== 1) return null;
3993
+
3994
+ // 1. Explicit role attribute
3995
+ const explicitRole = el.getAttribute('role');
3996
+ if (explicitRole) return explicitRole;
3997
+
3998
+ // 2. Implicit role from tag/type
3999
+ const tag = el.tagName.toLowerCase();
4000
+ const type = (el.getAttribute('type') || '').toLowerCase();
4001
+
4002
+ // Input types to roles
4003
+ if (tag === 'input') {
4004
+ var inputRoles = {
4005
+ 'button': 'button',
4006
+ 'submit': 'button',
4007
+ 'reset': 'button',
4008
+ 'image': 'button',
4009
+ 'checkbox': 'checkbox',
4010
+ 'radio': 'radio',
4011
+ 'range': 'slider',
4012
+ 'search': 'searchbox'
4013
+ };
4014
+ if (inputRoles[type]) return inputRoles[type];
4015
+ // text, email, tel, url, number, password all map to textbox
4016
+ return 'textbox';
4017
+ }
4018
+
4019
+ // Other tags with implicit roles
4020
+ var tagRoles = {
4021
+ 'button': 'button',
4022
+ 'select': 'combobox',
4023
+ 'textarea': 'textbox',
4024
+ 'nav': 'navigation',
4025
+ 'main': 'main',
4026
+ 'header': 'banner',
4027
+ 'footer': 'contentinfo',
4028
+ 'aside': 'complementary',
4029
+ 'article': 'article',
4030
+ 'ul': 'list',
4031
+ 'ol': 'list',
4032
+ 'li': 'listitem',
4033
+ 'table': 'table',
4034
+ 'tr': 'row',
4035
+ 'td': 'cell',
4036
+ 'th': 'columnheader',
4037
+ 'form': 'form',
4038
+ 'img': 'img',
4039
+ 'dialog': 'dialog',
4040
+ 'menu': 'menu',
4041
+ 'summary': 'button'
4042
+ };
4043
+ if (tagRoles[tag]) return tagRoles[tag];
4044
+
4045
+ // Anchor with href is a link
4046
+ if (tag === 'a' && el.hasAttribute('href')) return 'link';
4047
+
4048
+ // Section with aria-label or aria-labelledby is a region
4049
+ if (tag === 'section' && (el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby'))) {
4050
+ return 'region';
4051
+ }
4052
+
4053
+ return null;
4054
+ }
4055
+
3811
4056
  // Get element summary for debugging
3812
4057
  function getElementSummary(el) {
3813
4058
  if (!el || el.nodeType !== 1) return null;
@@ -3820,7 +4065,9 @@ var RECORDER_SCRIPT = `(function() {
3820
4065
  role: el.getAttribute('role') || null,
3821
4066
  ariaLabel: el.getAttribute('aria-label') || null,
3822
4067
  testid: el.getAttribute('data-testid') || null,
3823
- text: text || null
4068
+ text: text || null,
4069
+ accessibleName: getAccessibleName(el),
4070
+ computedRole: getRole(el)
3824
4071
  };
3825
4072
  }
3826
4073
 
package/dist/cli.mjs CHANGED
@@ -681,13 +681,18 @@ var INPUT_DEBOUNCE_MS = 300;
681
681
  var NAVIGATION_DEBOUNCE_MS = 500;
682
682
  function selectBestSelectors(candidates) {
683
683
  const qualityOrder = {
684
- "stable-attr": 0,
685
- id: 1,
686
- "css-path": 2
684
+ "role-name": 0,
685
+ text: 1,
686
+ "aria-label": 2,
687
+ testid: 3,
688
+ "stable-attr": 4,
689
+ id: 5,
690
+ "name-attr": 6,
691
+ "css-path": 7
687
692
  };
688
693
  const sorted = [...candidates].sort((a, b) => {
689
- const aOrder = qualityOrder[a.quality] ?? 3;
690
- const bOrder = qualityOrder[b.quality] ?? 3;
694
+ const aOrder = qualityOrder[a.quality] ?? 8;
695
+ const bOrder = qualityOrder[b.quality] ?? 8;
691
696
  return aOrder - bOrder;
692
697
  });
693
698
  const seen = /* @__PURE__ */ new Set();
@@ -700,6 +705,67 @@ function selectBestSelectors(candidates) {
700
705
  }
701
706
  return result;
702
707
  }
708
+ function generateAnnotation(event) {
709
+ const { kind, element, url } = event;
710
+ const name = element?.accessibleName || element?.text || element?.ariaLabel;
711
+ const role = element?.computedRole || element?.role || element?.tag || "";
712
+ switch (kind) {
713
+ case "click":
714
+ if (name && role) {
715
+ return `Clicked '${name}' ${role}`;
716
+ } else if (name) {
717
+ return `Clicked '${name}'`;
718
+ } else if (role) {
719
+ return `Clicked ${role}`;
720
+ }
721
+ return "Clicked element";
722
+ case "dblclick":
723
+ if (name && role) {
724
+ return `Double-clicked '${name}' ${role}`;
725
+ }
726
+ return "Double-clicked element";
727
+ case "input":
728
+ if (name) {
729
+ return `Filled '${name}' with value`;
730
+ }
731
+ return "Filled input with value";
732
+ case "change":
733
+ if (element?.type === "checkbox" || element?.type === "radio") {
734
+ const action = event.checked ? "Checked" : "Unchecked";
735
+ if (name) {
736
+ return `${action} '${name}' ${element.type}`;
737
+ }
738
+ return `${action} ${element.type}`;
739
+ }
740
+ if (element?.tag === "select") {
741
+ if (name) {
742
+ return `Selected option in '${name}'`;
743
+ }
744
+ return "Selected option";
745
+ }
746
+ if (name) {
747
+ return `Changed '${name}'`;
748
+ }
749
+ return "Changed element";
750
+ case "submit":
751
+ if (name) {
752
+ return `Submitted '${name}' form`;
753
+ }
754
+ return "Submitted form";
755
+ case "keydown":
756
+ if (event.key === "Enter") {
757
+ return "Pressed Enter";
758
+ }
759
+ return `Pressed ${event.key}`;
760
+ case "navigation":
761
+ return `Navigated to ${url}`;
762
+ default:
763
+ if (name) {
764
+ return `${kind} on '${name}'`;
765
+ }
766
+ return `${kind} on element`;
767
+ }
768
+ }
703
769
  function debounceInputEvents(events) {
704
770
  const result = [];
705
771
  for (let i = 0; i < events.length; i++) {
@@ -778,22 +844,37 @@ function insertNavigationSteps(events, startUrl) {
778
844
  }
779
845
  return result;
780
846
  }
847
+ function buildElementMeta(event) {
848
+ const el = event.element;
849
+ if (!el) return void 0;
850
+ return {
851
+ role: el.computedRole || el.role,
852
+ name: el.accessibleName || el.text || el.ariaLabel,
853
+ tag: el.tag
854
+ };
855
+ }
781
856
  function eventToStep(event) {
782
857
  const selectors = selectBestSelectors(event.selectors);
858
+ const elementMeta = buildElementMeta(event);
859
+ const annotation = generateAnnotation(event);
783
860
  switch (event.kind) {
784
861
  case "click":
785
862
  case "dblclick":
786
863
  if (selectors.length === 0) return null;
787
864
  return {
788
865
  action: "click",
789
- selector: selectors.length === 1 ? selectors[0] : selectors
866
+ selector: selectors.length === 1 ? selectors[0] : selectors,
867
+ element: elementMeta,
868
+ annotation
790
869
  };
791
870
  case "input":
792
871
  if (selectors.length === 0) return null;
793
872
  return {
794
873
  action: "fill",
795
874
  selector: selectors.length === 1 ? selectors[0] : selectors,
796
- value: event.value ?? ""
875
+ value: event.value ?? "",
876
+ element: elementMeta,
877
+ annotation
797
878
  };
798
879
  case "change": {
799
880
  if (selectors.length === 0) return null;
@@ -804,19 +885,25 @@ function eventToStep(event) {
804
885
  return {
805
886
  action: "select",
806
887
  selector: selectors.length === 1 ? selectors[0] : selectors,
807
- value: event.value ?? ""
888
+ value: event.value ?? "",
889
+ element: elementMeta,
890
+ annotation
808
891
  };
809
892
  }
810
893
  if (type === "checkbox" || type === "radio") {
811
894
  return {
812
895
  action: event.checked ? "check" : "uncheck",
813
- selector: selectors.length === 1 ? selectors[0] : selectors
896
+ selector: selectors.length === 1 ? selectors[0] : selectors,
897
+ element: elementMeta,
898
+ annotation
814
899
  };
815
900
  }
816
901
  return {
817
902
  action: "fill",
818
903
  selector: selectors.length === 1 ? selectors[0] : selectors,
819
- value: event.value ?? ""
904
+ value: event.value ?? "",
905
+ element: elementMeta,
906
+ annotation
820
907
  };
821
908
  }
822
909
  case "keydown":
@@ -825,7 +912,9 @@ function eventToStep(event) {
825
912
  return {
826
913
  action: "submit",
827
914
  selector: selectors.length === 1 ? selectors[0] : selectors,
828
- method: "enter"
915
+ method: "enter",
916
+ element: elementMeta,
917
+ annotation
829
918
  };
830
919
  }
831
920
  return null;
@@ -833,12 +922,15 @@ function eventToStep(event) {
833
922
  if (selectors.length === 0) return null;
834
923
  return {
835
924
  action: "submit",
836
- selector: selectors.length === 1 ? selectors[0] : selectors
925
+ selector: selectors.length === 1 ? selectors[0] : selectors,
926
+ element: elementMeta,
927
+ annotation
837
928
  };
838
929
  case "navigation":
839
930
  return {
840
931
  action: "goto",
841
- url: event.url
932
+ url: event.url,
933
+ annotation
842
934
  };
843
935
  default:
844
936
  return null;
@@ -981,19 +1073,50 @@ var RECORDER_SCRIPT = `(function() {
981
1073
  function getSelectorCandidates(el) {
982
1074
  const candidates = [];
983
1075
 
984
- // 1. Stable attributes (highest quality)
1076
+ // Get semantic info for role-based selectors
1077
+ const role = getRole(el);
1078
+ const name = getAccessibleName(el);
1079
+
1080
+ // 1. Role + name selector (highest priority for semantic elements)
1081
+ if (role && name) {
1082
+ const escapedName = name.replace(/'/g, "\\\\'");
1083
+ candidates.push({
1084
+ selector: "role=" + role + "[name='" + escapedName + "']",
1085
+ quality: 'role-name'
1086
+ });
1087
+ }
1088
+
1089
+ // 2. Text-based selector (for buttons, links, menuitems)
1090
+ if (name && ['button', 'link', 'menuitem'].includes(role)) {
1091
+ candidates.push({
1092
+ selector: "text=" + name,
1093
+ quality: 'text'
1094
+ });
1095
+ }
1096
+
1097
+ // 3. aria-label attribute selector
1098
+ const ariaLabel = el.getAttribute('aria-label');
1099
+ if (ariaLabel) {
1100
+ const escaped = ariaLabel.replace(/"/g, '\\\\"');
1101
+ candidates.push({
1102
+ selector: '[aria-label="' + escaped + '"]',
1103
+ quality: 'aria-label'
1104
+ });
1105
+ }
1106
+
1107
+ // 4. Stable attributes (testid, name)
985
1108
  const stableAttr = getStableAttrSelector(el);
986
1109
  if (stableAttr) {
987
1110
  candidates.push({ selector: stableAttr, quality: 'stable-attr' });
988
1111
  }
989
1112
 
990
- // 2. ID selector
1113
+ // 5. ID selector
991
1114
  const idSel = getIdSelector(el);
992
1115
  if (idSel) {
993
1116
  candidates.push({ selector: idSel, quality: 'id' });
994
1117
  }
995
1118
 
996
- // 3. CSS path (fallback)
1119
+ // 6. CSS path (fallback)
997
1120
  const cssPath = buildCssPath(el);
998
1121
  if (cssPath) {
999
1122
  candidates.push({ selector: cssPath, quality: 'css-path' });
@@ -1002,6 +1125,128 @@ var RECORDER_SCRIPT = `(function() {
1002
1125
  return candidates;
1003
1126
  }
1004
1127
 
1128
+ // Compute accessible name per W3C AccName spec
1129
+ // Priority: aria-labelledby > aria-label > label > title > content > alt > placeholder
1130
+ function getAccessibleName(el) {
1131
+ if (!el || el.nodeType !== 1) return null;
1132
+
1133
+ // 1. aria-labelledby
1134
+ const labelledBy = el.getAttribute('aria-labelledby');
1135
+ if (labelledBy) {
1136
+ const labels = labelledBy.split(/\\s+/)
1137
+ .map(function(id) {
1138
+ const ref = document.getElementById(id);
1139
+ return ref ? ref.textContent : null;
1140
+ })
1141
+ .filter(Boolean);
1142
+ if (labels.length) return labels.join(' ').trim().slice(0, 100);
1143
+ }
1144
+
1145
+ // 2. aria-label
1146
+ const ariaLabel = el.getAttribute('aria-label');
1147
+ if (ariaLabel) return ariaLabel.trim().slice(0, 100);
1148
+
1149
+ // 3. Native <label> for form elements
1150
+ if (el.labels && el.labels.length) {
1151
+ const labelTexts = Array.from(el.labels)
1152
+ .map(function(l) { return l.textContent; })
1153
+ .filter(Boolean);
1154
+ if (labelTexts.length) return labelTexts.join(' ').trim().slice(0, 100);
1155
+ }
1156
+
1157
+ // 4. title attribute
1158
+ const title = el.getAttribute('title');
1159
+ if (title) return title.trim().slice(0, 100);
1160
+
1161
+ // 5. Content for buttons, links, summary
1162
+ const tag = el.tagName.toLowerCase();
1163
+ const role = el.getAttribute('role');
1164
+ if (['button', 'a', 'summary'].includes(tag) || role === 'button' || role === 'link' || role === 'menuitem') {
1165
+ const text = (el.textContent || '').trim();
1166
+ if (text) return text.slice(0, 100);
1167
+ }
1168
+
1169
+ // 6. alt for images
1170
+ if (tag === 'img') {
1171
+ const alt = el.getAttribute('alt');
1172
+ if (alt) return alt.trim().slice(0, 100);
1173
+ }
1174
+
1175
+ // 7. placeholder for inputs
1176
+ if (['input', 'textarea'].includes(tag)) {
1177
+ const placeholder = el.getAttribute('placeholder');
1178
+ if (placeholder) return placeholder.trim().slice(0, 100);
1179
+ }
1180
+
1181
+ return null;
1182
+ }
1183
+
1184
+ // Get explicit ARIA role or implicit role from HTML tag
1185
+ function getRole(el) {
1186
+ if (!el || el.nodeType !== 1) return null;
1187
+
1188
+ // 1. Explicit role attribute
1189
+ const explicitRole = el.getAttribute('role');
1190
+ if (explicitRole) return explicitRole;
1191
+
1192
+ // 2. Implicit role from tag/type
1193
+ const tag = el.tagName.toLowerCase();
1194
+ const type = (el.getAttribute('type') || '').toLowerCase();
1195
+
1196
+ // Input types to roles
1197
+ if (tag === 'input') {
1198
+ var inputRoles = {
1199
+ 'button': 'button',
1200
+ 'submit': 'button',
1201
+ 'reset': 'button',
1202
+ 'image': 'button',
1203
+ 'checkbox': 'checkbox',
1204
+ 'radio': 'radio',
1205
+ 'range': 'slider',
1206
+ 'search': 'searchbox'
1207
+ };
1208
+ if (inputRoles[type]) return inputRoles[type];
1209
+ // text, email, tel, url, number, password all map to textbox
1210
+ return 'textbox';
1211
+ }
1212
+
1213
+ // Other tags with implicit roles
1214
+ var tagRoles = {
1215
+ 'button': 'button',
1216
+ 'select': 'combobox',
1217
+ 'textarea': 'textbox',
1218
+ 'nav': 'navigation',
1219
+ 'main': 'main',
1220
+ 'header': 'banner',
1221
+ 'footer': 'contentinfo',
1222
+ 'aside': 'complementary',
1223
+ 'article': 'article',
1224
+ 'ul': 'list',
1225
+ 'ol': 'list',
1226
+ 'li': 'listitem',
1227
+ 'table': 'table',
1228
+ 'tr': 'row',
1229
+ 'td': 'cell',
1230
+ 'th': 'columnheader',
1231
+ 'form': 'form',
1232
+ 'img': 'img',
1233
+ 'dialog': 'dialog',
1234
+ 'menu': 'menu',
1235
+ 'summary': 'button'
1236
+ };
1237
+ if (tagRoles[tag]) return tagRoles[tag];
1238
+
1239
+ // Anchor with href is a link
1240
+ if (tag === 'a' && el.hasAttribute('href')) return 'link';
1241
+
1242
+ // Section with aria-label or aria-labelledby is a region
1243
+ if (tag === 'section' && (el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby'))) {
1244
+ return 'region';
1245
+ }
1246
+
1247
+ return null;
1248
+ }
1249
+
1005
1250
  // Get element summary for debugging
1006
1251
  function getElementSummary(el) {
1007
1252
  if (!el || el.nodeType !== 1) return null;
@@ -1014,7 +1259,9 @@ var RECORDER_SCRIPT = `(function() {
1014
1259
  role: el.getAttribute('role') || null,
1015
1260
  ariaLabel: el.getAttribute('aria-label') || null,
1016
1261
  testid: el.getAttribute('data-testid') || null,
1017
- text: text || null
1262
+ text: text || null,
1263
+ accessibleName: getAccessibleName(el),
1264
+ computedRole: getRole(el)
1018
1265
  };
1019
1266
  }
1020
1267
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-pilot",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Lightweight CDP-based browser automation for Node.js, Bun, and Cloudflare Workers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",