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.
- package/dist/cli.cjs +264 -17
- package/dist/cli.mjs +264 -17
- 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
|
-
"
|
|
3491
|
-
|
|
3492
|
-
"
|
|
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] ??
|
|
3496
|
-
const bOrder = qualityOrder[b.quality] ??
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
"
|
|
685
|
-
|
|
686
|
-
"
|
|
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] ??
|
|
690
|
-
const bOrder = qualityOrder[b.quality] ??
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|