@piwitests/reporter 0.20.0 → 0.22.0
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/README.md +12 -0
- package/dist/cli/index.js +653 -10
- package/dist/global-setup-module.js +26 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1738 -1403
- package/dist/internal/capture/capture-fixtures.d.ts +3 -56
- package/dist/internal/capture/capture-fixtures.js +1658 -1349
- package/dist/internal/capture/locator-healing.js +75 -27
- package/dist/internal/capture/pick-on-failure.d.ts +6 -125
- package/dist/internal/capture/pick-on-failure.js +1108 -940
- package/package.json +4 -2
- package/templates/skills/apply-locator-healing/SKILL.md +32 -0
- package/templates/skills/investigate-failure/SKILL.md +36 -0
- package/templates/skills/setup-piwi/SKILL.md +62 -0
- package/templates/skills/stabilize-flaky-tests/SKILL.md +36 -0
package/dist/index.js
CHANGED
|
@@ -245,6 +245,32 @@ var HttpClient = class {
|
|
|
245
245
|
this.serverUrl = serverUrl;
|
|
246
246
|
this.logger = logger;
|
|
247
247
|
this.timeout = timeout;
|
|
248
|
+
this.warnIfInsecureTransport();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Warn once when the dashboard URL is plaintext `http://` to a non-loopback
|
|
252
|
+
* host. The reporter sends the API key as a Bearer token — and uploads
|
|
253
|
+
* captured test data, including request URLs whose query strings the server
|
|
254
|
+
* only strips after receipt — over this connection, so a non-TLS remote
|
|
255
|
+
* endpoint exposes the key and payloads on the wire. Loopback (localhost /
|
|
256
|
+
* 127.0.0.1 / ::1) is exempt: that traffic never leaves the machine. This
|
|
257
|
+
* warns rather than refuses so internal HTTP deployments (e.g. behind a VPN)
|
|
258
|
+
* still work, but the exposure is no longer silent.
|
|
259
|
+
*/
|
|
260
|
+
warnIfInsecureTransport() {
|
|
261
|
+
let url;
|
|
262
|
+
try {
|
|
263
|
+
url = new import_node_url.URL(this.serverUrl);
|
|
264
|
+
} catch {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (url.protocol !== "http:") return;
|
|
268
|
+
const host = url.hostname.toLowerCase();
|
|
269
|
+
const isLoopback = host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
270
|
+
if (isLoopback) return;
|
|
271
|
+
this.logger.warn(
|
|
272
|
+
`Dashboard URL ${this.serverUrl} uses plaintext http:// \u2014 the API key and captured test data are transmitted unencrypted. Use https:// for any non-local server.`
|
|
273
|
+
);
|
|
248
274
|
}
|
|
249
275
|
/** Base URL of the Piwi Dashboard server this client talks to (used to print run links). */
|
|
250
276
|
get baseUrl() {
|
|
@@ -1572,7 +1598,7 @@ var StreamManager = class {
|
|
|
1572
1598
|
}
|
|
1573
1599
|
};
|
|
1574
1600
|
|
|
1575
|
-
// ../
|
|
1601
|
+
// ../core/src/step-analysis.ts
|
|
1576
1602
|
function categorizeStep(title, pwCategory) {
|
|
1577
1603
|
if (!title) return "other";
|
|
1578
1604
|
if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
|
|
@@ -2016,7 +2042,7 @@ function wrapConfig(config, piwiOptions) {
|
|
|
2016
2042
|
};
|
|
2017
2043
|
}
|
|
2018
2044
|
|
|
2019
|
-
// ../
|
|
2045
|
+
// ../core/src/status-classify.ts
|
|
2020
2046
|
function mergeAnnotations(test, result) {
|
|
2021
2047
|
const out = [];
|
|
2022
2048
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2036,7 +2062,7 @@ function classifyStatus(rawStatus, annotations) {
|
|
|
2036
2062
|
return intentional ? "skipped" : "didnotrun";
|
|
2037
2063
|
}
|
|
2038
2064
|
|
|
2039
|
-
// ../
|
|
2065
|
+
// ../core/src/test-meta.ts
|
|
2040
2066
|
var PIWI_ANNOTATION_PREFIX = "piwi:";
|
|
2041
2067
|
var TEST_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
2042
2068
|
var MAX_TEST_TAGS = 20;
|
|
@@ -2123,7 +2149,7 @@ function collectTestMetadata(annotations) {
|
|
|
2123
2149
|
// src/internal/collect/error-text.ts
|
|
2124
2150
|
var path12 = __toESM(require("path"));
|
|
2125
2151
|
|
|
2126
|
-
// ../
|
|
2152
|
+
// ../core/src/error-text.ts
|
|
2127
2153
|
function joinErrorMessages(errors) {
|
|
2128
2154
|
if (!errors || errors.length === 0) return null;
|
|
2129
2155
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2766,1322 +2792,1775 @@ var PiwiDashboardReporter = class {
|
|
|
2766
2792
|
// src/internal/capture/capture-fixtures.ts
|
|
2767
2793
|
var import_node_zlib = require("zlib");
|
|
2768
2794
|
|
|
2769
|
-
// src/
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
if (!ariaSnapshot) return [];
|
|
2777
|
-
const out = [];
|
|
2778
|
-
for (const line of ariaSnapshot.split("\n")) {
|
|
2779
|
-
const m = line.match(/^\s*-\s+([a-z]+)(?:\s+"((?:[^"\\]|\\.)*)")?/i);
|
|
2780
|
-
if (!m) continue;
|
|
2781
|
-
const role = m[1];
|
|
2782
|
-
const name = m[2] != null ? m[2].replace(/\\(.)/g, "$1") : null;
|
|
2783
|
-
if (!name && (role === "generic" || role === "group" || role === "list" || role === "paragraph")) continue;
|
|
2784
|
-
const levelMatch = line.slice(m[0].length).match(/\[level=(\d+)\]/);
|
|
2785
|
-
const level = levelMatch ? Number(levelMatch[1]) : null;
|
|
2786
|
-
out.push({ role, name, level });
|
|
2787
|
-
}
|
|
2788
|
-
return out;
|
|
2789
|
-
}
|
|
2790
|
-
function textSimilarity(a, b) {
|
|
2791
|
-
const tok = (s) => new Set(
|
|
2792
|
-
(s ?? "").toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean)
|
|
2793
|
-
);
|
|
2794
|
-
const sa = tok(a);
|
|
2795
|
-
const sb = tok(b);
|
|
2796
|
-
if (sa.size === 0 && sb.size === 0) return 1;
|
|
2797
|
-
if (sa.size === 0 || sb.size === 0) return 0;
|
|
2798
|
-
let common = 0;
|
|
2799
|
-
for (const t of sa) if (sb.has(t)) common++;
|
|
2800
|
-
return 2 * common / (sa.size + sb.size);
|
|
2801
|
-
}
|
|
2802
|
-
function fingerprintPresent(fp, candidates) {
|
|
2803
|
-
if (!fp.name) return false;
|
|
2804
|
-
return candidates.some(
|
|
2805
|
-
(c) => (!fp.role || c.role === fp.role) && textSimilarity(c.name, fp.name) >= PRESENT_SIMILARITY
|
|
2806
|
-
);
|
|
2807
|
-
}
|
|
2808
|
-
function matchRenamedElement(fp, candidates) {
|
|
2809
|
-
if (candidates.length === 0) return null;
|
|
2810
|
-
const sameRole = fp.role ? candidates.filter((c) => c.role === fp.role) : candidates;
|
|
2811
|
-
if (sameRole.length === 0) return null;
|
|
2812
|
-
let pool = sameRole;
|
|
2813
|
-
if (fp.level != null) {
|
|
2814
|
-
const sameLevel = sameRole.filter((c) => c.level === fp.level);
|
|
2815
|
-
if (sameLevel.length > 0) pool = sameLevel;
|
|
2816
|
-
}
|
|
2817
|
-
if (pool.length === 1) {
|
|
2818
|
-
return { candidate: pool[0], confidence: 0.7 };
|
|
2795
|
+
// ../picker-dom/src/probe.ts
|
|
2796
|
+
function probeElementAttrs(el, arg) {
|
|
2797
|
+
const { keep, tagRoles, inputRoles, roleSources, includeStructural, includeLabelText } = arg;
|
|
2798
|
+
const attrMap = {};
|
|
2799
|
+
for (const key of keep) {
|
|
2800
|
+
const v = el.getAttribute(key) ?? el[key];
|
|
2801
|
+
attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
|
|
2819
2802
|
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2803
|
+
const r = el.getBoundingClientRect();
|
|
2804
|
+
const selectorCounts = {};
|
|
2805
|
+
try {
|
|
2806
|
+
const doc = el.ownerDocument;
|
|
2807
|
+
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
2808
|
+
const count = (sel) => {
|
|
2809
|
+
try {
|
|
2810
|
+
return doc.querySelectorAll(sel).length;
|
|
2811
|
+
} catch {
|
|
2812
|
+
return void 0;
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
if (attrMap["data-testid"]) {
|
|
2816
|
+
selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
|
|
2817
|
+
}
|
|
2818
|
+
if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
|
|
2819
|
+
if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
|
|
2820
|
+
if (attrMap["placeholder"]) {
|
|
2821
|
+
selectorCounts.placeholder = count(`[placeholder=${JSON.stringify(attrMap["placeholder"])}]`);
|
|
2822
|
+
}
|
|
2823
|
+
if (attrMap["alt"]) selectorCounts.alt = count(`[alt=${JSON.stringify(attrMap["alt"])}]`);
|
|
2824
|
+
if (attrMap["title"]) selectorCounts.title = count(`[title=${JSON.stringify(attrMap["title"])}]`);
|
|
2825
|
+
const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
|
|
2826
|
+
if (classList.length > 0) {
|
|
2827
|
+
const classCounts = {};
|
|
2828
|
+
for (const cls of classList) {
|
|
2829
|
+
const n = count(`.${cssEsc(cls)}`);
|
|
2830
|
+
if (n !== void 0) classCounts[cls] = n;
|
|
2831
|
+
}
|
|
2832
|
+
selectorCounts.classes = classCounts;
|
|
2827
2833
|
}
|
|
2834
|
+
} catch {
|
|
2828
2835
|
}
|
|
2829
|
-
|
|
2830
|
-
const
|
|
2831
|
-
if (
|
|
2832
|
-
|
|
2833
|
-
|
|
2836
|
+
let rolePosition = null;
|
|
2837
|
+
const ancestors = [];
|
|
2838
|
+
if (includeStructural && tagRoles && inputRoles && roleSources) {
|
|
2839
|
+
try {
|
|
2840
|
+
const doc = el.ownerDocument;
|
|
2841
|
+
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
2842
|
+
const count = (sel) => {
|
|
2843
|
+
try {
|
|
2844
|
+
return doc.querySelectorAll(sel).length;
|
|
2845
|
+
} catch {
|
|
2846
|
+
return void 0;
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
const roleMemo = /* @__PURE__ */ new Map();
|
|
2850
|
+
const textMemo = /* @__PURE__ */ new Map();
|
|
2851
|
+
const roleOf = (n) => {
|
|
2852
|
+
const cached = roleMemo.get(n);
|
|
2853
|
+
if (cached !== void 0) return cached;
|
|
2854
|
+
let role;
|
|
2855
|
+
const explicit = n.getAttribute("role");
|
|
2856
|
+
if (explicit) {
|
|
2857
|
+
role = explicit;
|
|
2858
|
+
} else {
|
|
2859
|
+
const tag = (n.tagName || "").toLowerCase();
|
|
2860
|
+
if (tag === "input") role = inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
2861
|
+
else if (tag === "select") role = n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
2862
|
+
else if (tag === "a") role = n.getAttribute("href") != null ? "link" : null;
|
|
2863
|
+
else role = tagRoles[tag] ?? null;
|
|
2864
|
+
}
|
|
2865
|
+
roleMemo.set(n, role);
|
|
2866
|
+
return role;
|
|
2867
|
+
};
|
|
2868
|
+
const levelOf = (n) => {
|
|
2869
|
+
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
2870
|
+
if (m) return Number(m[1]);
|
|
2871
|
+
const al = n.getAttribute("aria-level");
|
|
2872
|
+
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
2873
|
+
};
|
|
2874
|
+
const targetRole = roleOf(el);
|
|
2875
|
+
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
2876
|
+
const nameOf = (n) => {
|
|
2877
|
+
const al = n.getAttribute("aria-label");
|
|
2878
|
+
if (al) return al;
|
|
2879
|
+
const txt = (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
2880
|
+
if (txt) return txt;
|
|
2881
|
+
return n.getAttribute("title") || n.getAttribute("placeholder") || null;
|
|
2882
|
+
};
|
|
2883
|
+
const targetName = nameOf(el);
|
|
2884
|
+
const targetText = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
2885
|
+
const textNeedle = targetText ? targetText.toLowerCase() : null;
|
|
2886
|
+
const normText = (n) => {
|
|
2887
|
+
const cached = textMemo.get(n);
|
|
2888
|
+
if (cached !== void 0) return cached;
|
|
2889
|
+
const text = (n.textContent || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
2890
|
+
textMemo.set(n, text);
|
|
2891
|
+
return text;
|
|
2892
|
+
};
|
|
2893
|
+
const TEXT_COUNT_CAP = 2;
|
|
2894
|
+
const countTextOwners = (root, cap) => {
|
|
2895
|
+
if (!textNeedle) return -1;
|
|
2896
|
+
const els = root.querySelectorAll("*");
|
|
2897
|
+
if (els.length > cap) return -1;
|
|
2898
|
+
let total = 0;
|
|
2899
|
+
for (let i = 0; i < els.length; i++) {
|
|
2900
|
+
const n = els[i];
|
|
2901
|
+
if (normText(n).indexOf(textNeedle) === -1) continue;
|
|
2902
|
+
let deeper = false;
|
|
2903
|
+
const kids = n.children;
|
|
2904
|
+
for (let j = 0; j < kids.length; j++) {
|
|
2905
|
+
if (normText(kids[j]).indexOf(textNeedle) !== -1) {
|
|
2906
|
+
deeper = true;
|
|
2907
|
+
break;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (!deeper) total++;
|
|
2911
|
+
if (total >= TEXT_COUNT_CAP) return total;
|
|
2912
|
+
}
|
|
2913
|
+
return total;
|
|
2914
|
+
};
|
|
2915
|
+
const nodes = doc.querySelectorAll(roleSources);
|
|
2916
|
+
const nodesUsable = nodes.length <= 4e3;
|
|
2917
|
+
const rolesUsable = !!targetRole && nodesUsable;
|
|
2918
|
+
if (rolesUsable) {
|
|
2919
|
+
let roleCountAll = 0;
|
|
2920
|
+
let index = -1;
|
|
2921
|
+
let levelCount = 0;
|
|
2922
|
+
let roleNameCount = 0;
|
|
2923
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
2924
|
+
const n = nodes[i];
|
|
2925
|
+
if (roleOf(n) !== targetRole) continue;
|
|
2926
|
+
if (n === el) index = roleCountAll;
|
|
2927
|
+
roleCountAll++;
|
|
2928
|
+
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
2929
|
+
if (targetName != null && nameOf(n) === targetName) roleNameCount++;
|
|
2930
|
+
}
|
|
2931
|
+
if (targetName != null) selectorCounts.roleName = roleNameCount;
|
|
2932
|
+
if (index !== -1) {
|
|
2933
|
+
rolePosition = {
|
|
2934
|
+
role: targetRole,
|
|
2935
|
+
count: roleCountAll,
|
|
2936
|
+
index,
|
|
2937
|
+
...targetLevel != null ? { levelCount } : {}
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
if (textNeedle) {
|
|
2942
|
+
const textCount = countTextOwners(doc.body || doc.documentElement, 4e3);
|
|
2943
|
+
if (textCount >= 0) selectorCounts.text = textCount;
|
|
2944
|
+
}
|
|
2945
|
+
if (rolesUsable || textNeedle) {
|
|
2946
|
+
const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table", "li", "tr"];
|
|
2947
|
+
const NOISY_DATA_ATTR = /^data-(v-[0-9a-f]+|reactid|react-checksum|svelte-\w+|ng-\w+|ember\w*)$/i;
|
|
2948
|
+
const POSITIONAL_DATA_ATTR = /^data-(index|idx|i|key|row|rownum|col|column|position|pos|order|sort|offset|page)$/i;
|
|
2949
|
+
const TEST_DATA_ATTRS2 = ["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"];
|
|
2950
|
+
const usableDataAttr = (name, value) => {
|
|
2951
|
+
if (!name || name.slice(0, 5) !== "data-" || name === "data-testid") return false;
|
|
2952
|
+
if (NOISY_DATA_ATTR.test(name) || POSITIONAL_DATA_ATTR.test(name)) return false;
|
|
2953
|
+
return !!value && value.length <= 120;
|
|
2954
|
+
};
|
|
2955
|
+
const stableDataAttr = (n) => {
|
|
2956
|
+
const attrs = n.attributes;
|
|
2957
|
+
if (!attrs) return null;
|
|
2958
|
+
let fallback = null;
|
|
2959
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
2960
|
+
const name = attrs[i].name;
|
|
2961
|
+
const value = attrs[i].value;
|
|
2962
|
+
if (!usableDataAttr(name, value)) continue;
|
|
2963
|
+
if (TEST_DATA_ATTRS2.indexOf(name.toLowerCase()) !== -1) return { name, value };
|
|
2964
|
+
if (!fallback) fallback = { name, value };
|
|
2965
|
+
}
|
|
2966
|
+
return fallback;
|
|
2967
|
+
};
|
|
2968
|
+
const docRoleCount = (role) => {
|
|
2969
|
+
let c = 0;
|
|
2970
|
+
for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
|
|
2971
|
+
return c;
|
|
2972
|
+
};
|
|
2973
|
+
const rawText = (n) => (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
2974
|
+
const namesRatherThanReports = (t) => {
|
|
2975
|
+
const letters = t.replace(/[^A-Za-z]/g, "").length;
|
|
2976
|
+
if (letters < 2) return false;
|
|
2977
|
+
const digits = t.replace(/[^0-9]/g, "").length;
|
|
2978
|
+
return digits <= letters;
|
|
2979
|
+
};
|
|
2980
|
+
const usableDiscriminator = (t) => !!t && t.length <= 60 && t !== targetText && namesRatherThanReports(t);
|
|
2981
|
+
const discriminatingText = (anc) => {
|
|
2982
|
+
const heading = anc.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
|
|
2983
|
+
if (heading) {
|
|
2984
|
+
const t = rawText(heading);
|
|
2985
|
+
if (usableDiscriminator(t)) return t;
|
|
2986
|
+
}
|
|
2987
|
+
const els = anc.querySelectorAll("*");
|
|
2988
|
+
if (els.length > 200) return null;
|
|
2989
|
+
for (let i = 0; i < els.length; i++) {
|
|
2990
|
+
const n = els[i];
|
|
2991
|
+
if (n === el || n.children.length > 0) continue;
|
|
2992
|
+
const t = rawText(n);
|
|
2993
|
+
if (!usableDiscriminator(t)) continue;
|
|
2994
|
+
return t;
|
|
2995
|
+
}
|
|
2996
|
+
return null;
|
|
2997
|
+
};
|
|
2998
|
+
const filterMatchCount = (role, text) => {
|
|
2999
|
+
const needle = text.toLowerCase();
|
|
3000
|
+
let c = 0;
|
|
3001
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3002
|
+
if (roleOf(nodes[i]) !== role) continue;
|
|
3003
|
+
if (normText(nodes[i]).indexOf(needle) !== -1) c++;
|
|
3004
|
+
}
|
|
3005
|
+
return c;
|
|
3006
|
+
};
|
|
3007
|
+
let node = el.parentElement;
|
|
3008
|
+
let depth = 0;
|
|
3009
|
+
while (node && depth < 12 && ancestors.length < 4) {
|
|
3010
|
+
depth++;
|
|
3011
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
3012
|
+
if (tag === "body" || tag === "html") break;
|
|
3013
|
+
const testId = node.getAttribute("data-testid");
|
|
3014
|
+
const id = node.getAttribute("id");
|
|
3015
|
+
const explicitRole = node.getAttribute("role");
|
|
3016
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
3017
|
+
const anchorRole = explicitRole || (CONTAINER_TAGS.includes(tag) ? tagRoles[tag] : null) || null;
|
|
3018
|
+
const dataAttr = stableDataAttr(node);
|
|
3019
|
+
if (testId || id || anchorRole || ariaLabel || dataAttr) {
|
|
3020
|
+
let scopedRoleCount = -1;
|
|
3021
|
+
if (rolesUsable) {
|
|
3022
|
+
const scoped = node.querySelectorAll(roleSources);
|
|
3023
|
+
if (scoped.length <= 2e3) {
|
|
3024
|
+
scopedRoleCount = 0;
|
|
3025
|
+
for (let i = 0; i < scoped.length; i++) {
|
|
3026
|
+
const n = scoped[i];
|
|
3027
|
+
if (roleOf(n) !== targetRole) continue;
|
|
3028
|
+
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
3029
|
+
scopedRoleCount++;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const scopedTextCount = countTextOwners(node, 2e3);
|
|
3034
|
+
let filterText = null;
|
|
3035
|
+
try {
|
|
3036
|
+
if (anchorRole && nodesUsable) filterText = discriminatingText(node);
|
|
3037
|
+
} catch {
|
|
3038
|
+
filterText = null;
|
|
3039
|
+
}
|
|
3040
|
+
ancestors.push({
|
|
3041
|
+
tag,
|
|
3042
|
+
depth,
|
|
3043
|
+
testId: testId || null,
|
|
3044
|
+
id: id || null,
|
|
3045
|
+
role: explicitRole || null,
|
|
3046
|
+
ariaLabel: ariaLabel || null,
|
|
3047
|
+
...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
|
|
3048
|
+
...scopedTextCount >= 0 ? { scopedTextCount } : {},
|
|
3049
|
+
...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
|
|
3050
|
+
...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
3051
|
+
...anchorRole && nodesUsable ? { roleCount: docRoleCount(anchorRole) } : {},
|
|
3052
|
+
...filterText ? { filterText, filterRoleCount: filterMatchCount(anchorRole, filterText) } : {},
|
|
3053
|
+
...dataAttr ? { dataAttr, dataAttrCount: count(`[${dataAttr.name}=${JSON.stringify(dataAttr.value)}]`) } : {}
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
node = node.parentElement;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
} catch {
|
|
3060
|
+
}
|
|
2834
3061
|
}
|
|
2835
|
-
|
|
3062
|
+
const hasLabel = !!(el.labels && el.labels.length > 0);
|
|
3063
|
+
const labelText = includeLabelText ? hasLabel ? (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null : null : void 0;
|
|
3064
|
+
return {
|
|
3065
|
+
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
3066
|
+
attributes: attrMap,
|
|
3067
|
+
// Collapse whitespace so multi-line text can't produce a getByText
|
|
3068
|
+
// suggestion with literal newlines in it.
|
|
3069
|
+
textContent: (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80),
|
|
3070
|
+
center: {
|
|
3071
|
+
x: Math.round(r.x + r.width / 2),
|
|
3072
|
+
y: Math.round(r.y + r.height / 2)
|
|
3073
|
+
},
|
|
3074
|
+
hasLabel,
|
|
3075
|
+
...includeLabelText ? { labelText } : {},
|
|
3076
|
+
selectorCounts,
|
|
3077
|
+
...includeStructural ? { rolePosition, ancestors } : {}
|
|
3078
|
+
};
|
|
2836
3079
|
}
|
|
2837
3080
|
|
|
2838
|
-
// ../
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
"getByTitle",
|
|
2847
|
-
"locator"
|
|
2848
|
-
];
|
|
2849
|
-
|
|
2850
|
-
// ../packages/core/src/locator-generation.ts
|
|
2851
|
-
var TAG_TO_ROLE = {
|
|
2852
|
-
a: "link",
|
|
2853
|
-
button: "button",
|
|
2854
|
-
nav: "navigation",
|
|
2855
|
-
main: "main",
|
|
2856
|
-
article: "article",
|
|
2857
|
-
section: "region",
|
|
2858
|
-
form: "form",
|
|
2859
|
-
img: "img",
|
|
2860
|
-
figure: "figure",
|
|
2861
|
-
figcaption: "caption",
|
|
2862
|
-
blockquote: "blockquote",
|
|
2863
|
-
table: "table",
|
|
2864
|
-
ul: "list",
|
|
2865
|
-
ol: "list",
|
|
2866
|
-
li: "listitem",
|
|
2867
|
-
dialog: "dialog",
|
|
2868
|
-
output: "status",
|
|
2869
|
-
progress: "progressbar",
|
|
2870
|
-
meter: "meter",
|
|
2871
|
-
textarea: "textbox",
|
|
2872
|
-
h1: "heading",
|
|
2873
|
-
h2: "heading",
|
|
2874
|
-
h3: "heading",
|
|
2875
|
-
h4: "heading",
|
|
2876
|
-
h5: "heading",
|
|
2877
|
-
h6: "heading",
|
|
2878
|
-
details: "group",
|
|
2879
|
-
summary: "button",
|
|
2880
|
-
search: "search"
|
|
2881
|
-
};
|
|
2882
|
-
var INPUT_TYPE_TO_ROLE = {
|
|
2883
|
-
button: "button",
|
|
2884
|
-
submit: "button",
|
|
2885
|
-
reset: "button",
|
|
2886
|
-
image: "button",
|
|
2887
|
-
checkbox: "checkbox",
|
|
2888
|
-
radio: "radio",
|
|
2889
|
-
range: "slider",
|
|
2890
|
-
search: "searchbox",
|
|
2891
|
-
number: "spinbutton",
|
|
2892
|
-
text: "textbox",
|
|
2893
|
-
email: "textbox",
|
|
2894
|
-
tel: "textbox",
|
|
2895
|
-
url: "textbox",
|
|
2896
|
-
password: "textbox"
|
|
2897
|
-
};
|
|
2898
|
-
var CAPTURED_ATTRIBUTES = [
|
|
2899
|
-
"id",
|
|
2900
|
-
"class",
|
|
2901
|
-
"name",
|
|
2902
|
-
"data-testid",
|
|
2903
|
-
"placeholder",
|
|
2904
|
-
"alt",
|
|
2905
|
-
"title",
|
|
2906
|
-
"aria-label",
|
|
2907
|
-
"aria-level",
|
|
2908
|
-
"role",
|
|
2909
|
-
"type",
|
|
2910
|
-
"href",
|
|
2911
|
-
"multiple"
|
|
2912
|
-
];
|
|
2913
|
-
function resolveAriaRole(attrs) {
|
|
2914
|
-
const explicit = attrs.attributes["role"];
|
|
2915
|
-
if (explicit) return explicit;
|
|
2916
|
-
const tag = attrs.tagName;
|
|
2917
|
-
if (!tag) return null;
|
|
2918
|
-
if (tag === "input") {
|
|
2919
|
-
const type = (attrs.attributes["type"] ?? "text").toLowerCase();
|
|
2920
|
-
return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
|
|
2921
|
-
}
|
|
2922
|
-
if (tag === "select") {
|
|
2923
|
-
return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
|
|
2924
|
-
}
|
|
2925
|
-
if (tag === "a") {
|
|
2926
|
-
return attrs.attributes["href"] != null ? "link" : null;
|
|
3081
|
+
// ../picker-dom/src/overlay-element.ts
|
|
3082
|
+
function installPickerOverlay(arg) {
|
|
3083
|
+
const g = globalThis;
|
|
3084
|
+
const doc = g.document;
|
|
3085
|
+
if (!doc || !doc.body) {
|
|
3086
|
+
if (arg.transport === "postMessage") g.parent.postMessage({ type: "pickerClosed" }, "*");
|
|
3087
|
+
else g.__piwiPickState = "skipped";
|
|
3088
|
+
return;
|
|
2927
3089
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
const
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
className
|
|
2947
|
-
))
|
|
2948
|
-
return 25;
|
|
2949
|
-
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
2950
|
-
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
2951
|
-
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
2952
|
-
if (className.includes("_")) return 15;
|
|
2953
|
-
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
2954
|
-
return 15;
|
|
2955
|
-
}
|
|
2956
|
-
function isAutoGenerated(value) {
|
|
2957
|
-
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
2958
|
-
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
2959
|
-
if (/^[a-z]+-\d+$/.test(value)) return true;
|
|
2960
|
-
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
2961
|
-
if (value.startsWith("ng-")) return true;
|
|
2962
|
-
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
2963
|
-
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
2964
|
-
return false;
|
|
2965
|
-
}
|
|
2966
|
-
function generateAlternatives(attrs) {
|
|
2967
|
-
const alts = [];
|
|
2968
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2969
|
-
const add = (loc) => {
|
|
2970
|
-
if (!seen.has(loc.locator)) {
|
|
2971
|
-
seen.add(loc.locator);
|
|
2972
|
-
alts.push(loc);
|
|
3090
|
+
const Z = 2147483600;
|
|
3091
|
+
const highlight = doc.createElement("div");
|
|
3092
|
+
highlight.id = "__piwi_picker_highlight";
|
|
3093
|
+
highlight.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};display:none;box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55),inset 0 0 0 1px rgba(255,255,255,.5);`;
|
|
3094
|
+
const banner = doc.createElement("div");
|
|
3095
|
+
banner.id = "__piwi_picker_banner";
|
|
3096
|
+
banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
|
|
3097
|
+
const hlTokens = (expr) => {
|
|
3098
|
+
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3099
|
+
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
3100
|
+
let html = "";
|
|
3101
|
+
let last = 0;
|
|
3102
|
+
let m;
|
|
3103
|
+
while ((m = re.exec(expr)) !== null) {
|
|
3104
|
+
if (m.index > last) html += escHtml(expr.slice(last, m.index));
|
|
3105
|
+
const color = m[1] ? "#86efac" : m[2] ? "#d8b4fe" : m[3] ? "#93c5fd" : m[4] ? "#fcd34d" : "#9ca3af";
|
|
3106
|
+
html += `<span style="color:${color}">${escHtml(m[0])}</span>`;
|
|
3107
|
+
last = re.lastIndex;
|
|
2973
3108
|
}
|
|
3109
|
+
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
3110
|
+
return html;
|
|
2974
3111
|
};
|
|
2975
|
-
const
|
|
2976
|
-
const
|
|
2977
|
-
const
|
|
2978
|
-
|
|
2979
|
-
const
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
})
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
add({
|
|
3004
|
-
locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
|
|
3005
|
-
method: "getByRole",
|
|
3006
|
-
args: withLevel({ role, name: ariaLabel }),
|
|
3007
|
-
score: 85
|
|
3008
|
-
});
|
|
3009
|
-
}
|
|
3010
|
-
if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
|
|
3011
|
-
const label = attr(attrs, "aria-label");
|
|
3012
|
-
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
3013
|
-
if (labelBacked) {
|
|
3014
|
-
add({
|
|
3015
|
-
locator: `getByLabel('${esc(accessibleName)}')`,
|
|
3016
|
-
method: "getByLabel",
|
|
3017
|
-
args: { label: accessibleName },
|
|
3018
|
-
score: 85
|
|
3019
|
-
});
|
|
3112
|
+
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
3113
|
+
const hlLocator = (expr) => `<code style="font-family:${MONO}">${hlTokens(expr)}</code>`;
|
|
3114
|
+
const head = doc.createElement("div");
|
|
3115
|
+
head.innerHTML = arg.transport === "postMessage" ? "Click an element to generate locators" : arg.failing ? `Piwi locator picker \u2014 click the element that should replace ${hlLocator(arg.failing)}` : "Piwi inspector \u2014 click any element to generate locators for it";
|
|
3116
|
+
const locatorLine = doc.createElement("div");
|
|
3117
|
+
locatorLine.id = "__piwi_picker_locator";
|
|
3118
|
+
locatorLine.style.cssText = `display:none;margin-top:7px;padding:5px 9px;border-radius:7px;background:#0b1120;border:1px solid #4c1d95;font:13.5px/1.55 ${MONO};word-break:break-word;overflow-wrap:anywhere;`;
|
|
3119
|
+
const foot = doc.createElement("div");
|
|
3120
|
+
foot.id = "__piwi_picker_foot";
|
|
3121
|
+
foot.style.cssText = "color:#9ca3af;margin-top:6px;font-size:12px;";
|
|
3122
|
+
foot.textContent = "\u2191 parent \xB7 \u2193 child \xB7 Esc skip";
|
|
3123
|
+
banner.appendChild(head);
|
|
3124
|
+
banner.appendChild(locatorLine);
|
|
3125
|
+
banner.appendChild(foot);
|
|
3126
|
+
const label = doc.createElement("div");
|
|
3127
|
+
label.id = "__piwi_picker_label";
|
|
3128
|
+
label.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(620px,92vw);background:#0b1120;color:#f9fafb;border:1px solid #7c3aed;border-radius:7px;padding:4px 8px;font:12.5px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
3129
|
+
doc.body.appendChild(highlight);
|
|
3130
|
+
doc.body.appendChild(label);
|
|
3131
|
+
doc.body.appendChild(banner);
|
|
3132
|
+
const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3133
|
+
const describe = (el) => {
|
|
3134
|
+
const tag = (el.tagName || "?").toLowerCase();
|
|
3135
|
+
const testId = el.getAttribute && el.getAttribute("data-testid");
|
|
3136
|
+
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
3137
|
+
if (el.labels && el.labels.length > 0) {
|
|
3138
|
+
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3139
|
+
if (labelText) return `getByLabel('${escJs(labelText)}')`;
|
|
3020
3140
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
})
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
3042
|
-
add({
|
|
3043
|
-
locator: `locator('${esc(selector)}')`,
|
|
3044
|
-
method: "locator",
|
|
3045
|
-
args: { selector },
|
|
3046
|
-
score: 65
|
|
3047
|
-
});
|
|
3048
|
-
}
|
|
3049
|
-
const name = attr(attrs, "name");
|
|
3050
|
-
if (name && isUnique(counts?.name)) {
|
|
3051
|
-
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
3052
|
-
add({
|
|
3053
|
-
locator: `locator('${esc(selector)}')`,
|
|
3054
|
-
method: "locator",
|
|
3055
|
-
args: { selector },
|
|
3056
|
-
score: 60
|
|
3057
|
-
});
|
|
3058
|
-
}
|
|
3059
|
-
const alt = attr(attrs, "alt");
|
|
3060
|
-
if (alt) {
|
|
3061
|
-
add({
|
|
3062
|
-
locator: `getByAltText('${esc(alt)}')`,
|
|
3063
|
-
method: "getByAltText",
|
|
3064
|
-
args: { text: alt },
|
|
3065
|
-
score: 60
|
|
3066
|
-
});
|
|
3067
|
-
}
|
|
3068
|
-
const title = attr(attrs, "title");
|
|
3069
|
-
if (title) {
|
|
3070
|
-
add({
|
|
3071
|
-
locator: `getByTitle('${esc(title)}')`,
|
|
3072
|
-
method: "getByTitle",
|
|
3073
|
-
args: { title },
|
|
3074
|
-
score: 50
|
|
3075
|
-
});
|
|
3076
|
-
}
|
|
3077
|
-
const hasOwnTestId = !!(testId && isUnique(counts?.testId));
|
|
3078
|
-
if (role && !hasOwnTestId) {
|
|
3079
|
-
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
3080
|
-
const leafArgs = withLevel({ role });
|
|
3081
|
-
let testIdAnchorDone = false;
|
|
3082
|
-
let idAnchorDone = false;
|
|
3083
|
-
let roleAnchorDone = false;
|
|
3084
|
-
for (const anc of attrs.ancestors ?? []) {
|
|
3085
|
-
if (anc.scopedRoleCount !== 1) continue;
|
|
3086
|
-
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
3087
|
-
testIdAnchorDone = true;
|
|
3088
|
-
add({
|
|
3089
|
-
locator: `getByTestId('${esc(anc.testId)}').getByRole(${rolePart})`,
|
|
3090
|
-
method: "getByRole",
|
|
3091
|
-
args: { ...leafArgs, anchorTestId: anc.testId },
|
|
3092
|
-
score: 72
|
|
3093
|
-
});
|
|
3094
|
-
}
|
|
3095
|
-
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
3096
|
-
idAnchorDone = true;
|
|
3097
|
-
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
3098
|
-
add({
|
|
3099
|
-
locator: `locator('${esc(anchorSelector)}').getByRole(${rolePart})`,
|
|
3100
|
-
method: "getByRole",
|
|
3101
|
-
args: { ...leafArgs, anchorSelector },
|
|
3102
|
-
score: 64
|
|
3103
|
-
});
|
|
3104
|
-
}
|
|
3105
|
-
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
3106
|
-
if (!roleAnchorDone && ancestorRole && ancestorRole !== role && anc.roleCount === 1) {
|
|
3107
|
-
roleAnchorDone = true;
|
|
3108
|
-
add({
|
|
3109
|
-
locator: `getByRole('${esc(ancestorRole)}').getByRole(${rolePart})`,
|
|
3110
|
-
method: "getByRole",
|
|
3111
|
-
args: { ...leafArgs, anchorRole: ancestorRole },
|
|
3112
|
-
score: 55
|
|
3113
|
-
});
|
|
3114
|
-
}
|
|
3141
|
+
const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
|
|
3142
|
+
if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
|
|
3143
|
+
const placeholder = el.getAttribute && el.getAttribute("placeholder");
|
|
3144
|
+
if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
|
|
3145
|
+
const alt = el.getAttribute && el.getAttribute("alt");
|
|
3146
|
+
if (alt) return `getByAltText('${escJs(alt)}')`;
|
|
3147
|
+
const titleAttr = el.getAttribute && el.getAttribute("title");
|
|
3148
|
+
if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
|
|
3149
|
+
if (el.id) return `locator('#${escJs(el.id)}')`;
|
|
3150
|
+
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
3151
|
+
return cls ? `locator('.${escJs(cls)}')` : tag;
|
|
3152
|
+
};
|
|
3153
|
+
const buildChain = (raw) => {
|
|
3154
|
+
const chain2 = [];
|
|
3155
|
+
let node = raw;
|
|
3156
|
+
while (node && chain2.length < 15) {
|
|
3157
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
3158
|
+
if (tag === "body" || tag === "html") break;
|
|
3159
|
+
chain2.push(node);
|
|
3160
|
+
node = node.parentElement;
|
|
3115
3161
|
}
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3162
|
+
return chain2.length ? chain2 : [raw];
|
|
3163
|
+
};
|
|
3164
|
+
const ACTIONABLE_TAGS = ["button", "a", "input", "select", "textarea", "summary", "option"];
|
|
3165
|
+
const snapIndex = (chain2) => {
|
|
3166
|
+
for (let i = 0; i < Math.min(chain2.length, 4); i++) {
|
|
3167
|
+
const el = chain2[i];
|
|
3168
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
3169
|
+
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
3170
|
+
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
|
|
3124
3171
|
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
score
|
|
3136
|
-
});
|
|
3172
|
+
return 0;
|
|
3173
|
+
};
|
|
3174
|
+
const locatorOf = (el) => {
|
|
3175
|
+
const hook = g.__piwiDescribeElement;
|
|
3176
|
+
if (typeof hook === "function") {
|
|
3177
|
+
try {
|
|
3178
|
+
const derived = hook(el);
|
|
3179
|
+
if (typeof derived === "string" && derived) return derived;
|
|
3180
|
+
} catch {
|
|
3181
|
+
}
|
|
3137
3182
|
}
|
|
3138
|
-
|
|
3139
|
-
return alts.sort((a, b) => b.score - a.score);
|
|
3140
|
-
}
|
|
3141
|
-
function approximateAccessibleName(attrs) {
|
|
3142
|
-
const a = attrs.attributes;
|
|
3143
|
-
const ariaLabel = a["aria-label"];
|
|
3144
|
-
if (ariaLabel) return ariaLabel;
|
|
3145
|
-
if (attrs.textContent) return attrs.textContent;
|
|
3146
|
-
const title = a["title"];
|
|
3147
|
-
if (title) return title;
|
|
3148
|
-
const placeholder = a["placeholder"];
|
|
3149
|
-
if (placeholder) return placeholder;
|
|
3150
|
-
return null;
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
|
-
// src/internal/capture/locator-healing.ts
|
|
3154
|
-
function dedupeSnapshotsByLocation(snaps) {
|
|
3155
|
-
const lastWithElement = /* @__PURE__ */ new Map();
|
|
3156
|
-
const lastAny = /* @__PURE__ */ new Map();
|
|
3157
|
-
snaps.forEach((s, i) => {
|
|
3158
|
-
if (!s.location) return;
|
|
3159
|
-
lastAny.set(s.location, i);
|
|
3160
|
-
if (s.element) lastWithElement.set(s.location, i);
|
|
3161
|
-
});
|
|
3162
|
-
return snaps.filter((s, i) => {
|
|
3163
|
-
if (!s.location) return true;
|
|
3164
|
-
return (lastWithElement.get(s.location) ?? lastAny.get(s.location)) === i;
|
|
3165
|
-
});
|
|
3166
|
-
}
|
|
3167
|
-
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
3168
|
-
var CHAIN_METHODS = [
|
|
3169
|
-
"first",
|
|
3170
|
-
"nth",
|
|
3171
|
-
"last",
|
|
3172
|
-
"filter",
|
|
3173
|
-
"and",
|
|
3174
|
-
"or",
|
|
3175
|
-
"locator",
|
|
3176
|
-
"getByRole",
|
|
3177
|
-
"getByTestId",
|
|
3178
|
-
"getByText",
|
|
3179
|
-
"getByLabel",
|
|
3180
|
-
"getByPlaceholder",
|
|
3181
|
-
"getByAltText",
|
|
3182
|
-
"getByTitle"
|
|
3183
|
-
];
|
|
3184
|
-
var ACTION_METHODS = [
|
|
3185
|
-
"click",
|
|
3186
|
-
"fill",
|
|
3187
|
-
"check",
|
|
3188
|
-
"uncheck",
|
|
3189
|
-
"selectOption",
|
|
3190
|
-
"dblclick",
|
|
3191
|
-
"tap",
|
|
3192
|
-
"hover",
|
|
3193
|
-
"press",
|
|
3194
|
-
"type",
|
|
3195
|
-
"pressSequentially",
|
|
3196
|
-
"clear",
|
|
3197
|
-
"setInputFiles",
|
|
3198
|
-
"dragTo",
|
|
3199
|
-
"focus",
|
|
3200
|
-
"blur",
|
|
3201
|
-
"scrollIntoViewIfNeeded",
|
|
3202
|
-
"dispatchEvent",
|
|
3203
|
-
"selectText",
|
|
3204
|
-
// Not an action, but a successful waitFor proves the element resolved — the
|
|
3205
|
-
// closest capture hook available for assertion-style usage of a locator.
|
|
3206
|
-
"waitFor"
|
|
3207
|
-
];
|
|
3208
|
-
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
3209
|
-
var EXPECT_METHOD = "_expect";
|
|
3210
|
-
var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
3211
|
-
"to.be.attached",
|
|
3212
|
-
"to.be.checked",
|
|
3213
|
-
"to.be.disabled",
|
|
3214
|
-
"to.be.editable",
|
|
3215
|
-
"to.be.empty",
|
|
3216
|
-
"to.be.enabled",
|
|
3217
|
-
"to.be.focused",
|
|
3218
|
-
"to.be.in.viewport",
|
|
3219
|
-
"to.be.readonly",
|
|
3220
|
-
"to.be.visible",
|
|
3221
|
-
"to.contain.class",
|
|
3222
|
-
"to.contain.text",
|
|
3223
|
-
"to.have.accessible.description",
|
|
3224
|
-
"to.have.accessible.error.message",
|
|
3225
|
-
"to.have.accessible.name",
|
|
3226
|
-
"to.have.attribute",
|
|
3227
|
-
"to.have.attribute.value",
|
|
3228
|
-
"to.have.class",
|
|
3229
|
-
"to.have.css",
|
|
3230
|
-
"to.have.id",
|
|
3231
|
-
"to.have.js.property",
|
|
3232
|
-
"to.have.role",
|
|
3233
|
-
"to.have.text",
|
|
3234
|
-
"to.have.value",
|
|
3235
|
-
"to.match.aria"
|
|
3236
|
-
]);
|
|
3237
|
-
function extractAccessibleName(ariaSnapshot) {
|
|
3238
|
-
if (!ariaSnapshot) return null;
|
|
3239
|
-
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
3240
|
-
if (match) return match[1];
|
|
3241
|
-
return null;
|
|
3242
|
-
}
|
|
3243
|
-
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
3244
|
-
"getByText",
|
|
3245
|
-
"getByRole",
|
|
3246
|
-
"getByLabel",
|
|
3247
|
-
"getByPlaceholder",
|
|
3248
|
-
"getByTitle",
|
|
3249
|
-
"getByAltText"
|
|
3250
|
-
]);
|
|
3251
|
-
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
3252
|
-
var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
|
|
3253
|
-
"button",
|
|
3254
|
-
"link",
|
|
3255
|
-
"heading",
|
|
3256
|
-
"menuitem",
|
|
3257
|
-
"tab",
|
|
3258
|
-
"option",
|
|
3259
|
-
"cell",
|
|
3260
|
-
"columnheader",
|
|
3261
|
-
"rowheader",
|
|
3262
|
-
"gridcell",
|
|
3263
|
-
"treeitem",
|
|
3264
|
-
"listitem",
|
|
3265
|
-
"checkbox",
|
|
3266
|
-
"radio",
|
|
3267
|
-
"switch"
|
|
3268
|
-
]);
|
|
3269
|
-
var SUGG_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
|
|
3270
|
-
function failedNameAndRole(failed) {
|
|
3271
|
-
if (failed.method === "getByRole") {
|
|
3272
|
-
const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
|
|
3273
|
-
const opts = failed.args[1];
|
|
3274
|
-
const name = opts && typeof opts.name === "string" ? opts.name : null;
|
|
3275
|
-
const level = opts && typeof opts.level === "number" ? opts.level : null;
|
|
3276
|
-
return { role, name, level };
|
|
3277
|
-
}
|
|
3278
|
-
const first = failed.args.find((a) => typeof a === "string");
|
|
3279
|
-
return { role: null, name: typeof first === "string" ? first : null, level: null };
|
|
3280
|
-
}
|
|
3281
|
-
function renderFailing(failed) {
|
|
3282
|
-
const { role, name } = failedNameAndRole(failed);
|
|
3283
|
-
if (failed.method === "getByRole") {
|
|
3284
|
-
return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
|
|
3285
|
-
}
|
|
3286
|
-
return `${failed.method}('${escAttr(name ?? "")}')`;
|
|
3287
|
-
}
|
|
3288
|
-
function freshSuggestions(candidate, failedMethod) {
|
|
3289
|
-
const out = [];
|
|
3290
|
-
const role = candidate.role;
|
|
3291
|
-
const name = candidate.name;
|
|
3292
|
-
const push = (s) => {
|
|
3293
|
-
if (!out.includes(s)) out.push(s);
|
|
3183
|
+
return describe(el);
|
|
3294
3184
|
};
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
3321
|
-
const lines = stack.split("\n");
|
|
3322
|
-
let prevWasCaptureModule = false;
|
|
3323
|
-
let selfFile = null;
|
|
3324
|
-
for (let i = 1; i < lines.length; i++) {
|
|
3325
|
-
const line = lines[i].trim();
|
|
3326
|
-
if (!line.startsWith("at")) continue;
|
|
3327
|
-
const m = line.match(/^at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
3328
|
-
if (!m) {
|
|
3329
|
-
prevWasCaptureModule = false;
|
|
3330
|
-
continue;
|
|
3185
|
+
let chain = [];
|
|
3186
|
+
let idx = 0;
|
|
3187
|
+
let lastRaw = null;
|
|
3188
|
+
let labeled = null;
|
|
3189
|
+
const placeLabel = (r) => {
|
|
3190
|
+
label.style.display = "block";
|
|
3191
|
+
const lr = label.getBoundingClientRect();
|
|
3192
|
+
const vw = g.innerWidth || doc.documentElement.clientWidth || 0;
|
|
3193
|
+
const vh = g.innerHeight || doc.documentElement.clientHeight || 0;
|
|
3194
|
+
let top = r.top - lr.height - 6;
|
|
3195
|
+
if (top < 4) top = r.bottom + 6;
|
|
3196
|
+
if (top + lr.height > vh - 4) top = Math.max(4, vh - lr.height - 4);
|
|
3197
|
+
let left = r.left;
|
|
3198
|
+
if (left + lr.width > vw - 6) left = vw - lr.width - 6;
|
|
3199
|
+
if (left < 6) left = 6;
|
|
3200
|
+
label.style.left = left + "px";
|
|
3201
|
+
label.style.top = top + "px";
|
|
3202
|
+
};
|
|
3203
|
+
const current = () => chain[idx] ?? null;
|
|
3204
|
+
const refresh = () => {
|
|
3205
|
+
const el = current();
|
|
3206
|
+
if (!el) {
|
|
3207
|
+
highlight.style.display = "none";
|
|
3208
|
+
label.style.display = "none";
|
|
3209
|
+
return;
|
|
3331
3210
|
}
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3211
|
+
const r = el.getBoundingClientRect();
|
|
3212
|
+
highlight.style.display = "block";
|
|
3213
|
+
highlight.style.left = r.left + "px";
|
|
3214
|
+
highlight.style.top = r.top + "px";
|
|
3215
|
+
highlight.style.width = r.width + "px";
|
|
3216
|
+
highlight.style.height = r.height + "px";
|
|
3217
|
+
if (el !== labeled) {
|
|
3218
|
+
labeled = el;
|
|
3219
|
+
const tag = (el.tagName || "?").toLowerCase();
|
|
3220
|
+
const locatorHtml = hlTokens(locatorOf(el));
|
|
3221
|
+
label.innerHTML = `<span style="color:#c4b5fd"><${tag}></span> ${locatorHtml}`;
|
|
3222
|
+
locatorLine.innerHTML = locatorHtml;
|
|
3223
|
+
locatorLine.style.display = "block";
|
|
3224
|
+
}
|
|
3225
|
+
placeLabel(r);
|
|
3226
|
+
foot.textContent = "click to pick \xB7 \u2191 parent \xB7 \u2193 child \xB7 Esc skip";
|
|
3227
|
+
};
|
|
3228
|
+
const stop = (e) => {
|
|
3229
|
+
e.preventDefault();
|
|
3230
|
+
e.stopImmediatePropagation();
|
|
3231
|
+
};
|
|
3232
|
+
const isOwn = (el) => el === banner || el === highlight || el === label || banner.contains && banner.contains(el) || !!(el && el.__piwiHint);
|
|
3233
|
+
let bannerDocked = "top";
|
|
3234
|
+
const dockBanner = (side) => {
|
|
3235
|
+
if (bannerDocked === side) return;
|
|
3236
|
+
bannerDocked = side;
|
|
3237
|
+
if (side === "bottom") {
|
|
3238
|
+
banner.style.top = "auto";
|
|
3239
|
+
banner.style.bottom = "12px";
|
|
3240
|
+
} else {
|
|
3241
|
+
banner.style.top = "12px";
|
|
3242
|
+
banner.style.bottom = "auto";
|
|
3336
3243
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3244
|
+
};
|
|
3245
|
+
const onMove = (e) => {
|
|
3246
|
+
const raw = e.target;
|
|
3247
|
+
if (!raw || isOwn(raw)) {
|
|
3248
|
+
highlight.style.display = "none";
|
|
3249
|
+
label.style.display = "none";
|
|
3250
|
+
return;
|
|
3341
3251
|
}
|
|
3342
|
-
if (
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3252
|
+
if (raw !== lastRaw) {
|
|
3253
|
+
lastRaw = raw;
|
|
3254
|
+
chain = buildChain(raw);
|
|
3255
|
+
idx = snapIndex(chain);
|
|
3346
3256
|
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3257
|
+
refresh();
|
|
3258
|
+
const el = current();
|
|
3259
|
+
if (el) {
|
|
3260
|
+
const r = el.getBoundingClientRect();
|
|
3261
|
+
const br = banner.getBoundingClientRect();
|
|
3262
|
+
const margin = 8;
|
|
3263
|
+
if (r.left < br.right + margin && r.right > br.left - margin && r.top < br.bottom + margin && r.bottom > br.top - margin) {
|
|
3264
|
+
dockBanner(bannerDocked === "top" ? "bottom" : "top");
|
|
3265
|
+
}
|
|
3350
3266
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3267
|
+
};
|
|
3268
|
+
const suppressed = ["mousedown", "mouseup", "pointerdown", "pointerup", "auxclick", "dblclick"];
|
|
3269
|
+
const removePickingListeners = () => {
|
|
3270
|
+
doc.removeEventListener("mousemove", onMove, true);
|
|
3271
|
+
doc.removeEventListener("click", onClick, true);
|
|
3272
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
3273
|
+
if (arg.transport === "postMessage") g.removeEventListener("message", onParentMsg, false);
|
|
3274
|
+
};
|
|
3275
|
+
const removeSuppressed = () => {
|
|
3276
|
+
for (const t of suppressed) doc.removeEventListener(t, stop, true);
|
|
3277
|
+
};
|
|
3278
|
+
const cleanup = () => {
|
|
3279
|
+
highlight.remove();
|
|
3280
|
+
label.remove();
|
|
3281
|
+
banner.remove();
|
|
3282
|
+
};
|
|
3283
|
+
const reportPicked = (el) => {
|
|
3284
|
+
removePickingListeners();
|
|
3285
|
+
highlight.style.display = "none";
|
|
3286
|
+
label.style.display = "none";
|
|
3287
|
+
if (arg.transport === "postMessage") {
|
|
3288
|
+
const probeFn = g.__piwiProbe;
|
|
3289
|
+
const attrs = typeof probeFn === "function" ? probeFn(el, arg.probeArg) : null;
|
|
3290
|
+
g.__piwiSnapshotExtras?.onPick?.();
|
|
3291
|
+
doc.addEventListener("click", stop, true);
|
|
3292
|
+
doc.addEventListener("keydown", stop, true);
|
|
3293
|
+
foot.textContent = "Analyzing element\u2026";
|
|
3294
|
+
g.parent.postMessage({ type: "elementPicked", attrs }, "*");
|
|
3295
|
+
} else {
|
|
3296
|
+
removeSuppressed();
|
|
3297
|
+
g.__piwiPickedElement = el;
|
|
3298
|
+
g.__piwiPickState = "picked";
|
|
3299
|
+
foot.textContent = "Analyzing element\u2026";
|
|
3354
3300
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3301
|
+
};
|
|
3302
|
+
const reportSkipped = () => {
|
|
3303
|
+
removePickingListeners();
|
|
3304
|
+
if (arg.transport === "postMessage") {
|
|
3305
|
+
doc.removeEventListener("click", stop, true);
|
|
3306
|
+
doc.removeEventListener("keydown", stop, true);
|
|
3307
|
+
removeSuppressed();
|
|
3308
|
+
g.__piwiSnapshotExtras?.onClose?.();
|
|
3309
|
+
cleanup();
|
|
3310
|
+
g.parent.postMessage({ type: "pickerClosed" }, "*");
|
|
3311
|
+
} else {
|
|
3312
|
+
removeSuppressed();
|
|
3313
|
+
g.__piwiPickState = "skipped";
|
|
3314
|
+
cleanup();
|
|
3358
3315
|
}
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3316
|
+
};
|
|
3317
|
+
const onClick = (e) => {
|
|
3318
|
+
stop(e);
|
|
3319
|
+
const el = current();
|
|
3320
|
+
if (!el || isOwn(e.target)) return;
|
|
3321
|
+
reportPicked(el);
|
|
3322
|
+
};
|
|
3323
|
+
const onKey = (e) => {
|
|
3324
|
+
if (e.key === "Escape") {
|
|
3325
|
+
stop(e);
|
|
3326
|
+
reportSkipped();
|
|
3327
|
+
return;
|
|
3363
3328
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3329
|
+
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
3330
|
+
stop(e);
|
|
3331
|
+
if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
|
|
3332
|
+
else idx = Math.max(idx - 1, 0);
|
|
3333
|
+
refresh();
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
const onParentMsg = (e) => {
|
|
3337
|
+
const d = e.data;
|
|
3338
|
+
if (!d || typeof d.type !== "string" || d.type !== "piwiPickerKey" || typeof d.key !== "string") return;
|
|
3339
|
+
onKey({
|
|
3340
|
+
key: d.key,
|
|
3341
|
+
preventDefault() {
|
|
3342
|
+
},
|
|
3343
|
+
stopImmediatePropagation() {
|
|
3344
|
+
}
|
|
3345
|
+
});
|
|
3346
|
+
};
|
|
3347
|
+
g.__piwiPickCleanup = cleanup;
|
|
3348
|
+
doc.addEventListener("mousemove", onMove, true);
|
|
3349
|
+
doc.addEventListener("click", onClick, true);
|
|
3350
|
+
doc.addEventListener("keydown", onKey, true);
|
|
3351
|
+
for (const t of suppressed) doc.addEventListener(t, stop, true);
|
|
3352
|
+
if (arg.transport === "postMessage") {
|
|
3353
|
+
g.addEventListener("message", onParentMsg, false);
|
|
3354
|
+
g.parent.postMessage({ type: "pickerReady" }, "*");
|
|
3367
3355
|
}
|
|
3368
|
-
return null;
|
|
3369
3356
|
}
|
|
3370
3357
|
|
|
3371
|
-
// src/
|
|
3372
|
-
function
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
if (
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
if (gate.status !== "failed" && gate.status !== "timedOut") return false;
|
|
3380
|
-
if (gate.status === gate.expectedStatus) return false;
|
|
3381
|
-
return gate.retry >= gate.retries;
|
|
3382
|
-
}
|
|
3383
|
-
function environmentalSkipReason(gate) {
|
|
3384
|
-
if (gate.enabled !== "true") return null;
|
|
3385
|
-
if (gate.status !== "failed" && gate.status !== "timedOut") return null;
|
|
3386
|
-
if (gate.status === gate.expectedStatus) return null;
|
|
3387
|
-
if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
|
|
3388
|
-
if (gate.headless !== false) {
|
|
3389
|
-
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
3358
|
+
// ../picker-dom/src/overlay-anchors.ts
|
|
3359
|
+
function showAnchorPicker(arg) {
|
|
3360
|
+
const g = globalThis;
|
|
3361
|
+
const doc = g.document;
|
|
3362
|
+
const el = g.__piwiPickedElement;
|
|
3363
|
+
if (!doc || !doc.body || !el) {
|
|
3364
|
+
g.__piwiAnchorState = "skipped";
|
|
3365
|
+
return;
|
|
3390
3366
|
}
|
|
3391
|
-
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
retry: testInfo.retry,
|
|
3402
|
-
retries: testInfo.project?.retries ?? 0
|
|
3367
|
+
const Z = 2147483600;
|
|
3368
|
+
const { tagRoles, inputRoles, roleSources, leafRole, leafLevel, leafTestId } = arg;
|
|
3369
|
+
const roleOf = (n) => {
|
|
3370
|
+
const explicit = n.getAttribute && n.getAttribute("role");
|
|
3371
|
+
if (explicit) return explicit;
|
|
3372
|
+
const tag = (n.tagName || "").toLowerCase();
|
|
3373
|
+
if (tag === "input") return inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
3374
|
+
if (tag === "select") return n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
3375
|
+
if (tag === "a") return n.getAttribute("href") != null ? "link" : null;
|
|
3376
|
+
return tagRoles[tag] ?? null;
|
|
3403
3377
|
};
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3378
|
+
const levelOf = (n) => {
|
|
3379
|
+
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
3380
|
+
if (m) return Number(m[1]);
|
|
3381
|
+
const al = n.getAttribute && n.getAttribute("aria-level");
|
|
3382
|
+
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
3383
|
+
};
|
|
3384
|
+
const leafMatches = (scope) => {
|
|
3385
|
+
try {
|
|
3386
|
+
if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
|
|
3387
|
+
const nodes = scope.querySelectorAll(roleSources);
|
|
3388
|
+
if (nodes.length > 2e3) return -1;
|
|
3389
|
+
let matched = 0;
|
|
3390
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
3391
|
+
const n = nodes[i];
|
|
3392
|
+
if (roleOf(n) !== leafRole) continue;
|
|
3393
|
+
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
3394
|
+
matched++;
|
|
3395
|
+
}
|
|
3396
|
+
return matched;
|
|
3397
|
+
} catch {
|
|
3398
|
+
return -1;
|
|
3415
3399
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
if (s[i] === "{") depth++;
|
|
3424
|
-
else if (s[i] === "}" && --depth === 0) return i;
|
|
3425
|
-
}
|
|
3426
|
-
return s.length - 1;
|
|
3427
|
-
}
|
|
3428
|
-
function parseOptions(src) {
|
|
3429
|
-
const obj = {};
|
|
3430
|
-
const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
|
|
3431
|
-
let m;
|
|
3432
|
-
while ((m = re.exec(src)) !== null) {
|
|
3433
|
-
const key = m[1];
|
|
3434
|
-
const raw = m[2];
|
|
3435
|
-
if (raw === "true") obj[key] = true;
|
|
3436
|
-
else if (raw === "false") obj[key] = false;
|
|
3437
|
-
else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
|
|
3438
|
-
else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
3400
|
+
};
|
|
3401
|
+
let roleNodes = [];
|
|
3402
|
+
try {
|
|
3403
|
+
const all = doc.querySelectorAll(roleSources);
|
|
3404
|
+
if (all.length <= 4e3) roleNodes = Array.from(all);
|
|
3405
|
+
} catch {
|
|
3406
|
+
roleNodes = [];
|
|
3439
3407
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
while (i < inner.length) {
|
|
3446
|
-
const c = inner[i];
|
|
3447
|
-
if (c === " " || c === ",") {
|
|
3448
|
-
i++;
|
|
3449
|
-
continue;
|
|
3450
|
-
}
|
|
3451
|
-
if (c === "'" || c === '"') {
|
|
3452
|
-
const end = endOfString(inner, i);
|
|
3453
|
-
args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
|
|
3454
|
-
i = end + 1;
|
|
3455
|
-
continue;
|
|
3456
|
-
}
|
|
3457
|
-
if (c === "{") {
|
|
3458
|
-
const end = matchBrace(inner, i);
|
|
3459
|
-
args.push(parseOptions(inner.slice(i, end + 1)));
|
|
3460
|
-
i = end + 1;
|
|
3461
|
-
continue;
|
|
3408
|
+
const count = (sel) => {
|
|
3409
|
+
try {
|
|
3410
|
+
return doc.querySelectorAll(sel).length;
|
|
3411
|
+
} catch {
|
|
3412
|
+
return void 0;
|
|
3462
3413
|
}
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
}
|
|
3467
|
-
function leafExpression(expr) {
|
|
3414
|
+
};
|
|
3415
|
+
const rows = [];
|
|
3416
|
+
let node = el.parentElement;
|
|
3468
3417
|
let depth = 0;
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
const
|
|
3472
|
-
if (
|
|
3473
|
-
|
|
3474
|
-
|
|
3418
|
+
while (node && depth < 12) {
|
|
3419
|
+
depth++;
|
|
3420
|
+
const tag = (node.tagName || "").toLowerCase();
|
|
3421
|
+
if (tag === "body" || tag === "html") break;
|
|
3422
|
+
const testId = node.getAttribute("data-testid");
|
|
3423
|
+
const id = node.getAttribute("id");
|
|
3424
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
3425
|
+
const role = roleOf(node);
|
|
3426
|
+
const info = { tag, depth, testId: testId || null, id: id || null, ariaLabel: ariaLabel || null, role };
|
|
3427
|
+
if (testId) info.testIdCount = count(`[data-testid=${JSON.stringify(testId)}]`);
|
|
3428
|
+
if (id) {
|
|
3429
|
+
try {
|
|
3430
|
+
info.idCount = count(`#${doc.defaultView.CSS.escape(id)}`);
|
|
3431
|
+
} catch {
|
|
3432
|
+
}
|
|
3475
3433
|
}
|
|
3476
|
-
if (
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
}
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
const m = /^([A-Za-z]+)\((.*)\)$/s.exec(expr);
|
|
3487
|
-
if (!m) return null;
|
|
3488
|
-
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
3489
|
-
}
|
|
3490
|
-
function deriveFailedLocator(testInfo) {
|
|
3491
|
-
const info = testInfo;
|
|
3492
|
-
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
3493
|
-
for (const err of errors) {
|
|
3494
|
-
const text = `${err.message ?? ""}
|
|
3495
|
-
${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
3496
|
-
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
3497
|
-
if (!line) continue;
|
|
3498
|
-
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
3499
|
-
if (!parsed) continue;
|
|
3500
|
-
const loc = err.location;
|
|
3501
|
-
const location = loc ? `${path16.relative(process.cwd(), loc.file).split(path16.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
3502
|
-
return { method: parsed.method, args: parsed.args, location };
|
|
3503
|
-
}
|
|
3504
|
-
return null;
|
|
3505
|
-
}
|
|
3506
|
-
var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
3507
|
-
var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
|
|
3508
|
-
var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
|
|
3509
|
-
function anchorSegment(a) {
|
|
3510
|
-
if (a.testId && a.testIdCount === 1) {
|
|
3511
|
-
return { code: `getByTestId('${escStr(a.testId)}')`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
|
|
3512
|
-
}
|
|
3513
|
-
if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
|
|
3514
|
-
const anchorSelector = cssIdSelector(a.id);
|
|
3515
|
-
return { code: `locator('${escStr(anchorSelector)}')`, kind: "id", flatArgs: { anchorSelector } };
|
|
3516
|
-
}
|
|
3517
|
-
if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
|
|
3518
|
-
return {
|
|
3519
|
-
code: `getByRole('${escStr(a.role)}', { name: '${escStr(a.ariaLabel)}' })`,
|
|
3520
|
-
kind: "labeledRole",
|
|
3521
|
-
flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
|
|
3522
|
-
};
|
|
3523
|
-
}
|
|
3524
|
-
if (a.role && a.roleCount === 1) {
|
|
3525
|
-
return { code: `getByRole('${escStr(a.role)}')`, kind: "role", flatArgs: { anchorRole: a.role } };
|
|
3526
|
-
}
|
|
3527
|
-
return null;
|
|
3528
|
-
}
|
|
3529
|
-
function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
3530
|
-
if (!leaf.role || anchors.length === 0) return [];
|
|
3531
|
-
const out = [];
|
|
3532
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3533
|
-
const rolePart = leaf.level != null ? `'${leaf.role}', { level: ${leaf.level} }` : `'${leaf.role}'`;
|
|
3534
|
-
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
3535
|
-
const add = (l) => {
|
|
3536
|
-
if (!seen.has(l.locator)) {
|
|
3537
|
-
seen.add(l.locator);
|
|
3538
|
-
out.push(l);
|
|
3434
|
+
if (role) {
|
|
3435
|
+
let roleCount = 0;
|
|
3436
|
+
let labeledCount = 0;
|
|
3437
|
+
for (const n of roleNodes) {
|
|
3438
|
+
if (roleOf(n) !== role) continue;
|
|
3439
|
+
roleCount++;
|
|
3440
|
+
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
3441
|
+
}
|
|
3442
|
+
info.roleCount = roleCount;
|
|
3443
|
+
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
3539
3444
|
}
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
method: "getByRole",
|
|
3548
|
-
args: { ...leafArgs, ...seg.flatArgs },
|
|
3549
|
-
score: ANCHOR_KIND_SCORES[seg.kind]
|
|
3445
|
+
info.scopedLeafCount = leafMatches(node);
|
|
3446
|
+
const hookLabel = testId ? `data-testid="${testId}"` : id ? `#${id}` : ariaLabel && role ? `${role} "${ariaLabel}"` : role ? `role ${role}` : "no stable hook";
|
|
3447
|
+
rows.push({
|
|
3448
|
+
node,
|
|
3449
|
+
info,
|
|
3450
|
+
hookLabel,
|
|
3451
|
+
selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
|
|
3550
3452
|
});
|
|
3453
|
+
node = node.parentElement;
|
|
3551
3454
|
}
|
|
3552
|
-
if (
|
|
3553
|
-
|
|
3554
|
-
const segments = ordered.map(anchorSegment);
|
|
3555
|
-
if (segments.every((s) => s !== null)) {
|
|
3556
|
-
const innermost = segments[segments.length - 1];
|
|
3557
|
-
const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
|
|
3558
|
-
add({
|
|
3559
|
-
locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
|
|
3560
|
-
method: "getByRole",
|
|
3561
|
-
// Flat args describe the innermost anchor (what the healing fingerprint
|
|
3562
|
-
// logic understands); the full chain rides along for transparency.
|
|
3563
|
-
args: { ...leafArgs, ...innermost.flatArgs, anchorChain: segments.map((s) => s.code) },
|
|
3564
|
-
score
|
|
3565
|
-
});
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
return out.sort((a, b) => b.score - a.score);
|
|
3569
|
-
}
|
|
3570
|
-
function mergeCandidates(base, extra) {
|
|
3571
|
-
const seen = new Set(base.map((b) => b.locator));
|
|
3572
|
-
const merged = [...base];
|
|
3573
|
-
for (const e of extra) {
|
|
3574
|
-
if (seen.has(e.locator)) continue;
|
|
3575
|
-
seen.add(e.locator);
|
|
3576
|
-
merged.push(e);
|
|
3577
|
-
}
|
|
3578
|
-
return merged.sort((a, b) => b.score - a.score);
|
|
3579
|
-
}
|
|
3580
|
-
function installPickerOverlay(arg) {
|
|
3581
|
-
const g = globalThis;
|
|
3582
|
-
const doc = g.document;
|
|
3583
|
-
if (!doc || !doc.body) {
|
|
3584
|
-
g.__piwiPickState = "skipped";
|
|
3455
|
+
if (rows.length === 0) {
|
|
3456
|
+
g.__piwiAnchorState = "skipped";
|
|
3585
3457
|
return;
|
|
3586
3458
|
}
|
|
3587
|
-
const
|
|
3588
|
-
const
|
|
3589
|
-
|
|
3590
|
-
const
|
|
3591
|
-
|
|
3592
|
-
const
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
const testId = el.getAttribute && el.getAttribute("data-testid");
|
|
3620
|
-
if (testId) return `getByTestId('${escJs(testId)}')`;
|
|
3621
|
-
if (el.labels && el.labels.length > 0) {
|
|
3622
|
-
const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
3623
|
-
if (labelText) return `getByLabel('${escJs(labelText)}')`;
|
|
3459
|
+
const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
|
|
3460
|
+
const outline = doc.createElement("div");
|
|
3461
|
+
outline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
|
|
3462
|
+
const outlineLabel = doc.createElement("div");
|
|
3463
|
+
outlineLabel.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(420px,90vw);background:#0b1120;color:#f9fafb;border:1px solid #22c55e;border-radius:6px;padding:3px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
|
|
3464
|
+
const pickedOutline = doc.createElement("div");
|
|
3465
|
+
const pr = el.getBoundingClientRect();
|
|
3466
|
+
pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #a855f7;background:rgba(168,85,247,.14);border-radius:4px;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(59,7,100,.55);left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
|
|
3467
|
+
const panel = doc.createElement("div");
|
|
3468
|
+
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
3469
|
+
const title = doc.createElement("div");
|
|
3470
|
+
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
3471
|
+
title.textContent = "Scope to stable parents (optional)";
|
|
3472
|
+
const sub = doc.createElement("div");
|
|
3473
|
+
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
3474
|
+
sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
|
|
3475
|
+
panel.appendChild(title);
|
|
3476
|
+
panel.appendChild(sub);
|
|
3477
|
+
const selected = /* @__PURE__ */ new Set();
|
|
3478
|
+
const footer = doc.createElement("div");
|
|
3479
|
+
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
3480
|
+
const segMatches = (scope, info) => {
|
|
3481
|
+
try {
|
|
3482
|
+
if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
|
|
3483
|
+
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
3484
|
+
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
3485
|
+
if (nodes.length > 2e3) return [];
|
|
3486
|
+
return nodes.filter(
|
|
3487
|
+
(n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
3488
|
+
);
|
|
3489
|
+
} catch {
|
|
3490
|
+
return [];
|
|
3624
3491
|
}
|
|
3625
|
-
const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
|
|
3626
|
-
if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
|
|
3627
|
-
const placeholder = el.getAttribute && el.getAttribute("placeholder");
|
|
3628
|
-
if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
|
|
3629
|
-
const alt = el.getAttribute && el.getAttribute("alt");
|
|
3630
|
-
if (alt) return `getByAltText('${escJs(alt)}')`;
|
|
3631
|
-
const titleAttr = el.getAttribute && el.getAttribute("title");
|
|
3632
|
-
if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
|
|
3633
|
-
if (el.id) return `locator('#${escJs(el.id)}')`;
|
|
3634
|
-
const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
|
|
3635
|
-
return cls ? `locator('.${escJs(cls)}')` : tag;
|
|
3636
3492
|
};
|
|
3637
|
-
const
|
|
3638
|
-
const
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3493
|
+
const chainCount = () => {
|
|
3494
|
+
const chosen = rows.filter((_, i) => selected.has(i)).sort((a, b) => b.info.depth - a.info.depth);
|
|
3495
|
+
if (chosen.length === 0) return -1;
|
|
3496
|
+
let scopes = [doc];
|
|
3497
|
+
for (const row of chosen) {
|
|
3498
|
+
const next = [];
|
|
3499
|
+
for (const s of scopes) next.push(...segMatches(s, row.info));
|
|
3500
|
+
scopes = next.slice(0, 200);
|
|
3501
|
+
if (scopes.length === 0) return 0;
|
|
3645
3502
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
const el = chain2[i];
|
|
3652
|
-
const tag = (el.tagName || "").toLowerCase();
|
|
3653
|
-
if (ACTIONABLE_TAGS.includes(tag)) return i;
|
|
3654
|
-
if (el.getAttribute && (el.getAttribute("role") || el.getAttribute("data-testid"))) return i;
|
|
3503
|
+
let total = 0;
|
|
3504
|
+
for (const s of scopes) {
|
|
3505
|
+
const c = leafMatches(s);
|
|
3506
|
+
if (c > 0) total += c;
|
|
3507
|
+
if (total > 50) return total;
|
|
3655
3508
|
}
|
|
3656
|
-
return
|
|
3509
|
+
return total;
|
|
3657
3510
|
};
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
const el = current();
|
|
3664
|
-
if (!el) {
|
|
3665
|
-
highlight.style.display = "none";
|
|
3511
|
+
const refreshFooter = () => {
|
|
3512
|
+
if (selected.size === 0) {
|
|
3513
|
+
footer.textContent = "No parents selected \u2014 standard alternatives only.";
|
|
3514
|
+
footer.style.color = "#9ca3af";
|
|
3515
|
+
g.__piwiPickChainCount = void 0;
|
|
3666
3516
|
return;
|
|
3667
3517
|
}
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
e.preventDefault();
|
|
3678
|
-
e.stopImmediatePropagation();
|
|
3518
|
+
const c = chainCount();
|
|
3519
|
+
g.__piwiPickChainCount = c;
|
|
3520
|
+
if (c === 1) {
|
|
3521
|
+
footer.textContent = "\u2713 Selection matches exactly 1 element";
|
|
3522
|
+
footer.style.color = "#4ade80";
|
|
3523
|
+
} else {
|
|
3524
|
+
footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
|
|
3525
|
+
footer.style.color = "#fbbf24";
|
|
3526
|
+
}
|
|
3679
3527
|
};
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
const
|
|
3708
|
-
const
|
|
3709
|
-
const
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3528
|
+
rows.forEach((row, i) => {
|
|
3529
|
+
const line = doc.createElement("label");
|
|
3530
|
+
line.style.cssText = `display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid #374151;border-radius:6px;margin-bottom:6px;cursor:${row.selectable ? "pointer" : "default"};opacity:${row.selectable ? "1" : ".45"};`;
|
|
3531
|
+
const box = doc.createElement("input");
|
|
3532
|
+
box.type = "checkbox";
|
|
3533
|
+
box.disabled = !row.selectable;
|
|
3534
|
+
const text = doc.createElement("span");
|
|
3535
|
+
text.style.cssText = "flex:1;min-width:0;";
|
|
3536
|
+
const code = doc.createElement("code");
|
|
3537
|
+
code.style.cssText = `display:block;font:12px ${MONO};color:#f3f4f6;word-break:break-all;`;
|
|
3538
|
+
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
3539
|
+
const hint = doc.createElement("span");
|
|
3540
|
+
hint.style.cssText = "color:#9ca3af;";
|
|
3541
|
+
hint.textContent = row.selectable ? row.info.scopedLeafCount === 1 ? "contains exactly 1 matching element" : `contains ${row.info.scopedLeafCount ?? "?"} matching elements` : "add a data-testid to make this usable";
|
|
3542
|
+
text.appendChild(code);
|
|
3543
|
+
text.appendChild(hint);
|
|
3544
|
+
line.appendChild(box);
|
|
3545
|
+
line.appendChild(text);
|
|
3546
|
+
line.addEventListener("mouseenter", () => {
|
|
3547
|
+
const r = row.node.getBoundingClientRect();
|
|
3548
|
+
outline.style.display = "block";
|
|
3549
|
+
outline.style.left = r.left + "px";
|
|
3550
|
+
outline.style.top = r.top + "px";
|
|
3551
|
+
outline.style.width = r.width + "px";
|
|
3552
|
+
outline.style.height = r.height + "px";
|
|
3553
|
+
outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
3554
|
+
outlineLabel.style.display = "block";
|
|
3555
|
+
const lr = outlineLabel.getBoundingClientRect();
|
|
3556
|
+
const vw = g.innerWidth || doc.documentElement.clientWidth || 0;
|
|
3557
|
+
const top = r.top - lr.height - 6 < 4 ? r.bottom + 6 : r.top - lr.height - 6;
|
|
3558
|
+
outlineLabel.style.left = Math.max(6, Math.min(r.left, vw - lr.width - 6)) + "px";
|
|
3559
|
+
outlineLabel.style.top = top + "px";
|
|
3560
|
+
});
|
|
3561
|
+
line.addEventListener("mouseleave", () => {
|
|
3562
|
+
outline.style.display = "none";
|
|
3563
|
+
outlineLabel.style.display = "none";
|
|
3564
|
+
});
|
|
3565
|
+
box.addEventListener("change", () => {
|
|
3566
|
+
if (box.checked) selected.add(i);
|
|
3567
|
+
else selected.delete(i);
|
|
3568
|
+
refreshFooter();
|
|
3569
|
+
});
|
|
3570
|
+
panel.appendChild(line);
|
|
3571
|
+
});
|
|
3572
|
+
panel.appendChild(footer);
|
|
3573
|
+
const cleanup = () => {
|
|
3574
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
3575
|
+
panel.remove();
|
|
3576
|
+
outline.remove();
|
|
3577
|
+
outlineLabel.remove();
|
|
3578
|
+
pickedOutline.remove();
|
|
3714
3579
|
};
|
|
3715
|
-
const
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
g.__piwiPickedElement = el;
|
|
3720
|
-
g.__piwiPickState = "picked";
|
|
3721
|
-
removeListeners();
|
|
3722
|
-
highlight.style.display = "none";
|
|
3723
|
-
foot.textContent = "Analyzing element\u2026";
|
|
3580
|
+
const done = (state) => {
|
|
3581
|
+
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
3582
|
+
g.__piwiAnchorState = state;
|
|
3583
|
+
cleanup();
|
|
3724
3584
|
};
|
|
3725
3585
|
const onKey = (e) => {
|
|
3726
|
-
if (e.key
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
return;
|
|
3731
|
-
}
|
|
3732
|
-
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
3733
|
-
stop(e);
|
|
3734
|
-
if (e.key === "ArrowUp") idx = Math.min(idx + 1, chain.length - 1);
|
|
3735
|
-
else idx = Math.max(idx - 1, 0);
|
|
3736
|
-
refresh();
|
|
3737
|
-
}
|
|
3738
|
-
};
|
|
3739
|
-
const suppressed = ["mousedown", "mouseup", "pointerdown", "pointerup", "auxclick", "dblclick"];
|
|
3740
|
-
const removeListeners = () => {
|
|
3741
|
-
doc.removeEventListener("mousemove", onMove, true);
|
|
3742
|
-
doc.removeEventListener("click", onClick, true);
|
|
3743
|
-
doc.removeEventListener("keydown", onKey, true);
|
|
3744
|
-
for (const t of suppressed) doc.removeEventListener(t, stop, true);
|
|
3745
|
-
};
|
|
3746
|
-
const cleanup = () => {
|
|
3747
|
-
removeListeners();
|
|
3748
|
-
highlight.remove();
|
|
3749
|
-
banner.remove();
|
|
3586
|
+
if (e.key !== "Escape") return;
|
|
3587
|
+
e.preventDefault();
|
|
3588
|
+
e.stopImmediatePropagation();
|
|
3589
|
+
done("skipped");
|
|
3750
3590
|
};
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
doc.
|
|
3591
|
+
const buttonRow = doc.createElement("div");
|
|
3592
|
+
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
3593
|
+
const useBtn = doc.createElement("button");
|
|
3594
|
+
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
3595
|
+
useBtn.textContent = "Use selected parents";
|
|
3596
|
+
useBtn.addEventListener("click", (e) => {
|
|
3597
|
+
e.preventDefault();
|
|
3598
|
+
e.stopImmediatePropagation();
|
|
3599
|
+
done(selected.size > 0 ? "done" : "skipped");
|
|
3600
|
+
});
|
|
3601
|
+
const skipBtn = doc.createElement("button");
|
|
3602
|
+
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
3603
|
+
skipBtn.textContent = "Skip (Esc)";
|
|
3604
|
+
skipBtn.addEventListener("click", (e) => {
|
|
3605
|
+
e.preventDefault();
|
|
3606
|
+
e.stopImmediatePropagation();
|
|
3607
|
+
done("skipped");
|
|
3608
|
+
});
|
|
3609
|
+
buttonRow.appendChild(useBtn);
|
|
3610
|
+
buttonRow.appendChild(skipBtn);
|
|
3611
|
+
panel.appendChild(buttonRow);
|
|
3612
|
+
refreshFooter();
|
|
3613
|
+
g.__piwiAnchorCleanup = cleanup;
|
|
3754
3614
|
doc.addEventListener("keydown", onKey, true);
|
|
3755
|
-
|
|
3615
|
+
doc.body.appendChild(pickedOutline);
|
|
3616
|
+
doc.body.appendChild(outline);
|
|
3617
|
+
doc.body.appendChild(outlineLabel);
|
|
3618
|
+
doc.body.appendChild(panel);
|
|
3756
3619
|
}
|
|
3757
|
-
|
|
3620
|
+
|
|
3621
|
+
// ../picker-dom/src/overlay-confirm.ts
|
|
3622
|
+
function showPickerChoices(arg) {
|
|
3758
3623
|
const g = globalThis;
|
|
3759
3624
|
const doc = g.document;
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
g.__piwiAnchorState = "skipped";
|
|
3625
|
+
if (!doc || !doc.body) {
|
|
3626
|
+
g.__piwiPickChoice = -1;
|
|
3763
3627
|
return;
|
|
3764
3628
|
}
|
|
3765
3629
|
const Z = 2147483600;
|
|
3766
|
-
const
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
};
|
|
3782
|
-
const leafMatches = (scope) => {
|
|
3783
|
-
try {
|
|
3784
|
-
if (leafTestId) return scope.querySelectorAll(`[data-testid=${JSON.stringify(leafTestId)}]`).length;
|
|
3785
|
-
const nodes = scope.querySelectorAll(roleSources);
|
|
3786
|
-
if (nodes.length > 2e3) return -1;
|
|
3787
|
-
let matched = 0;
|
|
3788
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
3789
|
-
const n = nodes[i];
|
|
3790
|
-
if (roleOf(n) !== leafRole) continue;
|
|
3791
|
-
if (leafLevel != null && levelOf(n) !== leafLevel) continue;
|
|
3792
|
-
matched++;
|
|
3793
|
-
}
|
|
3794
|
-
return matched;
|
|
3795
|
-
} catch {
|
|
3796
|
-
return -1;
|
|
3630
|
+
const wrap = doc.createElement("div");
|
|
3631
|
+
wrap.style.cssText = `position:fixed;inset:0;z-index:${Z + 3};background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;font:13px/1.5 system-ui,sans-serif;`;
|
|
3632
|
+
const panel = doc.createElement("div");
|
|
3633
|
+
panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
|
|
3634
|
+
const hlLocator = (expr) => {
|
|
3635
|
+
const escHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3636
|
+
const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
|
|
3637
|
+
let html = "";
|
|
3638
|
+
let last = 0;
|
|
3639
|
+
let m;
|
|
3640
|
+
while ((m = re.exec(expr)) !== null) {
|
|
3641
|
+
if (m.index > last) html += escHtml(expr.slice(last, m.index));
|
|
3642
|
+
const color = m[1] ? "#86efac" : m[2] ? "#d8b4fe" : m[3] ? "#93c5fd" : m[4] ? "#fcd34d" : "#9ca3af";
|
|
3643
|
+
html += `<span style="color:${color}">${escHtml(m[0])}</span>`;
|
|
3644
|
+
last = re.lastIndex;
|
|
3797
3645
|
}
|
|
3646
|
+
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
3647
|
+
return html;
|
|
3798
3648
|
};
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3649
|
+
const title = doc.createElement("div");
|
|
3650
|
+
title.style.cssText = "font-weight:600;margin-bottom:4px;";
|
|
3651
|
+
title.textContent = arg.failing ? "Pick a replacement locator" : "Pick a locator";
|
|
3652
|
+
const sub = doc.createElement("div");
|
|
3653
|
+
sub.style.cssText = "color:#9ca3af;margin-bottom:12px;";
|
|
3654
|
+
if (arg.failing) {
|
|
3655
|
+
sub.innerHTML = `Replaces <code style="font-family:ui-monospace,Menlo,monospace">${hlLocator(arg.failing)}</code> \u2014 ranked by stability score.`;
|
|
3656
|
+
} else {
|
|
3657
|
+
sub.textContent = "For the element you picked \u2014 ranked by stability score.";
|
|
3805
3658
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3659
|
+
panel.appendChild(title);
|
|
3660
|
+
panel.appendChild(sub);
|
|
3661
|
+
const done = (choice) => {
|
|
3662
|
+
g.__piwiPickChoice = choice;
|
|
3663
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
3664
|
+
wrap.remove();
|
|
3812
3665
|
};
|
|
3813
|
-
const
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
const
|
|
3821
|
-
|
|
3822
|
-
const
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3666
|
+
const onKey = (e) => {
|
|
3667
|
+
if (e.key !== "Escape") return;
|
|
3668
|
+
e.preventDefault();
|
|
3669
|
+
e.stopImmediatePropagation();
|
|
3670
|
+
done(-1);
|
|
3671
|
+
};
|
|
3672
|
+
arg.choices.forEach((c, i) => {
|
|
3673
|
+
const btn = doc.createElement("button");
|
|
3674
|
+
btn.style.cssText = "display:flex;justify-content:space-between;align-items:center;gap:12px;width:100%;text-align:left;background:#0b1120;color:#f3f4f6;border:1px solid #374151;border-radius:6px;padding:8px 12px;margin:0 0 8px;cursor:pointer;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;";
|
|
3675
|
+
const code = doc.createElement("span");
|
|
3676
|
+
code.innerHTML = hlLocator(c.locator);
|
|
3677
|
+
code.style.cssText = "word-break:break-all;";
|
|
3678
|
+
const score = doc.createElement("span");
|
|
3679
|
+
score.textContent = String(c.score);
|
|
3680
|
+
score.style.cssText = "color:#c4b5fd;flex-shrink:0;font-variant-numeric:tabular-nums;";
|
|
3681
|
+
btn.appendChild(code);
|
|
3682
|
+
btn.appendChild(score);
|
|
3683
|
+
btn.addEventListener("click", (e) => {
|
|
3684
|
+
e.preventDefault();
|
|
3685
|
+
e.stopImmediatePropagation();
|
|
3686
|
+
done(i);
|
|
3687
|
+
});
|
|
3688
|
+
panel.appendChild(btn);
|
|
3689
|
+
});
|
|
3690
|
+
const skip = doc.createElement("button");
|
|
3691
|
+
skip.style.cssText = "background:none;border:none;color:#9ca3af;cursor:pointer;padding:6px 0 0;font:12px system-ui,sans-serif;";
|
|
3692
|
+
skip.textContent = "Skip \u2014 keep the failure as-is (Esc)";
|
|
3693
|
+
skip.addEventListener("click", (e) => {
|
|
3694
|
+
e.preventDefault();
|
|
3695
|
+
e.stopImmediatePropagation();
|
|
3696
|
+
done(-1);
|
|
3697
|
+
});
|
|
3698
|
+
panel.appendChild(skip);
|
|
3699
|
+
doc.addEventListener("keydown", onKey, true);
|
|
3700
|
+
wrap.appendChild(panel);
|
|
3701
|
+
doc.body.appendChild(wrap);
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// ../core/src/locator-generation.ts
|
|
3705
|
+
var TAG_TO_ROLE = {
|
|
3706
|
+
a: "link",
|
|
3707
|
+
button: "button",
|
|
3708
|
+
nav: "navigation",
|
|
3709
|
+
main: "main",
|
|
3710
|
+
article: "article",
|
|
3711
|
+
section: "region",
|
|
3712
|
+
form: "form",
|
|
3713
|
+
img: "img",
|
|
3714
|
+
figure: "figure",
|
|
3715
|
+
figcaption: "caption",
|
|
3716
|
+
blockquote: "blockquote",
|
|
3717
|
+
table: "table",
|
|
3718
|
+
caption: "caption",
|
|
3719
|
+
thead: "rowgroup",
|
|
3720
|
+
tbody: "rowgroup",
|
|
3721
|
+
tfoot: "rowgroup",
|
|
3722
|
+
tr: "row",
|
|
3723
|
+
td: "cell",
|
|
3724
|
+
ul: "list",
|
|
3725
|
+
ol: "list",
|
|
3726
|
+
li: "listitem",
|
|
3727
|
+
dialog: "dialog",
|
|
3728
|
+
output: "status",
|
|
3729
|
+
progress: "progressbar",
|
|
3730
|
+
meter: "meter",
|
|
3731
|
+
textarea: "textbox",
|
|
3732
|
+
h1: "heading",
|
|
3733
|
+
h2: "heading",
|
|
3734
|
+
h3: "heading",
|
|
3735
|
+
h4: "heading",
|
|
3736
|
+
h5: "heading",
|
|
3737
|
+
h6: "heading",
|
|
3738
|
+
details: "group",
|
|
3739
|
+
summary: "button",
|
|
3740
|
+
search: "search"
|
|
3741
|
+
};
|
|
3742
|
+
var INPUT_TYPE_TO_ROLE = {
|
|
3743
|
+
button: "button",
|
|
3744
|
+
submit: "button",
|
|
3745
|
+
reset: "button",
|
|
3746
|
+
image: "button",
|
|
3747
|
+
checkbox: "checkbox",
|
|
3748
|
+
radio: "radio",
|
|
3749
|
+
range: "slider",
|
|
3750
|
+
search: "searchbox",
|
|
3751
|
+
number: "spinbutton",
|
|
3752
|
+
text: "textbox",
|
|
3753
|
+
email: "textbox",
|
|
3754
|
+
tel: "textbox",
|
|
3755
|
+
url: "textbox",
|
|
3756
|
+
password: "textbox"
|
|
3757
|
+
};
|
|
3758
|
+
var CAPTURED_ATTRIBUTES = [
|
|
3759
|
+
"id",
|
|
3760
|
+
"class",
|
|
3761
|
+
"name",
|
|
3762
|
+
"data-testid",
|
|
3763
|
+
"placeholder",
|
|
3764
|
+
"alt",
|
|
3765
|
+
"title",
|
|
3766
|
+
"aria-label",
|
|
3767
|
+
"aria-level",
|
|
3768
|
+
"role",
|
|
3769
|
+
"type",
|
|
3770
|
+
"href",
|
|
3771
|
+
"multiple"
|
|
3772
|
+
];
|
|
3773
|
+
function resolveAriaRole(attrs) {
|
|
3774
|
+
const explicit = attrs.attributes["role"];
|
|
3775
|
+
if (explicit) return explicit;
|
|
3776
|
+
const tag = attrs.tagName;
|
|
3777
|
+
if (!tag) return null;
|
|
3778
|
+
if (tag === "input") {
|
|
3779
|
+
const type = (attrs.attributes["type"] ?? "text").toLowerCase();
|
|
3780
|
+
return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
|
|
3781
|
+
}
|
|
3782
|
+
if (tag === "select") {
|
|
3783
|
+
return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
|
|
3784
|
+
}
|
|
3785
|
+
if (tag === "a") {
|
|
3786
|
+
return attrs.attributes["href"] != null ? "link" : null;
|
|
3787
|
+
}
|
|
3788
|
+
return TAG_TO_ROLE[tag] ?? null;
|
|
3789
|
+
}
|
|
3790
|
+
function headingLevel(attrs, role) {
|
|
3791
|
+
if (role !== "heading") return null;
|
|
3792
|
+
const tagMatch = attrs.tagName.match(/^h([1-6])$/);
|
|
3793
|
+
if (tagMatch) return Number(tagMatch[1]);
|
|
3794
|
+
const ariaLevel = attrs.attributes["aria-level"];
|
|
3795
|
+
if (ariaLevel && /^\d+$/.test(ariaLevel)) return Number(ariaLevel);
|
|
3796
|
+
return null;
|
|
3797
|
+
}
|
|
3798
|
+
var attr = (a, key) => a.attributes[key] || null;
|
|
3799
|
+
var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3800
|
+
var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
3801
|
+
var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
|
|
3802
|
+
function classifyCssStability(className) {
|
|
3803
|
+
if (/[a-f0-9]{8,}/i.test(className)) return 10;
|
|
3804
|
+
if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
|
|
3805
|
+
if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
|
|
3806
|
+
className
|
|
3807
|
+
))
|
|
3808
|
+
return 25;
|
|
3809
|
+
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
3810
|
+
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
3811
|
+
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
3812
|
+
if (className.includes("_")) return 15;
|
|
3813
|
+
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
3814
|
+
return 15;
|
|
3815
|
+
}
|
|
3816
|
+
function isAutoGenerated(value) {
|
|
3817
|
+
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
3818
|
+
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
3819
|
+
if (/^[a-z]+-\d{4,}$/.test(value)) return true;
|
|
3820
|
+
if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
|
|
3821
|
+
value
|
|
3822
|
+
)) {
|
|
3823
|
+
return true;
|
|
3824
|
+
}
|
|
3825
|
+
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
3826
|
+
if (value.startsWith("ng-")) return true;
|
|
3827
|
+
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
3828
|
+
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
3829
|
+
return false;
|
|
3830
|
+
}
|
|
3831
|
+
var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
|
|
3832
|
+
function generateAlternatives(attrs) {
|
|
3833
|
+
const alts = [];
|
|
3834
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3835
|
+
const add = (loc) => {
|
|
3836
|
+
if (!seen.has(loc.locator)) {
|
|
3837
|
+
seen.add(loc.locator);
|
|
3838
|
+
alts.push(loc);
|
|
3839
|
+
}
|
|
3840
|
+
};
|
|
3841
|
+
const { accessibleName } = attrs;
|
|
3842
|
+
const tag = attrs.tagName;
|
|
3843
|
+
const role = resolveAriaRole(attrs);
|
|
3844
|
+
const counts = attrs.selectorCounts;
|
|
3845
|
+
const isUnique = (n) => n == null || n <= 1;
|
|
3846
|
+
const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
|
|
3847
|
+
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
3848
|
+
const testId = attr(attrs, "data-testid");
|
|
3849
|
+
if (testId && isUnique(counts?.testId)) {
|
|
3850
|
+
add({
|
|
3851
|
+
locator: `getByTestId('${esc(testId)}')`,
|
|
3852
|
+
method: "getByTestId",
|
|
3853
|
+
args: { testId },
|
|
3854
|
+
score: 100
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
const level = headingLevel(attrs, role);
|
|
3858
|
+
const levelPart = level != null ? `, level: ${level}` : "";
|
|
3859
|
+
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
3860
|
+
if (role && accessibleName) {
|
|
3861
|
+
add({
|
|
3862
|
+
locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
|
|
3863
|
+
method: "getByRole",
|
|
3864
|
+
args: withLevel({ role, name: accessibleName }),
|
|
3865
|
+
score: 90 - ambiguityPenalty(counts?.roleName)
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
const ariaLabel = attr(attrs, "aria-label");
|
|
3869
|
+
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
3870
|
+
add({
|
|
3871
|
+
locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
|
|
3872
|
+
method: "getByRole",
|
|
3873
|
+
args: withLevel({ role, name: ariaLabel }),
|
|
3874
|
+
score: 85 - ambiguityPenalty(counts?.roleName)
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
|
|
3878
|
+
const label = attr(attrs, "aria-label");
|
|
3879
|
+
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
3880
|
+
if (labelBacked) {
|
|
3881
|
+
add({
|
|
3882
|
+
locator: `getByLabel('${esc(accessibleName)}')`,
|
|
3883
|
+
method: "getByLabel",
|
|
3884
|
+
args: { label: accessibleName },
|
|
3885
|
+
score: 85
|
|
3886
|
+
});
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
const placeholder = attr(attrs, "placeholder");
|
|
3890
|
+
if (placeholder) {
|
|
3891
|
+
add({
|
|
3892
|
+
locator: `getByPlaceholder('${esc(placeholder)}')`,
|
|
3893
|
+
method: "getByPlaceholder",
|
|
3894
|
+
args: { placeholder },
|
|
3895
|
+
score: 80 - ambiguityPenalty(counts?.placeholder)
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
if (text && text.length < 80) {
|
|
3899
|
+
add({
|
|
3900
|
+
locator: `getByText('${esc(text)}')`,
|
|
3901
|
+
method: "getByText",
|
|
3902
|
+
args: { text },
|
|
3903
|
+
score: 75 - ambiguityPenalty(counts?.text)
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
const id = attr(attrs, "id");
|
|
3907
|
+
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
3908
|
+
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
3909
|
+
add({
|
|
3910
|
+
locator: `locator('${esc(selector)}')`,
|
|
3911
|
+
method: "locator",
|
|
3912
|
+
args: { selector },
|
|
3913
|
+
score: 65
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
const name = attr(attrs, "name");
|
|
3917
|
+
if (name && isUnique(counts?.name)) {
|
|
3918
|
+
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
3919
|
+
add({
|
|
3920
|
+
locator: `locator('${esc(selector)}')`,
|
|
3921
|
+
method: "locator",
|
|
3922
|
+
args: { selector },
|
|
3923
|
+
score: 60
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
const alt = attr(attrs, "alt");
|
|
3927
|
+
if (alt) {
|
|
3928
|
+
add({
|
|
3929
|
+
locator: `getByAltText('${esc(alt)}')`,
|
|
3930
|
+
method: "getByAltText",
|
|
3931
|
+
args: { text: alt },
|
|
3932
|
+
score: 60 - ambiguityPenalty(counts?.alt)
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
const title = attr(attrs, "title");
|
|
3936
|
+
if (title) {
|
|
3937
|
+
add({
|
|
3938
|
+
locator: `getByTitle('${esc(title)}')`,
|
|
3939
|
+
method: "getByTitle",
|
|
3940
|
+
args: { title },
|
|
3941
|
+
score: 50 - ambiguityPenalty(counts?.title)
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
const hasOwnTestId = !!(testId && isUnique(counts?.testId));
|
|
3945
|
+
const addAnchoredChains = (leaf, scopedCount, scores) => {
|
|
3946
|
+
let testIdAnchorDone = false;
|
|
3947
|
+
let idAnchorDone = false;
|
|
3948
|
+
let roleAnchorDone = false;
|
|
3949
|
+
let dataAnchorDone = false;
|
|
3950
|
+
let filterAnchorDone = false;
|
|
3951
|
+
for (const anc of attrs.ancestors ?? []) {
|
|
3952
|
+
if (scopedCount(anc) !== 1) continue;
|
|
3953
|
+
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
3954
|
+
testIdAnchorDone = true;
|
|
3955
|
+
add({
|
|
3956
|
+
locator: `getByTestId('${esc(anc.testId)}').${leaf.expr}`,
|
|
3957
|
+
method: leaf.method,
|
|
3958
|
+
args: { ...leaf.args, anchorTestId: anc.testId },
|
|
3959
|
+
score: scores.testId
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
3963
|
+
idAnchorDone = true;
|
|
3964
|
+
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
3965
|
+
add({
|
|
3966
|
+
locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
|
|
3967
|
+
method: leaf.method,
|
|
3968
|
+
args: { ...leaf.args, anchorSelector },
|
|
3969
|
+
score: scores.id
|
|
3970
|
+
});
|
|
3971
|
+
}
|
|
3972
|
+
if (!dataAnchorDone && anc.dataAttr && anc.dataAttrCount === 1) {
|
|
3973
|
+
dataAnchorDone = true;
|
|
3974
|
+
const { name: name2, value } = anc.dataAttr;
|
|
3975
|
+
const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
|
|
3976
|
+
add({
|
|
3977
|
+
locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
|
|
3978
|
+
method: leaf.method,
|
|
3979
|
+
args: { ...leaf.args, anchorSelector },
|
|
3980
|
+
score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
3984
|
+
if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
|
|
3985
|
+
roleAnchorDone = true;
|
|
3986
|
+
add({
|
|
3987
|
+
locator: `getByRole('${esc(ancestorRole)}').${leaf.expr}`,
|
|
3988
|
+
method: leaf.method,
|
|
3989
|
+
args: { ...leaf.args, anchorRole: ancestorRole },
|
|
3990
|
+
score: scores.role
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
|
|
3994
|
+
filterAnchorDone = true;
|
|
3995
|
+
add({
|
|
3996
|
+
locator: `getByRole('${esc(ancestorRole)}').filter({ hasText: '${esc(anc.filterText)}' }).${leaf.expr}`,
|
|
3997
|
+
method: leaf.method,
|
|
3998
|
+
args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
|
|
3999
|
+
score: scores.filter
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
if (role && !hasOwnTestId) {
|
|
4005
|
+
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
4006
|
+
const leafArgs = withLevel({ role });
|
|
4007
|
+
addAnchoredChains(
|
|
4008
|
+
{ expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
|
|
4009
|
+
(anc) => anc.scopedRoleCount,
|
|
4010
|
+
{ testId: 72, testData: 70, id: 64, data: 60, role: 55, filter: 53 }
|
|
4011
|
+
);
|
|
4012
|
+
const pos = attrs.rolePosition;
|
|
4013
|
+
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
4014
|
+
add({
|
|
4015
|
+
locator: `getByRole(${rolePart})`,
|
|
4016
|
+
method: "getByRole",
|
|
4017
|
+
args: leafArgs,
|
|
4018
|
+
score: 58
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
} else if (!role && text && text.length < 80 && !hasOwnTestId) {
|
|
4022
|
+
addAnchoredChains(
|
|
4023
|
+
{ expr: `getByText('${esc(text)}')`, method: "getByText", args: { text }, role: null },
|
|
4024
|
+
(anc) => anc.scopedTextCount,
|
|
4025
|
+
{ testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
const clsStr = attr(attrs, "class");
|
|
4029
|
+
if (clsStr) {
|
|
4030
|
+
const countsClasses = counts?.classes;
|
|
4031
|
+
const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
|
|
4032
|
+
for (const { cls, score } of classes) {
|
|
4033
|
+
add({
|
|
4034
|
+
locator: `locator('.${esc(cls)}')`,
|
|
4035
|
+
method: "locator",
|
|
4036
|
+
args: { selector: `.${cls}` },
|
|
4037
|
+
score
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
return alts.sort((a, b) => b.score - a.score);
|
|
4042
|
+
}
|
|
4043
|
+
function approximateAccessibleName(attrs) {
|
|
4044
|
+
const a = attrs.attributes;
|
|
4045
|
+
const ariaLabel = a["aria-label"];
|
|
4046
|
+
if (ariaLabel) return ariaLabel;
|
|
4047
|
+
if (attrs.textContent) return attrs.textContent;
|
|
4048
|
+
const title = a["title"];
|
|
4049
|
+
if (title) return title;
|
|
4050
|
+
const placeholder = a["placeholder"];
|
|
4051
|
+
if (placeholder) return placeholder;
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// ../picker-dom/src/anchor-alternatives.ts
|
|
4056
|
+
var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
4057
|
+
var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
|
|
4058
|
+
var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
|
|
4059
|
+
function anchorSegment(a) {
|
|
4060
|
+
if (a.testId && a.testIdCount === 1) {
|
|
4061
|
+
return { code: `getByTestId('${escStr(a.testId)}')`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
|
|
4062
|
+
}
|
|
4063
|
+
if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
|
|
4064
|
+
const anchorSelector = cssIdSelector(a.id);
|
|
4065
|
+
return { code: `locator('${escStr(anchorSelector)}')`, kind: "id", flatArgs: { anchorSelector } };
|
|
4066
|
+
}
|
|
4067
|
+
if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
|
|
4068
|
+
return {
|
|
4069
|
+
code: `getByRole('${escStr(a.role)}', { name: '${escStr(a.ariaLabel)}' })`,
|
|
4070
|
+
kind: "labeledRole",
|
|
4071
|
+
flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
|
|
4072
|
+
};
|
|
4073
|
+
}
|
|
4074
|
+
if (a.role && a.roleCount === 1) {
|
|
4075
|
+
return { code: `getByRole('${escStr(a.role)}')`, kind: "role", flatArgs: { anchorRole: a.role } };
|
|
4076
|
+
}
|
|
4077
|
+
return null;
|
|
4078
|
+
}
|
|
4079
|
+
function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
|
|
4080
|
+
if (!leaf.role || anchors.length === 0) return [];
|
|
4081
|
+
const out = [];
|
|
4082
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4083
|
+
const rolePart = leaf.level != null ? `'${leaf.role}', { level: ${leaf.level} }` : `'${leaf.role}'`;
|
|
4084
|
+
const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
|
|
4085
|
+
const add = (l) => {
|
|
4086
|
+
if (!seen.has(l.locator)) {
|
|
4087
|
+
seen.add(l.locator);
|
|
4088
|
+
out.push(l);
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
for (const anchor of anchors) {
|
|
4092
|
+
if (anchor.scopedLeafCount !== 1) continue;
|
|
4093
|
+
const seg = anchorSegment(anchor);
|
|
4094
|
+
if (!seg) continue;
|
|
4095
|
+
add({
|
|
4096
|
+
locator: `${seg.code}.getByRole(${rolePart})`,
|
|
4097
|
+
method: "getByRole",
|
|
4098
|
+
args: { ...leafArgs, ...seg.flatArgs },
|
|
4099
|
+
score: ANCHOR_KIND_SCORES[seg.kind]
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
if (anchors.length >= 2 && chainLeafCount === 1) {
|
|
4103
|
+
const ordered = [...anchors].sort((a, b) => b.depth - a.depth);
|
|
4104
|
+
const segments = ordered.map(anchorSegment);
|
|
4105
|
+
if (segments.every((s) => s !== null)) {
|
|
4106
|
+
const innermost = segments[segments.length - 1];
|
|
4107
|
+
const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
|
|
4108
|
+
add({
|
|
4109
|
+
locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
|
|
4110
|
+
method: "getByRole",
|
|
4111
|
+
// Flat args describe the innermost anchor (what the healing fingerprint
|
|
4112
|
+
// logic understands); the full chain rides along for transparency.
|
|
4113
|
+
args: { ...leafArgs, ...innermost.flatArgs, anchorChain: segments.map((s) => s.code) },
|
|
4114
|
+
score
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
return out.sort((a, b) => b.score - a.score);
|
|
4119
|
+
}
|
|
4120
|
+
function mergeCandidates(base, extra) {
|
|
4121
|
+
const seen = new Set(base.map((b) => b.locator));
|
|
4122
|
+
const merged = [...base];
|
|
4123
|
+
for (const e of extra) {
|
|
4124
|
+
if (seen.has(e.locator)) continue;
|
|
4125
|
+
seen.add(e.locator);
|
|
4126
|
+
merged.push(e);
|
|
4127
|
+
}
|
|
4128
|
+
return merged.sort((a, b) => b.score - a.score);
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
// src/internal/capture/locator-healing.ts
|
|
4132
|
+
var path15 = __toESM(require("path"));
|
|
4133
|
+
|
|
4134
|
+
// ../core/src/locator-fingerprint.ts
|
|
4135
|
+
var PRESENT_SIMILARITY = 0.8;
|
|
4136
|
+
var MATCH_SIMILARITY = 0.2;
|
|
4137
|
+
function parseAriaCandidates(ariaSnapshot) {
|
|
4138
|
+
if (!ariaSnapshot) return [];
|
|
4139
|
+
const out = [];
|
|
4140
|
+
for (const line of ariaSnapshot.split("\n")) {
|
|
4141
|
+
const m = line.match(/^\s*-\s+([a-z]+)(?:\s+"((?:[^"\\]|\\.)*)")?/i);
|
|
4142
|
+
if (!m) continue;
|
|
4143
|
+
const role = m[1];
|
|
4144
|
+
const name = m[2] != null ? m[2].replace(/\\(.)/g, "$1") : null;
|
|
4145
|
+
if (!name && (role === "generic" || role === "group" || role === "list" || role === "paragraph")) continue;
|
|
4146
|
+
const levelMatch = line.slice(m[0].length).match(/\[level=(\d+)\]/);
|
|
4147
|
+
const level = levelMatch ? Number(levelMatch[1]) : null;
|
|
4148
|
+
out.push({ role, name, level });
|
|
4149
|
+
}
|
|
4150
|
+
return out;
|
|
4151
|
+
}
|
|
4152
|
+
function textSimilarity(a, b) {
|
|
4153
|
+
const tok = (s) => new Set(
|
|
4154
|
+
(s ?? "").toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean)
|
|
4155
|
+
);
|
|
4156
|
+
const sa = tok(a);
|
|
4157
|
+
const sb = tok(b);
|
|
4158
|
+
if (sa.size === 0 && sb.size === 0) return 1;
|
|
4159
|
+
if (sa.size === 0 || sb.size === 0) return 0;
|
|
4160
|
+
let common = 0;
|
|
4161
|
+
for (const t of sa) if (sb.has(t)) common++;
|
|
4162
|
+
return 2 * common / (sa.size + sb.size);
|
|
4163
|
+
}
|
|
4164
|
+
function fingerprintPresent(fp, candidates) {
|
|
4165
|
+
if (!fp.name) return false;
|
|
4166
|
+
return candidates.some(
|
|
4167
|
+
(c) => (!fp.role || c.role === fp.role) && textSimilarity(c.name, fp.name) >= PRESENT_SIMILARITY
|
|
4168
|
+
);
|
|
4169
|
+
}
|
|
4170
|
+
function matchRenamedElement(fp, candidates) {
|
|
4171
|
+
if (candidates.length === 0) return null;
|
|
4172
|
+
const sameRole = fp.role ? candidates.filter((c) => c.role === fp.role) : candidates;
|
|
4173
|
+
if (sameRole.length === 0) return null;
|
|
4174
|
+
let pool = sameRole;
|
|
4175
|
+
if (fp.level != null) {
|
|
4176
|
+
const sameLevel = sameRole.filter((c) => c.level === fp.level);
|
|
4177
|
+
if (sameLevel.length > 0) pool = sameLevel;
|
|
4178
|
+
}
|
|
4179
|
+
if (pool.length === 1) {
|
|
4180
|
+
return { candidate: pool[0], confidence: 0.7 };
|
|
4181
|
+
}
|
|
4182
|
+
let best = null;
|
|
4183
|
+
let bestScore = -1;
|
|
4184
|
+
for (const c of pool) {
|
|
4185
|
+
const s = textSimilarity(c.name, fp.name);
|
|
4186
|
+
if (s > bestScore) {
|
|
4187
|
+
bestScore = s;
|
|
4188
|
+
best = c;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
if (best && bestScore >= MATCH_SIMILARITY) return { candidate: best, confidence: bestScore };
|
|
4192
|
+
const pos = fp.rolePosition;
|
|
4193
|
+
if (pos && fp.role && pos.role === fp.role && sameRole.length >= 2 && sameRole.length === pos.count) {
|
|
4194
|
+
const byIndex = sameRole[pos.index];
|
|
4195
|
+
if (byIndex) return { candidate: byIndex, confidence: 0.5 };
|
|
4196
|
+
}
|
|
4197
|
+
return null;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
// ../core/src/locator-methods.ts
|
|
4201
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
4202
|
+
"getByRole",
|
|
4203
|
+
"getByTestId",
|
|
4204
|
+
"getByText",
|
|
4205
|
+
"getByLabel",
|
|
4206
|
+
"getByPlaceholder",
|
|
4207
|
+
"getByAltText",
|
|
4208
|
+
"getByTitle",
|
|
4209
|
+
"locator"
|
|
4210
|
+
];
|
|
4211
|
+
|
|
4212
|
+
// src/internal/capture/locator-healing.ts
|
|
4213
|
+
function dedupeSnapshotsByLocation(snaps) {
|
|
4214
|
+
const lastWithElement = /* @__PURE__ */ new Map();
|
|
4215
|
+
const lastAny = /* @__PURE__ */ new Map();
|
|
4216
|
+
snaps.forEach((s, i) => {
|
|
4217
|
+
if (!s.location) return;
|
|
4218
|
+
lastAny.set(s.location, i);
|
|
4219
|
+
if (s.element) lastWithElement.set(s.location, i);
|
|
4220
|
+
});
|
|
4221
|
+
return snaps.filter((s, i) => {
|
|
4222
|
+
if (!s.location) return true;
|
|
4223
|
+
return (lastWithElement.get(s.location) ?? lastAny.get(s.location)) === i;
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
4227
|
+
var CHAIN_METHODS = [
|
|
4228
|
+
"first",
|
|
4229
|
+
"nth",
|
|
4230
|
+
"last",
|
|
4231
|
+
"filter",
|
|
4232
|
+
"and",
|
|
4233
|
+
"or",
|
|
4234
|
+
"locator",
|
|
4235
|
+
"getByRole",
|
|
4236
|
+
"getByTestId",
|
|
4237
|
+
"getByText",
|
|
4238
|
+
"getByLabel",
|
|
4239
|
+
"getByPlaceholder",
|
|
4240
|
+
"getByAltText",
|
|
4241
|
+
"getByTitle"
|
|
4242
|
+
];
|
|
4243
|
+
var ACTION_METHODS = [
|
|
4244
|
+
"click",
|
|
4245
|
+
"fill",
|
|
4246
|
+
"check",
|
|
4247
|
+
"uncheck",
|
|
4248
|
+
"selectOption",
|
|
4249
|
+
"dblclick",
|
|
4250
|
+
"tap",
|
|
4251
|
+
"hover",
|
|
4252
|
+
"press",
|
|
4253
|
+
"type",
|
|
4254
|
+
"pressSequentially",
|
|
4255
|
+
"clear",
|
|
4256
|
+
"setInputFiles",
|
|
4257
|
+
"dragTo",
|
|
4258
|
+
"focus",
|
|
4259
|
+
"blur",
|
|
4260
|
+
"scrollIntoViewIfNeeded",
|
|
4261
|
+
"dispatchEvent",
|
|
4262
|
+
"selectText",
|
|
4263
|
+
// Not an action, but a successful waitFor proves the element resolved — the
|
|
4264
|
+
// closest capture hook available for assertion-style usage of a locator.
|
|
4265
|
+
"waitFor"
|
|
4266
|
+
];
|
|
4267
|
+
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
4268
|
+
var EXPECT_METHOD = "_expect";
|
|
4269
|
+
var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
4270
|
+
"to.be.attached",
|
|
4271
|
+
"to.be.checked",
|
|
4272
|
+
"to.be.disabled",
|
|
4273
|
+
"to.be.editable",
|
|
4274
|
+
"to.be.empty",
|
|
4275
|
+
"to.be.enabled",
|
|
4276
|
+
"to.be.focused",
|
|
4277
|
+
"to.be.in.viewport",
|
|
4278
|
+
"to.be.readonly",
|
|
4279
|
+
"to.be.visible",
|
|
4280
|
+
"to.contain.class",
|
|
4281
|
+
"to.contain.text",
|
|
4282
|
+
"to.have.accessible.description",
|
|
4283
|
+
"to.have.accessible.error.message",
|
|
4284
|
+
"to.have.accessible.name",
|
|
4285
|
+
"to.have.attribute",
|
|
4286
|
+
"to.have.attribute.value",
|
|
4287
|
+
"to.have.class",
|
|
4288
|
+
"to.have.css",
|
|
4289
|
+
"to.have.id",
|
|
4290
|
+
"to.have.js.property",
|
|
4291
|
+
"to.have.role",
|
|
4292
|
+
"to.have.text",
|
|
4293
|
+
"to.have.value",
|
|
4294
|
+
"to.match.aria"
|
|
4295
|
+
]);
|
|
4296
|
+
function extractAccessibleName(ariaSnapshot) {
|
|
4297
|
+
if (!ariaSnapshot) return null;
|
|
4298
|
+
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
4299
|
+
if (match) return match[1];
|
|
4300
|
+
return null;
|
|
4301
|
+
}
|
|
4302
|
+
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
4303
|
+
"getByText",
|
|
4304
|
+
"getByRole",
|
|
4305
|
+
"getByLabel",
|
|
4306
|
+
"getByPlaceholder",
|
|
4307
|
+
"getByTitle",
|
|
4308
|
+
"getByAltText"
|
|
4309
|
+
]);
|
|
4310
|
+
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
4311
|
+
var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
|
|
4312
|
+
"button",
|
|
4313
|
+
"link",
|
|
4314
|
+
"heading",
|
|
4315
|
+
"menuitem",
|
|
4316
|
+
"tab",
|
|
4317
|
+
"option",
|
|
4318
|
+
"cell",
|
|
4319
|
+
"columnheader",
|
|
4320
|
+
"rowheader",
|
|
4321
|
+
"gridcell",
|
|
4322
|
+
"treeitem",
|
|
4323
|
+
"listitem",
|
|
4324
|
+
"checkbox",
|
|
4325
|
+
"radio",
|
|
4326
|
+
"switch"
|
|
4327
|
+
]);
|
|
4328
|
+
var SUGG_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
|
|
4329
|
+
function failedNameAndRole(failed) {
|
|
4330
|
+
if (failed.method === "getByRole") {
|
|
4331
|
+
const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
|
|
4332
|
+
const opts = failed.args[1];
|
|
4333
|
+
const name = opts && typeof opts.name === "string" ? opts.name : null;
|
|
4334
|
+
const level = opts && typeof opts.level === "number" ? opts.level : null;
|
|
4335
|
+
return { role, name, level };
|
|
4336
|
+
}
|
|
4337
|
+
const first = failed.args.find((a) => typeof a === "string");
|
|
4338
|
+
return { role: null, name: typeof first === "string" ? first : null, level: null };
|
|
4339
|
+
}
|
|
4340
|
+
function renderFailing(failed) {
|
|
4341
|
+
const { role, name } = failedNameAndRole(failed);
|
|
4342
|
+
if (failed.method === "getByRole") {
|
|
4343
|
+
return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
|
|
4344
|
+
}
|
|
4345
|
+
return `${failed.method}('${escAttr(name ?? "")}')`;
|
|
4346
|
+
}
|
|
4347
|
+
function freshSuggestions(candidate, failedMethod) {
|
|
4348
|
+
const out = [];
|
|
4349
|
+
const role = candidate.role;
|
|
4350
|
+
const name = candidate.name;
|
|
4351
|
+
const push = (s) => {
|
|
4352
|
+
if (!out.includes(s)) out.push(s);
|
|
4353
|
+
};
|
|
4354
|
+
const levelPart = candidate.level != null ? `, level: ${candidate.level}` : "";
|
|
4355
|
+
const roleLoc = `getByRole('${escAttr(role)}', { name: '${escAttr(name)}'${levelPart} })`;
|
|
4356
|
+
const textLoc = `getByText('${escAttr(name)}')`;
|
|
4357
|
+
const labelLoc = `getByLabel('${escAttr(name)}')`;
|
|
4358
|
+
if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
4359
|
+
if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
4360
|
+
push(roleLoc);
|
|
4361
|
+
if (SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
4362
|
+
else if (SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
4363
|
+
return out;
|
|
4364
|
+
}
|
|
4365
|
+
function suggestLocatorsFromAria(failed, ariaSnapshot) {
|
|
4366
|
+
if (!ariaSnapshot || !NAME_BASED_METHODS.has(failed.method)) return null;
|
|
4367
|
+
const { role, name, level } = failedNameAndRole(failed);
|
|
4368
|
+
if (!name) return null;
|
|
4369
|
+
const candidates = parseAriaCandidates(ariaSnapshot);
|
|
4370
|
+
if (candidates.length === 0) return null;
|
|
4371
|
+
const fingerprint = { role, name, level };
|
|
4372
|
+
if (fingerprintPresent(fingerprint, candidates)) return null;
|
|
4373
|
+
const best = matchRenamedElement(fingerprint, candidates)?.candidate;
|
|
4374
|
+
if (!best || !best.name) return null;
|
|
4375
|
+
const suggestions = freshSuggestions({ role: best.role, name: best.name, level: best.level }, failed.method);
|
|
4376
|
+
if (suggestions.length === 0) return null;
|
|
4377
|
+
return { failing: renderFailing(failed), suggestions };
|
|
4378
|
+
}
|
|
4379
|
+
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
4380
|
+
const lines = stack.split("\n");
|
|
4381
|
+
let prevWasCaptureModule = false;
|
|
4382
|
+
let selfFile = null;
|
|
4383
|
+
for (let i = 1; i < lines.length; i++) {
|
|
4384
|
+
const line = lines[i].trim();
|
|
4385
|
+
if (!line.startsWith("at")) continue;
|
|
4386
|
+
const m = line.match(/^at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
4387
|
+
if (!m) {
|
|
4388
|
+
prevWasCaptureModule = false;
|
|
4389
|
+
continue;
|
|
4390
|
+
}
|
|
4391
|
+
let file = m[2];
|
|
4392
|
+
if (!file || file.startsWith("node:")) {
|
|
4393
|
+
prevWasCaptureModule = false;
|
|
4394
|
+
continue;
|
|
3831
4395
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
if (roleOf(n) !== role) continue;
|
|
3837
|
-
roleCount++;
|
|
3838
|
-
if (ariaLabel && n.getAttribute && n.getAttribute("aria-label") === ariaLabel) labeledCount++;
|
|
3839
|
-
}
|
|
3840
|
-
info.roleCount = roleCount;
|
|
3841
|
-
if (ariaLabel) info.labeledRoleCount = labeledCount;
|
|
4396
|
+
file = file.replace(/^file:\/\/\/?/, "");
|
|
4397
|
+
if (!/\.[a-z]+$/i.test(file)) {
|
|
4398
|
+
prevWasCaptureModule = false;
|
|
4399
|
+
continue;
|
|
3842
4400
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
info,
|
|
3848
|
-
hookLabel,
|
|
3849
|
-
selectable: !!(testId || id || role && (ariaLabel || info.roleCount === 1))
|
|
3850
|
-
});
|
|
3851
|
-
node = node.parentElement;
|
|
3852
|
-
}
|
|
3853
|
-
if (rows.length === 0) {
|
|
3854
|
-
g.__piwiAnchorState = "skipped";
|
|
3855
|
-
return;
|
|
3856
|
-
}
|
|
3857
|
-
const outline = doc.createElement("div");
|
|
3858
|
-
outline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;border-radius:3px;display:none;`;
|
|
3859
|
-
const pickedOutline = doc.createElement("div");
|
|
3860
|
-
const pr = el.getBoundingClientRect();
|
|
3861
|
-
pickedOutline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #7c3aed;background:rgba(124,58,237,.10);border-radius:3px;left:${pr.left}px;top:${pr.top}px;width:${pr.width}px;height:${pr.height}px;`;
|
|
3862
|
-
const panel = doc.createElement("div");
|
|
3863
|
-
panel.style.cssText = `position:fixed;top:12px;right:12px;z-index:${Z + 3};width:340px;max-height:82vh;overflow:auto;background:#111827;color:#f9fafb;border-radius:10px;padding:16px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 8px 40px rgba(0,0,0,.5);`;
|
|
3864
|
-
const title = doc.createElement("div");
|
|
3865
|
-
title.style.cssText = "font-weight:600;font-size:13px;margin-bottom:2px;";
|
|
3866
|
-
title.textContent = "Scope to stable parents (optional)";
|
|
3867
|
-
const sub = doc.createElement("div");
|
|
3868
|
-
sub.style.cssText = "color:#9ca3af;margin-bottom:10px;";
|
|
3869
|
-
sub.textContent = "Pick one or more parents to anchor the locator to. Hover a row to see the parent.";
|
|
3870
|
-
panel.appendChild(title);
|
|
3871
|
-
panel.appendChild(sub);
|
|
3872
|
-
const selected = /* @__PURE__ */ new Set();
|
|
3873
|
-
const footer = doc.createElement("div");
|
|
3874
|
-
footer.style.cssText = "margin:10px 0;font-weight:600;";
|
|
3875
|
-
const segMatches = (scope, info) => {
|
|
3876
|
-
try {
|
|
3877
|
-
if (info.testId) return Array.from(scope.querySelectorAll(`[data-testid=${JSON.stringify(info.testId)}]`));
|
|
3878
|
-
if (info.id) return Array.from(scope.querySelectorAll(`#${doc.defaultView.CSS.escape(info.id)}`));
|
|
3879
|
-
const nodes = Array.from(scope.querySelectorAll(roleSources));
|
|
3880
|
-
if (nodes.length > 2e3) return [];
|
|
3881
|
-
return nodes.filter(
|
|
3882
|
-
(n) => roleOf(n) === info.role && (!info.ariaLabel || n.getAttribute && n.getAttribute("aria-label") === info.ariaLabel)
|
|
3883
|
-
);
|
|
3884
|
-
} catch {
|
|
3885
|
-
return [];
|
|
4401
|
+
if (selfFile === null || file === selfFile) {
|
|
4402
|
+
selfFile ??= file;
|
|
4403
|
+
prevWasCaptureModule = /[\\/]locator-healing\.[a-z]+$/i.test(file);
|
|
4404
|
+
continue;
|
|
3886
4405
|
}
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
if (chosen.length === 0) return -1;
|
|
3891
|
-
let scopes = [doc];
|
|
3892
|
-
for (const row of chosen) {
|
|
3893
|
-
const next = [];
|
|
3894
|
-
for (const s of scopes) next.push(...segMatches(s, row.info));
|
|
3895
|
-
scopes = next.slice(0, 200);
|
|
3896
|
-
if (scopes.length === 0) return 0;
|
|
4406
|
+
if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
|
|
4407
|
+
prevWasCaptureModule = true;
|
|
4408
|
+
continue;
|
|
3897
4409
|
}
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
if (c > 0) total += c;
|
|
3902
|
-
if (total > 50) return total;
|
|
4410
|
+
if (prevWasCaptureModule && /[\\/](?:capture-)?fixtures\.[a-z]+$/i.test(file)) {
|
|
4411
|
+
prevWasCaptureModule = false;
|
|
4412
|
+
continue;
|
|
3903
4413
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
if (selected.size === 0) {
|
|
3908
|
-
footer.textContent = "No parents selected \u2014 standard alternatives only.";
|
|
3909
|
-
footer.style.color = "#9ca3af";
|
|
3910
|
-
g.__piwiPickChainCount = void 0;
|
|
3911
|
-
return;
|
|
4414
|
+
if (/[\\/]node_modules[\\/]/.test(file)) {
|
|
4415
|
+
prevWasCaptureModule = false;
|
|
4416
|
+
continue;
|
|
3912
4417
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
footer.style.color = "#4ade80";
|
|
3918
|
-
} else {
|
|
3919
|
-
footer.textContent = c < 0 ? "Match count unavailable" : `\u2717 Selection matches ${c} elements`;
|
|
3920
|
-
footer.style.color = "#fbbf24";
|
|
4418
|
+
let rel = file;
|
|
4419
|
+
try {
|
|
4420
|
+
rel = path15.relative(process.cwd(), file);
|
|
4421
|
+
} catch {
|
|
3921
4422
|
}
|
|
4423
|
+
rel = rel.split(path15.sep).join("/");
|
|
4424
|
+
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
4425
|
+
return `${rel}:${m[3]}:${m[4]}`;
|
|
4426
|
+
}
|
|
4427
|
+
return null;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
// src/internal/capture/inspect-on-failure.ts
|
|
4431
|
+
function isCi(ci) {
|
|
4432
|
+
return ci !== void 0 && ci !== "" && ci !== "false";
|
|
4433
|
+
}
|
|
4434
|
+
function shouldInspectOnFailure(gate) {
|
|
4435
|
+
if (gate.enabled !== "true") return false;
|
|
4436
|
+
if (isCi(gate.ci)) return false;
|
|
4437
|
+
if (gate.headless !== false) return false;
|
|
4438
|
+
if (gate.status !== "failed" && gate.status !== "timedOut") return false;
|
|
4439
|
+
if (gate.status === gate.expectedStatus) return false;
|
|
4440
|
+
return gate.retry >= gate.retries;
|
|
4441
|
+
}
|
|
4442
|
+
function environmentalSkipReason(gate) {
|
|
4443
|
+
if (gate.enabled !== "true") return null;
|
|
4444
|
+
if (gate.status !== "failed" && gate.status !== "timedOut") return null;
|
|
4445
|
+
if (gate.status === gate.expectedStatus) return null;
|
|
4446
|
+
if (isCi(gate.ci)) return "running under CI \u2014 this is a headed, local-only feature";
|
|
4447
|
+
if (gate.headless !== false) {
|
|
4448
|
+
return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
|
|
4449
|
+
}
|
|
4450
|
+
return null;
|
|
4451
|
+
}
|
|
4452
|
+
function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT_ON_FAIL) {
|
|
4453
|
+
const use = testInfo.project?.use ?? {};
|
|
4454
|
+
return {
|
|
4455
|
+
enabled,
|
|
4456
|
+
ci: process.env.CI,
|
|
4457
|
+
status: testInfo.status,
|
|
4458
|
+
expectedStatus: testInfo.expectedStatus,
|
|
4459
|
+
headless: use.headless,
|
|
4460
|
+
retry: testInfo.retry,
|
|
4461
|
+
retries: testInfo.project?.retries ?? 0
|
|
3922
4462
|
};
|
|
3923
|
-
rows.forEach((row, i) => {
|
|
3924
|
-
const line = doc.createElement("label");
|
|
3925
|
-
line.style.cssText = `display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid #374151;border-radius:6px;margin-bottom:6px;cursor:${row.selectable ? "pointer" : "default"};opacity:${row.selectable ? "1" : ".45"};`;
|
|
3926
|
-
const box = doc.createElement("input");
|
|
3927
|
-
box.type = "checkbox";
|
|
3928
|
-
box.disabled = !row.selectable;
|
|
3929
|
-
const text = doc.createElement("span");
|
|
3930
|
-
text.style.cssText = "flex:1;min-width:0;";
|
|
3931
|
-
const code = doc.createElement("code");
|
|
3932
|
-
code.style.cssText = "display:block;font:11px ui-monospace,monospace;color:#e5e7eb;word-break:break-all;";
|
|
3933
|
-
code.textContent = `<${row.info.tag}> ${row.hookLabel}`;
|
|
3934
|
-
const hint = doc.createElement("span");
|
|
3935
|
-
hint.style.cssText = "color:#9ca3af;";
|
|
3936
|
-
hint.textContent = row.selectable ? row.info.scopedLeafCount === 1 ? "contains exactly 1 matching element" : `contains ${row.info.scopedLeafCount ?? "?"} matching elements` : "add a data-testid to make this usable";
|
|
3937
|
-
text.appendChild(code);
|
|
3938
|
-
text.appendChild(hint);
|
|
3939
|
-
line.appendChild(box);
|
|
3940
|
-
line.appendChild(text);
|
|
3941
|
-
line.addEventListener("mouseenter", () => {
|
|
3942
|
-
const r = row.node.getBoundingClientRect();
|
|
3943
|
-
outline.style.display = "block";
|
|
3944
|
-
outline.style.left = r.left + "px";
|
|
3945
|
-
outline.style.top = r.top + "px";
|
|
3946
|
-
outline.style.width = r.width + "px";
|
|
3947
|
-
outline.style.height = r.height + "px";
|
|
3948
|
-
});
|
|
3949
|
-
line.addEventListener("mouseleave", () => {
|
|
3950
|
-
outline.style.display = "none";
|
|
3951
|
-
});
|
|
3952
|
-
box.addEventListener("change", () => {
|
|
3953
|
-
if (box.checked) selected.add(i);
|
|
3954
|
-
else selected.delete(i);
|
|
3955
|
-
refreshFooter();
|
|
3956
|
-
});
|
|
3957
|
-
panel.appendChild(line);
|
|
3958
|
-
});
|
|
3959
|
-
panel.appendChild(footer);
|
|
3960
|
-
const cleanup = () => {
|
|
3961
|
-
doc.removeEventListener("keydown", onKey, true);
|
|
3962
|
-
panel.remove();
|
|
3963
|
-
outline.remove();
|
|
3964
|
-
pickedOutline.remove();
|
|
3965
|
-
};
|
|
3966
|
-
const done = (state) => {
|
|
3967
|
-
g.__piwiPickAnchors = state === "done" ? rows.filter((_, i) => selected.has(i)).map((r) => r.info) : [];
|
|
3968
|
-
g.__piwiAnchorState = state;
|
|
3969
|
-
cleanup();
|
|
3970
|
-
};
|
|
3971
|
-
const onKey = (e) => {
|
|
3972
|
-
if (e.key !== "Escape") return;
|
|
3973
|
-
e.preventDefault();
|
|
3974
|
-
e.stopImmediatePropagation();
|
|
3975
|
-
done("skipped");
|
|
3976
|
-
};
|
|
3977
|
-
const buttonRow = doc.createElement("div");
|
|
3978
|
-
buttonRow.style.cssText = "display:flex;gap:8px;margin-top:4px;";
|
|
3979
|
-
const useBtn = doc.createElement("button");
|
|
3980
|
-
useBtn.style.cssText = "flex:1;background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer;font:600 12px system-ui;";
|
|
3981
|
-
useBtn.textContent = "Use selected parents";
|
|
3982
|
-
useBtn.addEventListener("click", (e) => {
|
|
3983
|
-
e.preventDefault();
|
|
3984
|
-
e.stopImmediatePropagation();
|
|
3985
|
-
done(selected.size > 0 ? "done" : "skipped");
|
|
3986
|
-
});
|
|
3987
|
-
const skipBtn = doc.createElement("button");
|
|
3988
|
-
skipBtn.style.cssText = "background:none;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:8px 10px;cursor:pointer;font:12px system-ui;";
|
|
3989
|
-
skipBtn.textContent = "Skip (Esc)";
|
|
3990
|
-
skipBtn.addEventListener("click", (e) => {
|
|
3991
|
-
e.preventDefault();
|
|
3992
|
-
e.stopImmediatePropagation();
|
|
3993
|
-
done("skipped");
|
|
3994
|
-
});
|
|
3995
|
-
buttonRow.appendChild(useBtn);
|
|
3996
|
-
buttonRow.appendChild(skipBtn);
|
|
3997
|
-
panel.appendChild(buttonRow);
|
|
3998
|
-
refreshFooter();
|
|
3999
|
-
g.__piwiAnchorCleanup = cleanup;
|
|
4000
|
-
doc.addEventListener("keydown", onKey, true);
|
|
4001
|
-
doc.body.appendChild(pickedOutline);
|
|
4002
|
-
doc.body.appendChild(outline);
|
|
4003
|
-
doc.body.appendChild(panel);
|
|
4004
4463
|
}
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4464
|
+
|
|
4465
|
+
// src/internal/capture/pick-on-failure.ts
|
|
4466
|
+
var path16 = __toESM(require("path"));
|
|
4467
|
+
var ANSI_RE = /\[[0-9;]*m/g;
|
|
4468
|
+
function endOfString(s, start) {
|
|
4469
|
+
const q = s[start];
|
|
4470
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
4471
|
+
if (s[i] === "\\") {
|
|
4472
|
+
i++;
|
|
4473
|
+
continue;
|
|
4474
|
+
}
|
|
4475
|
+
if (s[i] === q) return i;
|
|
4476
|
+
}
|
|
4477
|
+
return s.length - 1;
|
|
4478
|
+
}
|
|
4479
|
+
function matchBrace(s, start) {
|
|
4480
|
+
let depth = 0;
|
|
4481
|
+
for (let i = start; i < s.length; i++) {
|
|
4482
|
+
if (s[i] === "{") depth++;
|
|
4483
|
+
else if (s[i] === "}" && --depth === 0) return i;
|
|
4484
|
+
}
|
|
4485
|
+
return s.length - 1;
|
|
4486
|
+
}
|
|
4487
|
+
function parseOptions(src) {
|
|
4488
|
+
const obj = {};
|
|
4489
|
+
const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
|
|
4490
|
+
let m;
|
|
4491
|
+
while ((m = re.exec(src)) !== null) {
|
|
4492
|
+
const key = m[1];
|
|
4493
|
+
const raw = m[2];
|
|
4494
|
+
if (raw === "true") obj[key] = true;
|
|
4495
|
+
else if (raw === "false") obj[key] = false;
|
|
4496
|
+
else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
|
|
4497
|
+
else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
4498
|
+
}
|
|
4499
|
+
return obj;
|
|
4500
|
+
}
|
|
4501
|
+
function parseArgs(inner) {
|
|
4502
|
+
const args = [];
|
|
4503
|
+
let i = 0;
|
|
4504
|
+
while (i < inner.length) {
|
|
4505
|
+
const c = inner[i];
|
|
4506
|
+
if (c === " " || c === ",") {
|
|
4507
|
+
i++;
|
|
4508
|
+
continue;
|
|
4509
|
+
}
|
|
4510
|
+
if (c === "'" || c === '"') {
|
|
4511
|
+
const end = endOfString(inner, i);
|
|
4512
|
+
args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
|
|
4513
|
+
i = end + 1;
|
|
4514
|
+
continue;
|
|
4515
|
+
}
|
|
4516
|
+
if (c === "{") {
|
|
4517
|
+
const end = matchBrace(inner, i);
|
|
4518
|
+
args.push(parseOptions(inner.slice(i, end + 1)));
|
|
4519
|
+
i = end + 1;
|
|
4520
|
+
continue;
|
|
4521
|
+
}
|
|
4522
|
+
i++;
|
|
4011
4523
|
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
const
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
last = re.lastIndex;
|
|
4524
|
+
return args;
|
|
4525
|
+
}
|
|
4526
|
+
function leafExpression(expr) {
|
|
4527
|
+
let depth = 0;
|
|
4528
|
+
let leafStart = 0;
|
|
4529
|
+
for (let i = 0; i < expr.length - 1; i++) {
|
|
4530
|
+
const c = expr[i];
|
|
4531
|
+
if (c === "'" || c === '"') {
|
|
4532
|
+
i = endOfString(expr, i);
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
if (c === "(") depth++;
|
|
4536
|
+
else if (c === ")") {
|
|
4537
|
+
depth--;
|
|
4538
|
+
if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
|
|
4028
4539
|
}
|
|
4029
|
-
if (last < expr.length) html += escHtml(expr.slice(last));
|
|
4030
|
-
return html;
|
|
4031
|
-
};
|
|
4032
|
-
const title = doc.createElement("div");
|
|
4033
|
-
title.style.cssText = "font-weight:600;margin-bottom:4px;";
|
|
4034
|
-
title.textContent = arg.failing ? "Pick a replacement locator" : "Pick a locator";
|
|
4035
|
-
const sub = doc.createElement("div");
|
|
4036
|
-
sub.style.cssText = "color:#9ca3af;margin-bottom:12px;";
|
|
4037
|
-
if (arg.failing) {
|
|
4038
|
-
sub.innerHTML = `Replaces <code style="font-family:ui-monospace,Menlo,monospace">${hlLocator(arg.failing)}</code> \u2014 ranked by stability score.`;
|
|
4039
|
-
} else {
|
|
4040
|
-
sub.textContent = "For the element you picked \u2014 ranked by stability score.";
|
|
4041
4540
|
}
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
};
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
const
|
|
4057
|
-
|
|
4058
|
-
const
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
btn.appendChild(score);
|
|
4066
|
-
btn.addEventListener("click", (e) => {
|
|
4067
|
-
e.preventDefault();
|
|
4068
|
-
e.stopImmediatePropagation();
|
|
4069
|
-
done(i);
|
|
4070
|
-
});
|
|
4071
|
-
panel.appendChild(btn);
|
|
4072
|
-
});
|
|
4073
|
-
const skip = doc.createElement("button");
|
|
4074
|
-
skip.style.cssText = "background:none;border:none;color:#9ca3af;cursor:pointer;padding:6px 0 0;font:12px system-ui,sans-serif;";
|
|
4075
|
-
skip.textContent = "Skip \u2014 keep the failure as-is (Esc)";
|
|
4076
|
-
skip.addEventListener("click", (e) => {
|
|
4077
|
-
e.preventDefault();
|
|
4078
|
-
e.stopImmediatePropagation();
|
|
4079
|
-
done(-1);
|
|
4080
|
-
});
|
|
4081
|
-
panel.appendChild(skip);
|
|
4082
|
-
doc.addEventListener("keydown", onKey, true);
|
|
4083
|
-
wrap.appendChild(panel);
|
|
4084
|
-
doc.body.appendChild(wrap);
|
|
4541
|
+
return expr.slice(leafStart);
|
|
4542
|
+
}
|
|
4543
|
+
function parseLeafLocatorExpression(rawExpr) {
|
|
4544
|
+
const expr = leafExpression(rawExpr.trim());
|
|
4545
|
+
const m = /^([A-Za-z]+)\((.*)\)$/s.exec(expr);
|
|
4546
|
+
if (!m) return null;
|
|
4547
|
+
return { method: m[1], args: parseArgs(m[2].trim()) };
|
|
4548
|
+
}
|
|
4549
|
+
function deriveFailedLocator(testInfo) {
|
|
4550
|
+
const info = testInfo;
|
|
4551
|
+
const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
|
|
4552
|
+
for (const err of errors) {
|
|
4553
|
+
const text = `${err.message ?? ""}
|
|
4554
|
+
${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
4555
|
+
const line = /^\s*Locator:\s*(.+)$/m.exec(text);
|
|
4556
|
+
if (!line) continue;
|
|
4557
|
+
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
4558
|
+
if (!parsed) continue;
|
|
4559
|
+
const loc = err.location;
|
|
4560
|
+
const location = loc ? `${path16.relative(process.cwd(), loc.file).split(path16.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
4561
|
+
return { method: parsed.method, args: parsed.args, location };
|
|
4562
|
+
}
|
|
4563
|
+
return null;
|
|
4085
4564
|
}
|
|
4086
4565
|
async function cleanupPicker(page) {
|
|
4087
4566
|
try {
|
|
@@ -4109,7 +4588,8 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
4109
4588
|
`
|
|
4110
4589
|
[piwi] "${testInfo.title}" ${testInfo.status} \u2014 ${rendered ? "locator picker" : "inspector"} open in the browser: ${rendered ? `click the element that should replace ${rendered}` : "click any element to generate locators for it"} (\u2191/\u2193 to select a parent/child, Esc to skip).`
|
|
4111
4590
|
);
|
|
4112
|
-
|
|
4591
|
+
const overlayArg = { transport: "global", failing: rendered };
|
|
4592
|
+
await page.evaluate(installPickerOverlay, overlayArg);
|
|
4113
4593
|
await page.waitForFunction(() => globalThis.__piwiPickState !== void 0, void 0, {
|
|
4114
4594
|
timeout: 0,
|
|
4115
4595
|
polling: 250
|
|
@@ -4130,9 +4610,9 @@ async function runLocatorPicker(page, testInfo, failed, probe) {
|
|
|
4130
4610
|
if (role) {
|
|
4131
4611
|
const probeArg = probe.arg;
|
|
4132
4612
|
await page.evaluate(showAnchorPicker, {
|
|
4133
|
-
tagRoles: probeArg.tagRoles,
|
|
4134
|
-
inputRoles: probeArg.inputRoles,
|
|
4135
|
-
roleSources: probeArg.roleSources,
|
|
4613
|
+
tagRoles: probeArg.tagRoles ?? {},
|
|
4614
|
+
inputRoles: probeArg.inputRoles ?? {},
|
|
4615
|
+
roleSources: probeArg.roleSources ?? "",
|
|
4136
4616
|
leafRole: role,
|
|
4137
4617
|
leafLevel: level,
|
|
4138
4618
|
leafTestId: attrs.attributes["data-testid"] ?? null
|
|
@@ -4497,7 +4977,12 @@ var CAPTURED_ATTRS_ARG = {
|
|
|
4497
4977
|
inputRoles: INPUT_TYPE_TO_ROLE,
|
|
4498
4978
|
// '[role]' plus every tag the maps can resolve (input/select are handled by
|
|
4499
4979
|
// special-cased logic in the probe, so add them explicitly).
|
|
4500
|
-
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(",")
|
|
4980
|
+
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
|
|
4981
|
+
// The reporter always wants ancestor-anchored alternatives (the picker's
|
|
4982
|
+
// anchors step and generateAnchoredAlternatives both need them); it derives
|
|
4983
|
+
// the accessible name itself, so the probe's own labelText is unneeded.
|
|
4984
|
+
includeStructural: true,
|
|
4985
|
+
includeLabelText: false
|
|
4501
4986
|
};
|
|
4502
4987
|
async function ariaSnapshotBestEffort(target, timeout) {
|
|
4503
4988
|
if (typeof target.ariaSnapshot !== "function") return null;
|
|
@@ -4517,156 +5002,6 @@ async function ariaSnapshotBestEffort(target, timeout) {
|
|
|
4517
5002
|
}
|
|
4518
5003
|
}
|
|
4519
5004
|
}
|
|
4520
|
-
function probeElementAttrs(el, arg) {
|
|
4521
|
-
const { keep, tagRoles, inputRoles, roleSources } = arg;
|
|
4522
|
-
const attrMap = {};
|
|
4523
|
-
for (const key of keep) {
|
|
4524
|
-
const v = el.getAttribute(key) ?? el[key];
|
|
4525
|
-
attrMap[key] = typeof v === "string" ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
|
|
4526
|
-
}
|
|
4527
|
-
const r = el.getBoundingClientRect();
|
|
4528
|
-
const selectorCounts = {};
|
|
4529
|
-
try {
|
|
4530
|
-
const doc = el.ownerDocument;
|
|
4531
|
-
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
4532
|
-
const count = (sel) => {
|
|
4533
|
-
try {
|
|
4534
|
-
return doc.querySelectorAll(sel).length;
|
|
4535
|
-
} catch {
|
|
4536
|
-
return void 0;
|
|
4537
|
-
}
|
|
4538
|
-
};
|
|
4539
|
-
if (attrMap["data-testid"]) {
|
|
4540
|
-
selectorCounts.testId = count(`[data-testid=${JSON.stringify(attrMap["data-testid"])}]`);
|
|
4541
|
-
}
|
|
4542
|
-
if (attrMap["id"]) selectorCounts.id = count(`#${cssEsc(attrMap["id"])}`);
|
|
4543
|
-
if (attrMap["name"]) selectorCounts.name = count(`[name=${JSON.stringify(attrMap["name"])}]`);
|
|
4544
|
-
const classList = (attrMap["class"] || "").split(/\s+/).filter((c) => c.length > 1).slice(0, 10);
|
|
4545
|
-
if (classList.length > 0) {
|
|
4546
|
-
const classCounts = {};
|
|
4547
|
-
for (const cls of classList) {
|
|
4548
|
-
const n = count(`.${cssEsc(cls)}`);
|
|
4549
|
-
if (n !== void 0) classCounts[cls] = n;
|
|
4550
|
-
}
|
|
4551
|
-
selectorCounts.classes = classCounts;
|
|
4552
|
-
}
|
|
4553
|
-
} catch {
|
|
4554
|
-
}
|
|
4555
|
-
let rolePosition = null;
|
|
4556
|
-
const ancestors = [];
|
|
4557
|
-
try {
|
|
4558
|
-
const doc = el.ownerDocument;
|
|
4559
|
-
const cssEsc = (s) => doc.defaultView.CSS.escape(s);
|
|
4560
|
-
const count = (sel) => {
|
|
4561
|
-
try {
|
|
4562
|
-
return doc.querySelectorAll(sel).length;
|
|
4563
|
-
} catch {
|
|
4564
|
-
return void 0;
|
|
4565
|
-
}
|
|
4566
|
-
};
|
|
4567
|
-
const roleOf = (n) => {
|
|
4568
|
-
const explicit = n.getAttribute("role");
|
|
4569
|
-
if (explicit) return explicit;
|
|
4570
|
-
const tag = (n.tagName || "").toLowerCase();
|
|
4571
|
-
if (tag === "input") return inputRoles[(n.getAttribute("type") || "text").toLowerCase()] ?? "textbox";
|
|
4572
|
-
if (tag === "select") return n.getAttribute("multiple") != null ? "listbox" : "combobox";
|
|
4573
|
-
if (tag === "a") return n.getAttribute("href") != null ? "link" : null;
|
|
4574
|
-
return tagRoles[tag] ?? null;
|
|
4575
|
-
};
|
|
4576
|
-
const levelOf = (n) => {
|
|
4577
|
-
const m = /^h([1-6])$/.exec((n.tagName || "").toLowerCase());
|
|
4578
|
-
if (m) return Number(m[1]);
|
|
4579
|
-
const al = n.getAttribute("aria-level");
|
|
4580
|
-
return al && /^\d+$/.test(al) ? Number(al) : null;
|
|
4581
|
-
};
|
|
4582
|
-
const targetRole = roleOf(el);
|
|
4583
|
-
const targetLevel = targetRole === "heading" ? levelOf(el) : null;
|
|
4584
|
-
if (targetRole) {
|
|
4585
|
-
const nodes = doc.querySelectorAll(roleSources);
|
|
4586
|
-
if (nodes.length <= 4e3) {
|
|
4587
|
-
let roleCountAll = 0;
|
|
4588
|
-
let index = -1;
|
|
4589
|
-
let levelCount = 0;
|
|
4590
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
4591
|
-
const n = nodes[i];
|
|
4592
|
-
if (roleOf(n) !== targetRole) continue;
|
|
4593
|
-
if (n === el) index = roleCountAll;
|
|
4594
|
-
roleCountAll++;
|
|
4595
|
-
if (targetLevel != null && levelOf(n) === targetLevel) levelCount++;
|
|
4596
|
-
}
|
|
4597
|
-
if (index !== -1) {
|
|
4598
|
-
rolePosition = {
|
|
4599
|
-
role: targetRole,
|
|
4600
|
-
count: roleCountAll,
|
|
4601
|
-
index,
|
|
4602
|
-
...targetLevel != null ? { levelCount } : {}
|
|
4603
|
-
};
|
|
4604
|
-
}
|
|
4605
|
-
const CONTAINER_TAGS = ["form", "nav", "main", "article", "section", "dialog", "table"];
|
|
4606
|
-
const docRoleCount = (role) => {
|
|
4607
|
-
let c = 0;
|
|
4608
|
-
for (let i = 0; i < nodes.length; i++) if (roleOf(nodes[i]) === role) c++;
|
|
4609
|
-
return c;
|
|
4610
|
-
};
|
|
4611
|
-
let node = el.parentElement;
|
|
4612
|
-
let depth = 0;
|
|
4613
|
-
while (node && depth < 12 && ancestors.length < 4) {
|
|
4614
|
-
depth++;
|
|
4615
|
-
const tag = (node.tagName || "").toLowerCase();
|
|
4616
|
-
if (tag === "body" || tag === "html") break;
|
|
4617
|
-
const testId = node.getAttribute("data-testid");
|
|
4618
|
-
const id = node.getAttribute("id");
|
|
4619
|
-
const explicitRole = node.getAttribute("role");
|
|
4620
|
-
const ariaLabel = node.getAttribute("aria-label");
|
|
4621
|
-
const anchorRole = explicitRole || (CONTAINER_TAGS.includes(tag) ? tagRoles[tag] : null) || null;
|
|
4622
|
-
if (testId || id || anchorRole || ariaLabel) {
|
|
4623
|
-
const scoped = node.querySelectorAll(roleSources);
|
|
4624
|
-
let scopedRoleCount = 0;
|
|
4625
|
-
if (scoped.length <= 2e3) {
|
|
4626
|
-
for (let i = 0; i < scoped.length; i++) {
|
|
4627
|
-
const n = scoped[i];
|
|
4628
|
-
if (roleOf(n) !== targetRole) continue;
|
|
4629
|
-
if (targetLevel != null && levelOf(n) !== targetLevel) continue;
|
|
4630
|
-
scopedRoleCount++;
|
|
4631
|
-
}
|
|
4632
|
-
} else {
|
|
4633
|
-
scopedRoleCount = -1;
|
|
4634
|
-
}
|
|
4635
|
-
ancestors.push({
|
|
4636
|
-
tag,
|
|
4637
|
-
depth,
|
|
4638
|
-
testId: testId || null,
|
|
4639
|
-
id: id || null,
|
|
4640
|
-
role: explicitRole || null,
|
|
4641
|
-
ariaLabel: ariaLabel || null,
|
|
4642
|
-
...scopedRoleCount >= 0 ? { scopedRoleCount } : {},
|
|
4643
|
-
...testId ? { testIdCount: count(`[data-testid=${JSON.stringify(testId)}]`) } : {},
|
|
4644
|
-
...id ? { idCount: count(`#${cssEsc(id)}`) } : {},
|
|
4645
|
-
...anchorRole ? { roleCount: docRoleCount(anchorRole) } : {}
|
|
4646
|
-
});
|
|
4647
|
-
}
|
|
4648
|
-
node = node.parentElement;
|
|
4649
|
-
}
|
|
4650
|
-
}
|
|
4651
|
-
}
|
|
4652
|
-
} catch {
|
|
4653
|
-
}
|
|
4654
|
-
return {
|
|
4655
|
-
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
4656
|
-
attributes: attrMap,
|
|
4657
|
-
// Collapse whitespace so multi-line text can't produce a getByText
|
|
4658
|
-
// suggestion with literal newlines in it.
|
|
4659
|
-
textContent: (el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80),
|
|
4660
|
-
center: {
|
|
4661
|
-
x: Math.round(r.x + r.width / 2),
|
|
4662
|
-
y: Math.round(r.y + r.height / 2)
|
|
4663
|
-
},
|
|
4664
|
-
hasLabel: !!(el.labels && el.labels.length > 0),
|
|
4665
|
-
selectorCounts,
|
|
4666
|
-
rolePosition,
|
|
4667
|
-
ancestors
|
|
4668
|
-
};
|
|
4669
|
-
}
|
|
4670
5005
|
function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
4671
5006
|
const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
4672
5007
|
const settledProbe = probe.then(
|