@piwitests/reporter 0.12.0 → 0.13.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/dist/global-setup-module.d.ts +2 -1
- package/dist/global-setup-module.js +432 -3
- package/dist/index.d.ts +204 -8
- package/dist/index.js +4724 -23
- package/dist/internal/capture/attachments.d.ts +8 -3
- package/dist/internal/capture/attachments.js +46 -21
- package/dist/internal/capture/capture-fixtures.d.ts +16 -14
- package/dist/internal/capture/capture-fixtures.js +2211 -971
- package/dist/internal/capture/inspect-on-failure.d.ts +50 -0
- package/dist/internal/capture/inspect-on-failure.js +66 -0
- package/dist/internal/capture/locator-healing.d.ts +33 -190
- package/dist/internal/capture/locator-healing.js +599 -815
- package/dist/internal/capture/pick-on-failure.d.ts +198 -0
- package/dist/internal/capture/pick-on-failure.js +1203 -0
- package/package.json +9 -4
- package/dist/internal/collect/error-text.d.ts +0 -18
- package/dist/internal/collect/error-text.js +0 -79
- package/dist/internal/collect/metadata-collector.d.ts +0 -32
- package/dist/internal/collect/metadata-collector.js +0 -246
- package/dist/internal/collect/skip-classify.d.ts +0 -27
- package/dist/internal/collect/skip-classify.js +0 -40
- package/dist/internal/collect/step-analyzer.d.ts +0 -103
- package/dist/internal/collect/step-analyzer.js +0 -221
- package/dist/internal/config/env.d.ts +0 -46
- package/dist/internal/config/env.js +0 -162
- package/dist/internal/files/compression.d.ts +0 -5
- package/dist/internal/files/compression.js +0 -69
- package/dist/internal/files/file-handler.d.ts +0 -38
- package/dist/internal/files/file-handler.js +0 -207
- package/dist/internal/streaming/crash-recovery.d.ts +0 -23
- package/dist/internal/streaming/crash-recovery.js +0 -106
- package/dist/internal/streaming/stream-buffer.d.ts +0 -17
- package/dist/internal/streaming/stream-buffer.js +0 -102
- package/dist/internal/streaming/stream-manager.d.ts +0 -88
- package/dist/internal/streaming/stream-manager.js +0 -395
- package/dist/internal/submit/run-submitter.d.ts +0 -67
- package/dist/internal/submit/run-submitter.js +0 -190
- package/dist/internal/submit/serializer.d.ts +0 -45
- package/dist/internal/submit/serializer.js +0 -108
- package/dist/internal/submit/uploader.d.ts +0 -89
- package/dist/internal/submit/uploader.js +0 -226
- package/dist/internal/support/ci.d.ts +0 -2
- package/dist/internal/support/ci.js +0 -32
- package/dist/internal/support/cli-filters.d.ts +0 -1
- package/dist/internal/support/cli-filters.js +0 -51
- package/dist/internal/support/errors.d.ts +0 -8
- package/dist/internal/support/errors.js +0 -15
- package/dist/internal/support/instance-id.d.ts +0 -4
- package/dist/internal/support/instance-id.js +0 -48
- package/dist/internal/support/limiter.d.ts +0 -2
- package/dist/internal/support/limiter.js +0 -27
- package/dist/internal/support/logger.d.ts +0 -26
- package/dist/internal/support/logger.js +0 -43
- package/dist/internal/support/reporter-version.d.ts +0 -2
- package/dist/internal/support/reporter-version.js +0 -54
- package/dist/internal/support/setup-file.d.ts +0 -13
- package/dist/internal/support/setup-file.js +0 -61
- package/dist/internal/support/source-snippet.d.ts +0 -12
- package/dist/internal/support/source-snippet.js +0 -97
- package/dist/internal/support/worker-index.d.ts +0 -7
- package/dist/internal/support/worker-index.js +0 -14
- package/dist/internal/transport/http-client.d.ts +0 -52
- package/dist/internal/transport/http-client.js +0 -201
- package/dist/public/config-wrapper.d.ts +0 -21
- package/dist/public/config-wrapper.js +0 -64
- package/dist/public/global-setup.d.ts +0 -13
- package/dist/public/global-setup.js +0 -146
- package/dist/public/options.d.ts +0 -86
- package/dist/public/options.js +0 -2
- package/dist/public/reporter.d.ts +0 -68
- package/dist/public/reporter.js +0 -376
- package/dist/types/collected.d.ts +0 -97
- package/dist/types/collected.js +0 -10
- package/dist/types/wire.d.ts +0 -176
- package/dist/types/wire.js +0 -14
- package/dist/types.d.ts +0 -11
- package/dist/types.js +0 -27
|
@@ -1,855 +1,639 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/internal/capture/locator-healing.ts
|
|
31
|
+
var locator_healing_exports = {};
|
|
32
|
+
__export(locator_healing_exports, {
|
|
33
|
+
ACTION_METHODS: () => ACTION_METHODS,
|
|
34
|
+
CAPTURED_ATTRIBUTES: () => CAPTURED_ATTRIBUTES,
|
|
35
|
+
CHAIN_METHODS: () => CHAIN_METHODS,
|
|
36
|
+
INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
|
|
37
|
+
LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
|
|
38
|
+
LOCATOR_METHODS: () => LOCATOR_METHODS,
|
|
39
|
+
TAG_TO_ROLE: () => TAG_TO_ROLE,
|
|
40
|
+
approximateAccessibleName: () => approximateAccessibleName,
|
|
41
|
+
captureCallerLocation: () => captureCallerLocation,
|
|
42
|
+
classifyCssStability: () => classifyCssStability,
|
|
43
|
+
dedupeSnapshotsByLocation: () => dedupeSnapshotsByLocation,
|
|
44
|
+
extractAccessibleName: () => extractAccessibleName,
|
|
45
|
+
generateAlternatives: () => generateAlternatives,
|
|
46
|
+
headingLevel: () => headingLevel,
|
|
47
|
+
isAutoGenerated: () => isAutoGenerated,
|
|
48
|
+
renderFailing: () => renderFailing,
|
|
49
|
+
resolveAriaRole: () => resolveAriaRole,
|
|
50
|
+
suggestLocatorsFromAria: () => suggestLocatorsFromAria
|
|
17
51
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
exports.resolveAriaRole = resolveAriaRole;
|
|
39
|
-
exports.headingLevel = headingLevel;
|
|
40
|
-
exports.generateAlternatives = generateAlternatives;
|
|
41
|
-
exports.classifyCssStability = classifyCssStability;
|
|
42
|
-
exports.isAutoGenerated = isAutoGenerated;
|
|
43
|
-
exports.extractAccessibleName = extractAccessibleName;
|
|
44
|
-
exports.approximateAccessibleName = approximateAccessibleName;
|
|
45
|
-
exports.parseAriaRoleName = parseAriaRoleName;
|
|
46
|
-
exports.nameSimilarity = nameSimilarity;
|
|
47
|
-
exports.suggestLocatorsFromAria = suggestLocatorsFromAria;
|
|
48
|
-
exports.captureCallerLocation = captureCallerLocation;
|
|
49
|
-
const path = __importStar(require("node:path"));
|
|
50
|
-
/**
|
|
51
|
-
* Drop repeated captures of the same call site before attaching: a loop (or a
|
|
52
|
-
* page-object method called repeatedly) produces one snapshot per action, but
|
|
53
|
-
* the server keeps only the latest per location anyway. Keeps the last
|
|
54
|
-
* element-bearing snapshot per location — falling back to the last placeholder
|
|
55
|
-
* when no capture resolved, so the location still counts as "seen this run"
|
|
56
|
-
* for the server's stale-location purge. Entries with no location pass through.
|
|
57
|
-
*/
|
|
58
|
-
function dedupeSnapshotsByLocation(snaps) {
|
|
59
|
-
const lastWithElement = new Map();
|
|
60
|
-
const lastAny = new Map();
|
|
61
|
-
snaps.forEach((s, i) => {
|
|
62
|
-
if (!s.location)
|
|
63
|
-
return;
|
|
64
|
-
lastAny.set(s.location, i);
|
|
65
|
-
if (s.element)
|
|
66
|
-
lastWithElement.set(s.location, i);
|
|
67
|
-
});
|
|
68
|
-
return snaps.filter((s, i) => {
|
|
69
|
-
if (!s.location)
|
|
70
|
-
return true;
|
|
71
|
-
return (lastWithElement.get(s.location) ?? lastAny.get(s.location)) === i;
|
|
72
|
-
});
|
|
52
|
+
module.exports = __toCommonJS(locator_healing_exports);
|
|
53
|
+
var path = __toESM(require("path"));
|
|
54
|
+
|
|
55
|
+
// ../packages/core/src/locator-fingerprint.ts
|
|
56
|
+
var PRESENT_SIMILARITY = 0.8;
|
|
57
|
+
var MATCH_SIMILARITY = 0.2;
|
|
58
|
+
function parseAriaCandidates(ariaSnapshot) {
|
|
59
|
+
if (!ariaSnapshot) return [];
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const line of ariaSnapshot.split("\n")) {
|
|
62
|
+
const m = line.match(/^\s*-\s+([a-z]+)(?:\s+"((?:[^"\\]|\\.)*)")?/i);
|
|
63
|
+
if (!m) continue;
|
|
64
|
+
const role = m[1];
|
|
65
|
+
const name = m[2] != null ? m[2].replace(/\\(.)/g, "$1") : null;
|
|
66
|
+
if (!name && (role === "generic" || role === "group" || role === "list" || role === "paragraph")) continue;
|
|
67
|
+
const levelMatch = line.slice(m[0].length).match(/\[level=(\d+)\]/);
|
|
68
|
+
const level = levelMatch ? Number(levelMatch[1]) : null;
|
|
69
|
+
out.push({ role, name, level });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
73
72
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
'selectText',
|
|
133
|
-
// Not an action, but a successful waitFor proves the element resolved — the
|
|
134
|
-
// closest capture hook available for assertion-style usage of a locator.
|
|
135
|
-
'waitFor',
|
|
136
|
-
];
|
|
137
|
-
/** Chain methods that create a new locator scope (origin tracks the chain call). */
|
|
138
|
-
exports.LOCATOR_CREATING_CHAINS = new Set(exports.LOCATOR_METHODS);
|
|
139
|
-
/**
|
|
140
|
-
* Element attributes to capture after a successful action, passed into the
|
|
141
|
-
* in-page `evaluate`. Shared so the reporter and dogfooding fixtures capture
|
|
142
|
-
* the same attribute set.
|
|
143
|
-
*/
|
|
144
|
-
exports.CAPTURED_ATTRIBUTES = [
|
|
145
|
-
'id',
|
|
146
|
-
'class',
|
|
147
|
-
'name',
|
|
148
|
-
'data-testid',
|
|
149
|
-
'placeholder',
|
|
150
|
-
'alt',
|
|
151
|
-
'title',
|
|
152
|
-
'aria-label',
|
|
153
|
-
// `aria-level` carries the heading level for `role="heading"` elements
|
|
154
|
-
// (h1-h6 levels come from the tag itself).
|
|
155
|
-
'aria-level',
|
|
156
|
-
'role',
|
|
157
|
-
'type',
|
|
158
|
-
'href',
|
|
159
|
-
// `multiple` distinguishes listbox vs combobox for <select>. `value` is
|
|
160
|
-
// deliberately NOT captured: no alternative generator uses it, and reading
|
|
161
|
-
// the live value after fill() would leak user-typed secrets (passwords,
|
|
162
|
-
// tokens) into the snapshot payload and server storage.
|
|
163
|
-
'multiple',
|
|
73
|
+
function textSimilarity(a, b) {
|
|
74
|
+
const tok = (s) => new Set(
|
|
75
|
+
(s ?? "").toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean)
|
|
76
|
+
);
|
|
77
|
+
const sa = tok(a);
|
|
78
|
+
const sb = tok(b);
|
|
79
|
+
if (sa.size === 0 && sb.size === 0) return 1;
|
|
80
|
+
if (sa.size === 0 || sb.size === 0) return 0;
|
|
81
|
+
let common = 0;
|
|
82
|
+
for (const t of sa) if (sb.has(t)) common++;
|
|
83
|
+
return 2 * common / (sa.size + sb.size);
|
|
84
|
+
}
|
|
85
|
+
function fingerprintPresent(fp, candidates) {
|
|
86
|
+
if (!fp.name) return false;
|
|
87
|
+
return candidates.some(
|
|
88
|
+
(c) => (!fp.role || c.role === fp.role) && textSimilarity(c.name, fp.name) >= PRESENT_SIMILARITY
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function matchRenamedElement(fp, candidates) {
|
|
92
|
+
if (candidates.length === 0) return null;
|
|
93
|
+
const sameRole = fp.role ? candidates.filter((c) => c.role === fp.role) : candidates;
|
|
94
|
+
if (sameRole.length === 0) return null;
|
|
95
|
+
let pool = sameRole;
|
|
96
|
+
if (fp.level != null) {
|
|
97
|
+
const sameLevel = sameRole.filter((c) => c.level === fp.level);
|
|
98
|
+
if (sameLevel.length > 0) pool = sameLevel;
|
|
99
|
+
}
|
|
100
|
+
if (pool.length === 1) {
|
|
101
|
+
return { candidate: pool[0], confidence: 0.7 };
|
|
102
|
+
}
|
|
103
|
+
let best = null;
|
|
104
|
+
let bestScore = -1;
|
|
105
|
+
for (const c of pool) {
|
|
106
|
+
const s = textSimilarity(c.name, fp.name);
|
|
107
|
+
if (s > bestScore) {
|
|
108
|
+
bestScore = s;
|
|
109
|
+
best = c;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (best && bestScore >= MATCH_SIMILARITY) return { candidate: best, confidence: bestScore };
|
|
113
|
+
const pos = fp.rolePosition;
|
|
114
|
+
if (pos && fp.role && pos.role === fp.role && sameRole.length >= 2 && sameRole.length === pos.count) {
|
|
115
|
+
const byIndex = sameRole[pos.index];
|
|
116
|
+
if (byIndex) return { candidate: byIndex, confidence: 0.5 };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ../packages/core/src/locator-methods.ts
|
|
122
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
123
|
+
"getByRole",
|
|
124
|
+
"getByTestId",
|
|
125
|
+
"getByText",
|
|
126
|
+
"getByLabel",
|
|
127
|
+
"getByPlaceholder",
|
|
128
|
+
"getByAltText",
|
|
129
|
+
"getByTitle",
|
|
130
|
+
"locator"
|
|
164
131
|
];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
h4: 'heading',
|
|
198
|
-
h5: 'heading',
|
|
199
|
-
h6: 'heading',
|
|
200
|
-
details: 'group',
|
|
201
|
-
summary: 'button',
|
|
202
|
-
search: 'search',
|
|
132
|
+
|
|
133
|
+
// ../packages/core/src/locator-generation.ts
|
|
134
|
+
var TAG_TO_ROLE = {
|
|
135
|
+
a: "link",
|
|
136
|
+
button: "button",
|
|
137
|
+
nav: "navigation",
|
|
138
|
+
main: "main",
|
|
139
|
+
article: "article",
|
|
140
|
+
section: "region",
|
|
141
|
+
form: "form",
|
|
142
|
+
img: "img",
|
|
143
|
+
figure: "figure",
|
|
144
|
+
figcaption: "caption",
|
|
145
|
+
blockquote: "blockquote",
|
|
146
|
+
table: "table",
|
|
147
|
+
ul: "list",
|
|
148
|
+
ol: "list",
|
|
149
|
+
li: "listitem",
|
|
150
|
+
dialog: "dialog",
|
|
151
|
+
output: "status",
|
|
152
|
+
progress: "progressbar",
|
|
153
|
+
meter: "meter",
|
|
154
|
+
textarea: "textbox",
|
|
155
|
+
h1: "heading",
|
|
156
|
+
h2: "heading",
|
|
157
|
+
h3: "heading",
|
|
158
|
+
h4: "heading",
|
|
159
|
+
h5: "heading",
|
|
160
|
+
h6: "heading",
|
|
161
|
+
details: "group",
|
|
162
|
+
summary: "button",
|
|
163
|
+
search: "search"
|
|
203
164
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
password: 'textbox',
|
|
165
|
+
var INPUT_TYPE_TO_ROLE = {
|
|
166
|
+
button: "button",
|
|
167
|
+
submit: "button",
|
|
168
|
+
reset: "button",
|
|
169
|
+
image: "button",
|
|
170
|
+
checkbox: "checkbox",
|
|
171
|
+
radio: "radio",
|
|
172
|
+
range: "slider",
|
|
173
|
+
search: "searchbox",
|
|
174
|
+
number: "spinbutton",
|
|
175
|
+
text: "textbox",
|
|
176
|
+
email: "textbox",
|
|
177
|
+
tel: "textbox",
|
|
178
|
+
url: "textbox",
|
|
179
|
+
password: "textbox"
|
|
220
180
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
181
|
+
var CAPTURED_ATTRIBUTES = [
|
|
182
|
+
"id",
|
|
183
|
+
"class",
|
|
184
|
+
"name",
|
|
185
|
+
"data-testid",
|
|
186
|
+
"placeholder",
|
|
187
|
+
"alt",
|
|
188
|
+
"title",
|
|
189
|
+
"aria-label",
|
|
190
|
+
"aria-level",
|
|
191
|
+
"role",
|
|
192
|
+
"type",
|
|
193
|
+
"href",
|
|
194
|
+
"multiple"
|
|
195
|
+
];
|
|
228
196
|
function resolveAriaRole(attrs) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (tag === 'a') {
|
|
245
|
-
return attrs.attributes['href'] != null ? 'link' : null;
|
|
246
|
-
}
|
|
247
|
-
return exports.TAG_TO_ROLE[tag] ?? null;
|
|
197
|
+
const explicit = attrs.attributes["role"];
|
|
198
|
+
if (explicit) return explicit;
|
|
199
|
+
const tag = attrs.tagName;
|
|
200
|
+
if (!tag) return null;
|
|
201
|
+
if (tag === "input") {
|
|
202
|
+
const type = (attrs.attributes["type"] ?? "text").toLowerCase();
|
|
203
|
+
return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
|
|
204
|
+
}
|
|
205
|
+
if (tag === "select") {
|
|
206
|
+
return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
|
|
207
|
+
}
|
|
208
|
+
if (tag === "a") {
|
|
209
|
+
return attrs.attributes["href"] != null ? "link" : null;
|
|
210
|
+
}
|
|
211
|
+
return TAG_TO_ROLE[tag] ?? null;
|
|
248
212
|
}
|
|
249
|
-
/**
|
|
250
|
-
* Heading level for a `heading`-role element: h1-h6 from the tag, else an
|
|
251
|
-
* explicit `aria-level` attribute. Null when unknown (don't guess the ARIA
|
|
252
|
-
* default) or when the element isn't a heading.
|
|
253
|
-
*/
|
|
254
213
|
function headingLevel(attrs, role) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
214
|
+
if (role !== "heading") return null;
|
|
215
|
+
const tagMatch = attrs.tagName.match(/^h([1-6])$/);
|
|
216
|
+
if (tagMatch) return Number(tagMatch[1]);
|
|
217
|
+
const ariaLevel = attrs.attributes["aria-level"];
|
|
218
|
+
if (ariaLevel && /^\d+$/.test(ariaLevel)) return Number(ariaLevel);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
var attr = (a, key) => a.attributes[key] || null;
|
|
222
|
+
var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
223
|
+
var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
224
|
+
var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
|
|
225
|
+
function classifyCssStability(className) {
|
|
226
|
+
if (/[a-f0-9]{8,}/i.test(className)) return 10;
|
|
227
|
+
if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
|
|
228
|
+
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(
|
|
229
|
+
className
|
|
230
|
+
))
|
|
231
|
+
return 25;
|
|
232
|
+
if (/__/.test(className) || /--/.test(className)) return 35;
|
|
233
|
+
if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
|
|
234
|
+
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
|
|
235
|
+
if (className.includes("_")) return 15;
|
|
236
|
+
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
|
|
237
|
+
return 15;
|
|
238
|
+
}
|
|
239
|
+
function isAutoGenerated(value) {
|
|
240
|
+
if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
|
|
241
|
+
if (/^[a-f0-9]{8,}$/i.test(value)) return true;
|
|
242
|
+
if (/^[a-z]+-\d+$/.test(value)) return true;
|
|
243
|
+
if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
|
|
244
|
+
if (value.startsWith("ng-")) return true;
|
|
245
|
+
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value)) return true;
|
|
246
|
+
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
|
|
247
|
+
return false;
|
|
264
248
|
}
|
|
265
|
-
// ── Alternative generation ───────────────────────────────────────────────────
|
|
266
|
-
const attr = (a, key) => a.attributes[key] || null;
|
|
267
|
-
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
268
|
-
/** Escape a value for use inside a double-quoted CSS attribute selector. */
|
|
269
|
-
const escCssAttrValue = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
270
|
-
/** An id usable as a bare `#id` CSS selector without escaping (framework ids like `:r3:` are not). */
|
|
271
|
-
const isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
|
|
272
|
-
/**
|
|
273
|
-
* Build a ranked list of alternative locators from the captured element
|
|
274
|
-
* attributes. The list is sorted descending by stability score.
|
|
275
|
-
*
|
|
276
|
-
* Only generates alternatives that differ from each other — no duplicates
|
|
277
|
-
* of the same locator expression.
|
|
278
|
-
*/
|
|
279
249
|
function generateAlternatives(attrs) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
const { accessibleName } = attrs;
|
|
289
|
-
const tag = attrs.tagName;
|
|
290
|
-
const role = resolveAriaRole(attrs);
|
|
291
|
-
// A probed count > 1 means the selector matches several elements on the
|
|
292
|
-
// captured page — a strict-mode violation, so the alternative is suppressed.
|
|
293
|
-
// Unknown counts (probe absent/failed) keep the alternative.
|
|
294
|
-
const counts = attrs.selectorCounts;
|
|
295
|
-
const isUnique = (n) => n == null || n <= 1;
|
|
296
|
-
// Collapse whitespace: multi-line/indented textContent would otherwise
|
|
297
|
-
// produce a getByText with literal newlines — invalid when pasted as code.
|
|
298
|
-
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, ' ').trim() : null;
|
|
299
|
-
// 1. data-testid — highest stability (100)
|
|
300
|
-
const testId = attr(attrs, 'data-testid');
|
|
301
|
-
if (testId && isUnique(counts?.testId)) {
|
|
302
|
-
add({
|
|
303
|
-
locator: `getByTestId('${esc(testId)}')`,
|
|
304
|
-
method: 'getByTestId',
|
|
305
|
-
args: { testId },
|
|
306
|
-
score: 100,
|
|
307
|
-
});
|
|
250
|
+
const alts = [];
|
|
251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
252
|
+
const add = (loc) => {
|
|
253
|
+
if (!seen.has(loc.locator)) {
|
|
254
|
+
seen.add(loc.locator);
|
|
255
|
+
alts.push(loc);
|
|
308
256
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
method: 'getByPlaceholder',
|
|
356
|
-
args: { placeholder },
|
|
357
|
-
score: 80,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
// 6. getByText — from visible text content (70-80)
|
|
361
|
-
if (text && text.length < 80) {
|
|
362
|
-
add({
|
|
363
|
-
locator: `getByText('${esc(text)}')`,
|
|
364
|
-
method: 'getByText',
|
|
365
|
-
args: { text },
|
|
366
|
-
score: 75,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
// 7. locator('#id') — if id exists and doesn't look auto-generated (50-70).
|
|
370
|
-
// Framework ids that aren't valid bare CSS identifiers (React useId's
|
|
371
|
-
// `:r3:`, ids with dots) fall back to an attribute selector.
|
|
372
|
-
const id = attr(attrs, 'id');
|
|
373
|
-
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
374
|
-
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
375
|
-
add({
|
|
376
|
-
locator: `locator('${esc(selector)}')`,
|
|
377
|
-
method: 'locator',
|
|
378
|
-
args: { selector },
|
|
379
|
-
score: 65,
|
|
380
|
-
});
|
|
257
|
+
};
|
|
258
|
+
const { accessibleName } = attrs;
|
|
259
|
+
const tag = attrs.tagName;
|
|
260
|
+
const role = resolveAriaRole(attrs);
|
|
261
|
+
const counts = attrs.selectorCounts;
|
|
262
|
+
const isUnique = (n) => n == null || n <= 1;
|
|
263
|
+
const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
|
|
264
|
+
const testId = attr(attrs, "data-testid");
|
|
265
|
+
if (testId && isUnique(counts?.testId)) {
|
|
266
|
+
add({
|
|
267
|
+
locator: `getByTestId('${esc(testId)}')`,
|
|
268
|
+
method: "getByTestId",
|
|
269
|
+
args: { testId },
|
|
270
|
+
score: 100
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const level = headingLevel(attrs, role);
|
|
274
|
+
const levelPart = level != null ? `, level: ${level}` : "";
|
|
275
|
+
const withLevel = (base) => level != null ? { ...base, level } : base;
|
|
276
|
+
if (role && accessibleName) {
|
|
277
|
+
add({
|
|
278
|
+
locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
|
|
279
|
+
method: "getByRole",
|
|
280
|
+
args: withLevel({ role, name: accessibleName }),
|
|
281
|
+
score: 90
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const ariaLabel = attr(attrs, "aria-label");
|
|
285
|
+
if (role && ariaLabel && ariaLabel !== accessibleName) {
|
|
286
|
+
add({
|
|
287
|
+
locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
|
|
288
|
+
method: "getByRole",
|
|
289
|
+
args: withLevel({ role, name: ariaLabel }),
|
|
290
|
+
score: 85
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
|
|
294
|
+
const label = attr(attrs, "aria-label");
|
|
295
|
+
const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
|
|
296
|
+
if (labelBacked) {
|
|
297
|
+
add({
|
|
298
|
+
locator: `getByLabel('${esc(accessibleName)}')`,
|
|
299
|
+
method: "getByLabel",
|
|
300
|
+
args: { label: accessibleName },
|
|
301
|
+
score: 85
|
|
302
|
+
});
|
|
381
303
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
304
|
+
}
|
|
305
|
+
const placeholder = attr(attrs, "placeholder");
|
|
306
|
+
if (placeholder) {
|
|
307
|
+
add({
|
|
308
|
+
locator: `getByPlaceholder('${esc(placeholder)}')`,
|
|
309
|
+
method: "getByPlaceholder",
|
|
310
|
+
args: { placeholder },
|
|
311
|
+
score: 80
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (text && text.length < 80) {
|
|
315
|
+
add({
|
|
316
|
+
locator: `getByText('${esc(text)}')`,
|
|
317
|
+
method: "getByText",
|
|
318
|
+
args: { text },
|
|
319
|
+
score: 75
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const id = attr(attrs, "id");
|
|
323
|
+
if (id && !isAutoGenerated(id) && isUnique(counts?.id)) {
|
|
324
|
+
const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
|
|
325
|
+
add({
|
|
326
|
+
locator: `locator('${esc(selector)}')`,
|
|
327
|
+
method: "locator",
|
|
328
|
+
args: { selector },
|
|
329
|
+
score: 65
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const name = attr(attrs, "name");
|
|
333
|
+
if (name && isUnique(counts?.name)) {
|
|
334
|
+
const selector = `[name="${escCssAttrValue(name)}"]`;
|
|
335
|
+
add({
|
|
336
|
+
locator: `locator('${esc(selector)}')`,
|
|
337
|
+
method: "locator",
|
|
338
|
+
args: { selector },
|
|
339
|
+
score: 60
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const alt = attr(attrs, "alt");
|
|
343
|
+
if (alt) {
|
|
344
|
+
add({
|
|
345
|
+
locator: `getByAltText('${esc(alt)}')`,
|
|
346
|
+
method: "getByAltText",
|
|
347
|
+
args: { text: alt },
|
|
348
|
+
score: 60
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const title = attr(attrs, "title");
|
|
352
|
+
if (title) {
|
|
353
|
+
add({
|
|
354
|
+
locator: `getByTitle('${esc(title)}')`,
|
|
355
|
+
method: "getByTitle",
|
|
356
|
+
args: { title },
|
|
357
|
+
score: 50
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const hasOwnTestId = !!(testId && isUnique(counts?.testId));
|
|
361
|
+
if (role && !hasOwnTestId) {
|
|
362
|
+
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
363
|
+
const leafArgs = withLevel({ role });
|
|
364
|
+
let testIdAnchorDone = false;
|
|
365
|
+
let idAnchorDone = false;
|
|
366
|
+
let roleAnchorDone = false;
|
|
367
|
+
for (const anc of attrs.ancestors ?? []) {
|
|
368
|
+
if (anc.scopedRoleCount !== 1) continue;
|
|
369
|
+
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
370
|
+
testIdAnchorDone = true;
|
|
386
371
|
add({
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
372
|
+
locator: `getByTestId('${esc(anc.testId)}').getByRole(${rolePart})`,
|
|
373
|
+
method: "getByRole",
|
|
374
|
+
args: { ...leafArgs, anchorTestId: anc.testId },
|
|
375
|
+
score: 72
|
|
391
376
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
377
|
+
}
|
|
378
|
+
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
379
|
+
idAnchorDone = true;
|
|
380
|
+
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
396
381
|
add({
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
382
|
+
locator: `locator('${esc(anchorSelector)}').getByRole(${rolePart})`,
|
|
383
|
+
method: "getByRole",
|
|
384
|
+
args: { ...leafArgs, anchorSelector },
|
|
385
|
+
score: 64
|
|
401
386
|
});
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
387
|
+
}
|
|
388
|
+
const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
|
|
389
|
+
if (!roleAnchorDone && ancestorRole && ancestorRole !== role && anc.roleCount === 1) {
|
|
390
|
+
roleAnchorDone = true;
|
|
406
391
|
add({
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
392
|
+
locator: `getByRole('${esc(ancestorRole)}').getByRole(${rolePart})`,
|
|
393
|
+
method: "getByRole",
|
|
394
|
+
args: { ...leafArgs, anchorRole: ancestorRole },
|
|
395
|
+
score: 55
|
|
411
396
|
});
|
|
397
|
+
}
|
|
412
398
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
|
|
422
|
-
const leafArgs = withLevel({ role });
|
|
423
|
-
// Ancestor-anchored role locators: nearest anchor of each kind whose own
|
|
424
|
-
// hook is document-unique and that contains exactly one leaf-role match
|
|
425
|
-
// (level-scoped for headings — the probe counts accordingly).
|
|
426
|
-
let testIdAnchorDone = false;
|
|
427
|
-
let idAnchorDone = false;
|
|
428
|
-
let roleAnchorDone = false;
|
|
429
|
-
for (const anc of attrs.ancestors ?? []) {
|
|
430
|
-
if (anc.scopedRoleCount !== 1)
|
|
431
|
-
continue;
|
|
432
|
-
if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
|
|
433
|
-
testIdAnchorDone = true;
|
|
434
|
-
add({
|
|
435
|
-
locator: `getByTestId('${esc(anc.testId)}').getByRole(${rolePart})`,
|
|
436
|
-
method: 'getByRole',
|
|
437
|
-
args: { ...leafArgs, anchorTestId: anc.testId },
|
|
438
|
-
score: 72,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
if (!idAnchorDone && anc.id && !isAutoGenerated(anc.id) && anc.idCount === 1) {
|
|
442
|
-
idAnchorDone = true;
|
|
443
|
-
const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
|
|
444
|
-
add({
|
|
445
|
-
locator: `locator('${esc(anchorSelector)}').getByRole(${rolePart})`,
|
|
446
|
-
method: 'getByRole',
|
|
447
|
-
args: { ...leafArgs, anchorSelector },
|
|
448
|
-
score: 64,
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
const ancestorRole = anc.role || exports.TAG_TO_ROLE[anc.tag] || null;
|
|
452
|
-
if (!roleAnchorDone && ancestorRole && ancestorRole !== role && anc.roleCount === 1) {
|
|
453
|
-
roleAnchorDone = true;
|
|
454
|
-
add({
|
|
455
|
-
locator: `getByRole('${esc(ancestorRole)}').getByRole(${rolePart})`,
|
|
456
|
-
method: 'getByRole',
|
|
457
|
-
args: { ...leafArgs, anchorRole: ancestorRole },
|
|
458
|
-
score: 55,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// Name-free bare role — the sole element of its role on the page, or the
|
|
463
|
-
// sole heading at its level (a lone h1 among many h2s).
|
|
464
|
-
const pos = attrs.rolePosition;
|
|
465
|
-
if (pos && pos.role === role && (pos.count === 1 || (level != null && pos.levelCount === 1))) {
|
|
466
|
-
add({
|
|
467
|
-
locator: `getByRole(${rolePart})`,
|
|
468
|
-
method: 'getByRole',
|
|
469
|
-
args: leafArgs,
|
|
470
|
-
score: 58,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
399
|
+
const pos = attrs.rolePosition;
|
|
400
|
+
if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
|
|
401
|
+
add({
|
|
402
|
+
locator: `getByRole(${rolePart})`,
|
|
403
|
+
method: "getByRole",
|
|
404
|
+
args: leafArgs,
|
|
405
|
+
score: 58
|
|
406
|
+
});
|
|
473
407
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}))
|
|
487
|
-
.sort((a, b) => b.score - a.score)
|
|
488
|
-
.slice(0, 3);
|
|
489
|
-
for (const { cls, score } of classes) {
|
|
490
|
-
add({
|
|
491
|
-
locator: `locator('.${esc(cls)}')`,
|
|
492
|
-
method: 'locator',
|
|
493
|
-
args: { selector: `.${cls}` },
|
|
494
|
-
score,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
408
|
+
}
|
|
409
|
+
const clsStr = attr(attrs, "class");
|
|
410
|
+
if (clsStr) {
|
|
411
|
+
const countsClasses = counts?.classes;
|
|
412
|
+
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);
|
|
413
|
+
for (const { cls, score } of classes) {
|
|
414
|
+
add({
|
|
415
|
+
locator: `locator('.${esc(cls)}')`,
|
|
416
|
+
method: "locator",
|
|
417
|
+
args: { selector: `.${cls}` },
|
|
418
|
+
score
|
|
419
|
+
});
|
|
497
420
|
}
|
|
498
|
-
|
|
421
|
+
}
|
|
422
|
+
return alts.sort((a, b) => b.score - a.score);
|
|
499
423
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
*/
|
|
511
|
-
function classifyCssStability(className) {
|
|
512
|
-
// Hex hash patterns: contains 8+ consecutive hex digits
|
|
513
|
-
if (/[a-f0-9]{8,}/i.test(className))
|
|
514
|
-
return 10;
|
|
515
|
-
// CSS-in-JS patterns — check before utility/semantic since prefixes like
|
|
516
|
-
// "css-", "emotion-" are generated and fragile
|
|
517
|
-
if (/^(?:css|sc|emotion|styled)-/i.test(className))
|
|
518
|
-
return 15;
|
|
519
|
-
// Tailwind/utility classes
|
|
520
|
-
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(className))
|
|
521
|
-
return 25;
|
|
522
|
-
// BEM-style: block__element--modifier
|
|
523
|
-
if (/__/.test(className) || /--/.test(className))
|
|
524
|
-
return 35;
|
|
525
|
-
// Plain semantic class: hyphenated word pairs or camelCase
|
|
526
|
-
if (/^[a-z]+(-[a-z]+)+$/.test(className))
|
|
527
|
-
return 40;
|
|
528
|
-
if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className))
|
|
529
|
-
return 40;
|
|
530
|
-
// Hyphenated strings with digit-containing suffixes that look hashed
|
|
531
|
-
if (className.includes('_'))
|
|
532
|
-
return 15;
|
|
533
|
-
if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className))
|
|
534
|
-
return 15;
|
|
535
|
-
// Unknown — score conservatively
|
|
536
|
-
return 15;
|
|
424
|
+
function approximateAccessibleName(attrs) {
|
|
425
|
+
const a = attrs.attributes;
|
|
426
|
+
const ariaLabel = a["aria-label"];
|
|
427
|
+
if (ariaLabel) return ariaLabel;
|
|
428
|
+
if (attrs.textContent) return attrs.textContent;
|
|
429
|
+
const title = a["title"];
|
|
430
|
+
if (title) return title;
|
|
431
|
+
const placeholder = a["placeholder"];
|
|
432
|
+
if (placeholder) return placeholder;
|
|
433
|
+
return null;
|
|
537
434
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return true;
|
|
553
|
-
// Random-looking CSS-in-JS IDs
|
|
554
|
-
if (/^(emotion-|styled-|css-|sc-)/.test(value))
|
|
555
|
-
return true;
|
|
556
|
-
// Angular-style generated IDs (ng-xxx-N)
|
|
557
|
-
if (value.startsWith('ng-'))
|
|
558
|
-
return true;
|
|
559
|
-
// Component-library generated ids (Radix, Headless UI, MUI, Mantine, Chakra)
|
|
560
|
-
if (/^(radix-|headlessui-|mui-|mantine-|chakra-)/i.test(value))
|
|
561
|
-
return true;
|
|
562
|
-
// React useId format — `:r1:` (18) / `«r1»` (19)
|
|
563
|
-
if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value))
|
|
564
|
-
return true;
|
|
565
|
-
return false;
|
|
435
|
+
|
|
436
|
+
// src/internal/capture/locator-healing.ts
|
|
437
|
+
function dedupeSnapshotsByLocation(snaps) {
|
|
438
|
+
const lastWithElement = /* @__PURE__ */ new Map();
|
|
439
|
+
const lastAny = /* @__PURE__ */ new Map();
|
|
440
|
+
snaps.forEach((s, i) => {
|
|
441
|
+
if (!s.location) return;
|
|
442
|
+
lastAny.set(s.location, i);
|
|
443
|
+
if (s.element) lastWithElement.set(s.location, i);
|
|
444
|
+
});
|
|
445
|
+
return snaps.filter((s, i) => {
|
|
446
|
+
if (!s.location) return true;
|
|
447
|
+
return (lastWithElement.get(s.location) ?? lastAny.get(s.location)) === i;
|
|
448
|
+
});
|
|
566
449
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
450
|
+
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
451
|
+
var CHAIN_METHODS = [
|
|
452
|
+
"first",
|
|
453
|
+
"nth",
|
|
454
|
+
"last",
|
|
455
|
+
"filter",
|
|
456
|
+
"and",
|
|
457
|
+
"or",
|
|
458
|
+
"locator",
|
|
459
|
+
"getByRole",
|
|
460
|
+
"getByTestId",
|
|
461
|
+
"getByText",
|
|
462
|
+
"getByLabel",
|
|
463
|
+
"getByPlaceholder",
|
|
464
|
+
"getByAltText",
|
|
465
|
+
"getByTitle"
|
|
466
|
+
];
|
|
467
|
+
var ACTION_METHODS = [
|
|
468
|
+
"click",
|
|
469
|
+
"fill",
|
|
470
|
+
"check",
|
|
471
|
+
"uncheck",
|
|
472
|
+
"selectOption",
|
|
473
|
+
"dblclick",
|
|
474
|
+
"tap",
|
|
475
|
+
"hover",
|
|
476
|
+
"press",
|
|
477
|
+
"type",
|
|
478
|
+
"pressSequentially",
|
|
479
|
+
"clear",
|
|
480
|
+
"setInputFiles",
|
|
481
|
+
"dragTo",
|
|
482
|
+
"focus",
|
|
483
|
+
"blur",
|
|
484
|
+
"scrollIntoViewIfNeeded",
|
|
485
|
+
"dispatchEvent",
|
|
486
|
+
"selectText",
|
|
487
|
+
// Not an action, but a successful waitFor proves the element resolved — the
|
|
488
|
+
// closest capture hook available for assertion-style usage of a locator.
|
|
489
|
+
"waitFor"
|
|
490
|
+
];
|
|
491
|
+
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
579
492
|
function extractAccessibleName(ariaSnapshot) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
585
|
-
if (match)
|
|
586
|
-
return match[1];
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
/**
|
|
590
|
-
* Approximate the accessible name from HTML attributes when ariaSnapshot()
|
|
591
|
-
* is unavailable. Priority: aria-label > text content > title > placeholder.
|
|
592
|
-
*/
|
|
593
|
-
function approximateAccessibleName(attrs) {
|
|
594
|
-
const a = attrs.attributes;
|
|
595
|
-
const ariaLabel = a['aria-label'];
|
|
596
|
-
if (ariaLabel)
|
|
597
|
-
return ariaLabel;
|
|
598
|
-
if (attrs.textContent)
|
|
599
|
-
return attrs.textContent;
|
|
600
|
-
const title = a['title'];
|
|
601
|
-
if (title)
|
|
602
|
-
return title;
|
|
603
|
-
const placeholder = a['placeholder'];
|
|
604
|
-
if (placeholder)
|
|
605
|
-
return placeholder;
|
|
606
|
-
return null;
|
|
493
|
+
if (!ariaSnapshot) return null;
|
|
494
|
+
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
495
|
+
if (match) return match[1];
|
|
496
|
+
return null;
|
|
607
497
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
'getByAltText',
|
|
498
|
+
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
499
|
+
"getByText",
|
|
500
|
+
"getByRole",
|
|
501
|
+
"getByLabel",
|
|
502
|
+
"getByPlaceholder",
|
|
503
|
+
"getByTitle",
|
|
504
|
+
"getByAltText"
|
|
616
505
|
]);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
// markers like `[ref=eN]` are ignored). Scan only past the matched part so
|
|
635
|
-
// brackets inside the quoted name can't fake a level.
|
|
636
|
-
const levelMatch = /\[level=(\d+)\]/.exec(line.slice(m[0].length));
|
|
637
|
-
const level = levelMatch ? Number(levelMatch[1]) : null;
|
|
638
|
-
out.push({ role, name, level });
|
|
639
|
-
}
|
|
640
|
-
return out;
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Token-set (Dice) similarity, 0-1, case- and punctuation-insensitive.
|
|
644
|
-
* Duplicated from `textSimilarity` in application/shared/locator-fingerprint.ts —
|
|
645
|
-
* this package publishes standalone to npm and can't import monorepo-relative
|
|
646
|
-
* shared/ code, so keep the two implementations in sync by hand. Exported so
|
|
647
|
-
* the dashboard's drift-guard unit test can compare the two.
|
|
648
|
-
*/
|
|
649
|
-
function nameSimilarity(a, b) {
|
|
650
|
-
const tok = (s) => new Set((s ?? '')
|
|
651
|
-
.toLowerCase()
|
|
652
|
-
.split(/[^a-z0-9]+/i)
|
|
653
|
-
.filter(Boolean));
|
|
654
|
-
const sa = tok(a);
|
|
655
|
-
const sb = tok(b);
|
|
656
|
-
if (sa.size === 0 && sb.size === 0)
|
|
657
|
-
return 1;
|
|
658
|
-
if (sa.size === 0 || sb.size === 0)
|
|
659
|
-
return 0;
|
|
660
|
-
let common = 0;
|
|
661
|
-
for (const t of sa)
|
|
662
|
-
if (sb.has(t))
|
|
663
|
-
common++;
|
|
664
|
-
return (2 * common) / (sa.size + sb.size);
|
|
665
|
-
}
|
|
666
|
-
const SUGG_TEXT_ROLES = new Set([
|
|
667
|
-
'button',
|
|
668
|
-
'link',
|
|
669
|
-
'heading',
|
|
670
|
-
'menuitem',
|
|
671
|
-
'tab',
|
|
672
|
-
'option',
|
|
673
|
-
'cell',
|
|
674
|
-
'columnheader',
|
|
675
|
-
'rowheader',
|
|
676
|
-
'gridcell',
|
|
677
|
-
'treeitem',
|
|
678
|
-
'listitem',
|
|
679
|
-
'checkbox',
|
|
680
|
-
'radio',
|
|
681
|
-
'switch',
|
|
506
|
+
var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
|
|
507
|
+
var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
|
|
508
|
+
"button",
|
|
509
|
+
"link",
|
|
510
|
+
"heading",
|
|
511
|
+
"menuitem",
|
|
512
|
+
"tab",
|
|
513
|
+
"option",
|
|
514
|
+
"cell",
|
|
515
|
+
"columnheader",
|
|
516
|
+
"rowheader",
|
|
517
|
+
"gridcell",
|
|
518
|
+
"treeitem",
|
|
519
|
+
"listitem",
|
|
520
|
+
"checkbox",
|
|
521
|
+
"radio",
|
|
522
|
+
"switch"
|
|
682
523
|
]);
|
|
683
|
-
|
|
684
|
-
/** Extract the role (for getByRole), targeted accessible name, and heading level from a failed locator's args. */
|
|
524
|
+
var SUGG_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
|
|
685
525
|
function failedNameAndRole(failed) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
526
|
+
if (failed.method === "getByRole") {
|
|
527
|
+
const role = typeof failed.args[0] === "string" ? failed.args[0] : null;
|
|
528
|
+
const opts = failed.args[1];
|
|
529
|
+
const name = opts && typeof opts.name === "string" ? opts.name : null;
|
|
530
|
+
const level = opts && typeof opts.level === "number" ? opts.level : null;
|
|
531
|
+
return { role, name, level };
|
|
532
|
+
}
|
|
533
|
+
const first = failed.args.find((a) => typeof a === "string");
|
|
534
|
+
return { role: null, name: typeof first === "string" ? first : null, level: null };
|
|
695
535
|
}
|
|
696
|
-
/** Render the failed locator back to source for the annotation message. */
|
|
697
536
|
function renderFailing(failed) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
return `${failed.method}('${escAttr(name ?? '')}')`;
|
|
537
|
+
const { role, name } = failedNameAndRole(failed);
|
|
538
|
+
if (failed.method === "getByRole") {
|
|
539
|
+
return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
|
|
540
|
+
}
|
|
541
|
+
return `${failed.method}('${escAttr(name ?? "")}')`;
|
|
705
542
|
}
|
|
706
|
-
/** Build fresh locator suggestions for a matched candidate, with the failed method's style first. */
|
|
707
543
|
function freshSuggestions(candidate, failedMethod) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
push(roleLoc);
|
|
725
|
-
if (SUGG_TEXT_ROLES.has(role))
|
|
726
|
-
push(textLoc);
|
|
727
|
-
else if (SUGG_FIELD_ROLES.has(role))
|
|
728
|
-
push(labelLoc);
|
|
729
|
-
return out;
|
|
544
|
+
const out = [];
|
|
545
|
+
const role = candidate.role;
|
|
546
|
+
const name = candidate.name;
|
|
547
|
+
const push = (s) => {
|
|
548
|
+
if (!out.includes(s)) out.push(s);
|
|
549
|
+
};
|
|
550
|
+
const levelPart = candidate.level != null ? `, level: ${candidate.level}` : "";
|
|
551
|
+
const roleLoc = `getByRole('${escAttr(role)}', { name: '${escAttr(name)}'${levelPart} })`;
|
|
552
|
+
const textLoc = `getByText('${escAttr(name)}')`;
|
|
553
|
+
const labelLoc = `getByLabel('${escAttr(name)}')`;
|
|
554
|
+
if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
555
|
+
if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
556
|
+
push(roleLoc);
|
|
557
|
+
if (SUGG_TEXT_ROLES.has(role)) push(textLoc);
|
|
558
|
+
else if (SUGG_FIELD_ROLES.has(role)) push(labelLoc);
|
|
559
|
+
return out;
|
|
730
560
|
}
|
|
731
|
-
/**
|
|
732
|
-
* Best-effort runtime suggestion for a locator that matched nothing: find the
|
|
733
|
-
* element on the *current* page that the failed locator most likely targeted
|
|
734
|
-
* (by accessible name similarity, restricted to the failed role when given) and
|
|
735
|
-
* return fresh locators for it.
|
|
736
|
-
*
|
|
737
|
-
* Unlike the server lookup this has no pre-captured fingerprint — only the
|
|
738
|
-
* failed locator + the live page — so it's a hint, not a guarantee. Returns null
|
|
739
|
-
* for non-name-based locators (testid/CSS), when the targeted name is still
|
|
740
|
-
* present (so the failure wasn't a rename), or when no candidate is confident.
|
|
741
|
-
*/
|
|
742
561
|
function suggestLocatorsFromAria(failed, ariaSnapshot) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
562
|
+
if (!ariaSnapshot || !NAME_BASED_METHODS.has(failed.method)) return null;
|
|
563
|
+
const { role, name, level } = failedNameAndRole(failed);
|
|
564
|
+
if (!name) return null;
|
|
565
|
+
const candidates = parseAriaCandidates(ariaSnapshot);
|
|
566
|
+
if (candidates.length === 0) return null;
|
|
567
|
+
const fingerprint = { role, name, level };
|
|
568
|
+
if (fingerprintPresent(fingerprint, candidates)) return null;
|
|
569
|
+
const best = matchRenamedElement(fingerprint, candidates)?.candidate;
|
|
570
|
+
if (!best || !best.name) return null;
|
|
571
|
+
const suggestions = freshSuggestions({ role: best.role, name: best.name, level: best.level }, failed.method);
|
|
572
|
+
if (suggestions.length === 0) return null;
|
|
573
|
+
return { failing: renderFailing(failed), suggestions };
|
|
574
|
+
}
|
|
575
|
+
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
576
|
+
const lines = stack.split("\n");
|
|
577
|
+
let prevWasCaptureModule = false;
|
|
578
|
+
for (let i = 1; i < lines.length; i++) {
|
|
579
|
+
const line = lines[i].trim();
|
|
580
|
+
if (!line.startsWith("at")) continue;
|
|
581
|
+
const m = line.match(/^at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
582
|
+
if (!m) {
|
|
583
|
+
prevWasCaptureModule = false;
|
|
584
|
+
continue;
|
|
760
585
|
}
|
|
761
|
-
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
let bestScore = -1;
|
|
766
|
-
for (const c of pool) {
|
|
767
|
-
const s = nameSimilarity(c.name, name);
|
|
768
|
-
if (s > bestScore) {
|
|
769
|
-
bestScore = s;
|
|
770
|
-
best = c;
|
|
771
|
-
}
|
|
586
|
+
let file = m[2];
|
|
587
|
+
if (!file || file.startsWith("node:")) {
|
|
588
|
+
prevWasCaptureModule = false;
|
|
589
|
+
continue;
|
|
772
590
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
// ── Call-site capture ────────────────────────────────────────────────────────
|
|
783
|
-
/**
|
|
784
|
-
* Capture the calling test's source location (`file:line:col`) from the current
|
|
785
|
-
* stack, for stamping onto a locator snapshot. The path is made cwd-relative so
|
|
786
|
-
* it matches the location format Playwright embeds in error messages — which is
|
|
787
|
-
* what the server's exact-match healing lookup (`extractErrorLocation`) parses.
|
|
788
|
-
*
|
|
789
|
-
* Stamping at action *call* time (rather than correlating with `pw:api` step
|
|
790
|
-
* indices at step *end* time) avoids three classes of misalignment: `pw:api`
|
|
791
|
-
* steps with no wrapped locator (e.g. `page.keyboard.press`), cross-worker step
|
|
792
|
-
* interleaving, and concurrent actions reordering by end time.
|
|
793
|
-
*
|
|
794
|
-
* Returns null when no user frame can be identified — the snapshot keeps
|
|
795
|
-
* `location: null` and the server falls back to fingerprint / ARIA lookup.
|
|
796
|
-
*/
|
|
797
|
-
function captureCallerLocation(stack = new Error().stack ?? '') {
|
|
798
|
-
const lines = stack.split('\n');
|
|
799
|
-
// The capture machinery's own frames sit at the top of the stack: this module
|
|
800
|
-
// (locator-healing) then the single fixtures-proxy frame that called it. Skip
|
|
801
|
-
// exactly those, so the first remaining frame is the real test call site. A
|
|
802
|
-
// USER file named `fixtures.*` further down the stack must be kept, so the
|
|
803
|
-
// fixtures-proxy frame is only skipped when it directly follows this module.
|
|
804
|
-
let prevWasCaptureModule = false;
|
|
805
|
-
for (let i = 1; i < lines.length; i++) {
|
|
806
|
-
const line = lines[i].trim();
|
|
807
|
-
if (!line.startsWith('at'))
|
|
808
|
-
continue;
|
|
809
|
-
const m = line.match(/^at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
|
|
810
|
-
if (!m) {
|
|
811
|
-
prevWasCaptureModule = false;
|
|
812
|
-
continue;
|
|
813
|
-
}
|
|
814
|
-
let file = m[2];
|
|
815
|
-
if (!file || file.startsWith('node:')) {
|
|
816
|
-
prevWasCaptureModule = false;
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
file = file.replace(/^file:\/\/\/?/, '');
|
|
820
|
-
// Must look like a source file (has an extension).
|
|
821
|
-
if (!/\.[a-z]+$/i.test(file)) {
|
|
822
|
-
prevWasCaptureModule = false;
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
// This module — always an internal frame; remember it so the immediately
|
|
826
|
-
// following fixtures-proxy frame can be skipped too.
|
|
827
|
-
if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
|
|
828
|
-
prevWasCaptureModule = true;
|
|
829
|
-
continue;
|
|
830
|
-
}
|
|
831
|
-
// The fixtures proxy that called us — `capture-fixtures.*` in this
|
|
832
|
-
// package, `fixtures.*` in the dogfood mirror. Skip only when it directly
|
|
833
|
-
// follows this module, so a user's own `fixtures.*` deeper down is kept.
|
|
834
|
-
if (prevWasCaptureModule && /[\\/](?:capture-)?fixtures\.[a-z]+$/i.test(file)) {
|
|
835
|
-
prevWasCaptureModule = false;
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (/[\\/]node_modules[\\/]/.test(file)) {
|
|
839
|
-
prevWasCaptureModule = false;
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
let rel = file;
|
|
843
|
-
try {
|
|
844
|
-
rel = path.relative(process.cwd(), file);
|
|
845
|
-
}
|
|
846
|
-
catch {
|
|
847
|
-
/* keep absolute if relative fails */
|
|
848
|
-
}
|
|
849
|
-
rel = rel.split(path.sep).join('/');
|
|
850
|
-
if (rel.startsWith('./'))
|
|
851
|
-
rel = rel.slice(2);
|
|
852
|
-
return `${rel}:${m[3]}:${m[4]}`;
|
|
591
|
+
file = file.replace(/^file:\/\/\/?/, "");
|
|
592
|
+
if (!/\.[a-z]+$/i.test(file)) {
|
|
593
|
+
prevWasCaptureModule = false;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
|
|
597
|
+
prevWasCaptureModule = true;
|
|
598
|
+
continue;
|
|
853
599
|
}
|
|
854
|
-
|
|
600
|
+
if (prevWasCaptureModule && /[\\/](?:capture-)?fixtures\.[a-z]+$/i.test(file)) {
|
|
601
|
+
prevWasCaptureModule = false;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (/[\\/]node_modules[\\/]/.test(file)) {
|
|
605
|
+
prevWasCaptureModule = false;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
let rel = file;
|
|
609
|
+
try {
|
|
610
|
+
rel = path.relative(process.cwd(), file);
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
rel = rel.split(path.sep).join("/");
|
|
614
|
+
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
615
|
+
return `${rel}:${m[3]}:${m[4]}`;
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
855
618
|
}
|
|
619
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
620
|
+
0 && (module.exports = {
|
|
621
|
+
ACTION_METHODS,
|
|
622
|
+
CAPTURED_ATTRIBUTES,
|
|
623
|
+
CHAIN_METHODS,
|
|
624
|
+
INPUT_TYPE_TO_ROLE,
|
|
625
|
+
LOCATOR_CREATING_CHAINS,
|
|
626
|
+
LOCATOR_METHODS,
|
|
627
|
+
TAG_TO_ROLE,
|
|
628
|
+
approximateAccessibleName,
|
|
629
|
+
captureCallerLocation,
|
|
630
|
+
classifyCssStability,
|
|
631
|
+
dedupeSnapshotsByLocation,
|
|
632
|
+
extractAccessibleName,
|
|
633
|
+
generateAlternatives,
|
|
634
|
+
headingLevel,
|
|
635
|
+
isAutoGenerated,
|
|
636
|
+
renderFailing,
|
|
637
|
+
resolveAriaRole,
|
|
638
|
+
suggestLocatorsFromAria
|
|
639
|
+
});
|