pw-core 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +145 -421
  3. package/dist/codegen/action-formatter.d.ts +19 -0
  4. package/dist/codegen/action-formatter.js +160 -0
  5. package/dist/codegen/action-processor.d.ts +23 -0
  6. package/dist/codegen/action-processor.js +112 -0
  7. package/dist/codegen/floating-panel/client.d.ts +14 -1
  8. package/dist/codegen/floating-panel/client.js +20 -25
  9. package/dist/codegen/floating-panel/manager.js +16 -22
  10. package/dist/codegen/generator/action.validator.d.ts +2 -1
  11. package/dist/codegen/generator/action.validator.js +2 -1
  12. package/dist/codegen/generator/candidate-scorer.d.ts +7 -0
  13. package/dist/codegen/generator/candidate-scorer.js +206 -0
  14. package/dist/codegen/generator/dom-scanner.d.ts +39 -0
  15. package/dist/codegen/generator/dom-scanner.js +429 -0
  16. package/dist/codegen/generator/id-stability.d.ts +6 -0
  17. package/dist/codegen/generator/id-stability.js +38 -0
  18. package/dist/codegen/generator/index.d.ts +8 -10
  19. package/dist/codegen/generator/index.js +23 -678
  20. package/dist/codegen/generator/key-builder.d.ts +13 -0
  21. package/dist/codegen/generator/key-builder.js +32 -0
  22. package/dist/codegen/hover-tracker/client.d.ts +5 -0
  23. package/dist/codegen/hover-tracker/client.js +14 -15
  24. package/dist/codegen/hover-tracker/manager.js +2 -6
  25. package/dist/codegen/index.d.ts +9 -32
  26. package/dist/codegen/index.js +9 -1128
  27. package/dist/codegen/key-utils.d.ts +16 -0
  28. package/dist/codegen/key-utils.js +29 -1
  29. package/dist/codegen/project-finder.d.ts +29 -0
  30. package/dist/codegen/project-finder.js +283 -0
  31. package/dist/codegen/registry-matcher.d.ts +27 -0
  32. package/dist/codegen/registry-matcher.js +254 -0
  33. package/dist/codegen/registry-store.d.ts +52 -0
  34. package/dist/codegen/registry-store.js +652 -0
  35. package/dist/codegen/selector-parser.d.ts +16 -0
  36. package/dist/codegen/selector-parser.js +121 -0
  37. package/dist/codegen/types.d.ts +18 -1
  38. package/dist/codegen/uniqueness.d.ts +3 -0
  39. package/dist/codegen/uniqueness.js +15 -0
  40. package/dist/component/table.d.ts +2 -2
  41. package/dist/component/table.js +7 -6
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +19 -0
  44. package/dist/page/actions/locator-actions.d.ts +6 -10
  45. package/dist/page/actions/locator-actions.js +24 -23
  46. package/dist/page/assertions/verify-chain.d.ts +11 -13
  47. package/dist/page/assertions/verify-chain.js +26 -7
  48. package/dist/page/assertions/verify-helpers.d.ts +5 -15
  49. package/dist/page/config.d.ts +25 -25
  50. package/dist/page/locators/dynamic-locator-resolver.js +22 -9
  51. package/dist/page/locators/resolver.d.ts +17 -11
  52. package/dist/page/locators/resolver.js +84 -78
  53. package/dist/page/registry.d.ts +35 -35
  54. package/dist/page/registry.js +86 -82
  55. package/dist/page/typed-page.d.ts +1 -0
  56. package/dist/page/typed-page.js +22 -12
  57. package/dist/page/types/proxy-methods.d.ts +11 -19
  58. package/dist/page/types/validation.d.ts +1 -1
  59. package/dist/page/utils/formatter.d.ts +3 -3
  60. package/dist/page/utils/formatter.js +5 -2
  61. package/package.json +6 -3
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scoreAndRankCandidates = scoreAndRankCandidates;
4
+ const scorer_1 = require("../scorer");
5
+ const locator_restricted_1 = require("./locator.restricted");
6
+ const id_stability_1 = require("./id-stability");
7
+ const key_builder_1 = require("./key-builder");
8
+ function normalize(value) {
9
+ return value
10
+ .toLowerCase()
11
+ .replace(/[-_\s]/g, '')
12
+ .replace(/[^\w]/g, '');
13
+ }
14
+ /**
15
+ * Transforms raw scanned candidates into fully scored and uniqueness-validated LocatorCandidates.
16
+ */
17
+ async function scoreAndRankCandidates(page, scanResult) {
18
+ const targetNormalized = normalize(scanResult.targetText || scanResult.targetAccessibleName || '');
19
+ const candidates = [];
20
+ for (const c of scanResult.candidates) {
21
+ const val = c.valueToScore || '';
22
+ const valLower = val.toLowerCase();
23
+ // No long values more than 50 chars
24
+ if (val.length > 50) {
25
+ continue;
26
+ }
27
+ // Reject unstable text
28
+ if (c.strategy === 'text') {
29
+ if (Object.values(locator_restricted_1.PATTERNS.restrictedTexts).some((keyword) => valLower.includes(keyword)) ||
30
+ val.length > 40) {
31
+ continue;
32
+ }
33
+ }
34
+ // Calculate priority
35
+ const priorityScore = (0, scorer_1.getPriorityScore)(c);
36
+ // Calculate Proximity Bonus
37
+ let proximityScore = 0;
38
+ if (c.source === 'target')
39
+ proximityScore = 300;
40
+ else if (c.source === 'child')
41
+ proximityScore = 250;
42
+ else if (c.source === 'sibling')
43
+ proximityScore = 250;
44
+ else if (c.source === 'parent')
45
+ proximityScore = 200;
46
+ else if (c.source === 'ancestor') {
47
+ if (c.depth === 1)
48
+ proximityScore = 150; // Grandparent
49
+ else
50
+ proximityScore = 100; // Ancestor depth 3
51
+ }
52
+ // Calculate Similarity Bonus
53
+ const candidateNormalized = normalize(val);
54
+ const similarityScore = targetNormalized && targetNormalized === candidateNormalized ? 200 : 0;
55
+ // Calculate Semantic Bonus
56
+ let semanticScore = 0;
57
+ if (/[a-zA-Z]/.test(valLower)) {
58
+ const hasHyphen = valLower.includes('-');
59
+ const hasUnderscore = valLower.includes('_');
60
+ const hasCamelCase = /[a-z][A-Z]/.test(val);
61
+ const wordsCount = valLower.split(/[-_\s]+/).filter(Boolean).length;
62
+ if (hasHyphen || hasUnderscore || hasCamelCase || wordsCount > 1) {
63
+ semanticScore = 100;
64
+ }
65
+ }
66
+ // Calculate Accessibility Bonus
67
+ const accessibilityBonus = c.strategy === 'label' || c.strategy === 'ariaLabel' ? 150 : 0;
68
+ // Calculate Stability Bonus
69
+ const isStable = c.strategy === 'testId' ||
70
+ (c.strategy === 'id' && (0, id_stability_1.isStableId)(val)) ||
71
+ (c.strategy === 'dataAttribute' &&
72
+ (c.attrName === 'data-page' ||
73
+ c.attrName === 'data-section' ||
74
+ c.attrName === 'data-parent-id' ||
75
+ c.attrName === 'data-test-id' ||
76
+ c.attrName === 'data-testid' ||
77
+ c.attrName === 'datatestid'));
78
+ const stabilityBonus = isStable ? 100 : 0;
79
+ // Calculate Penalties
80
+ let penalties = 0;
81
+ if (c.strategy === 'role' && !c.selector.includes('name=')) {
82
+ penalties += 350; // penalize role without name so placeholder/label/text are preferred
83
+ }
84
+ if (c.strategy === 'id' && !(0, id_stability_1.isStableId)(val)) {
85
+ penalties += 600; // heavily penalize unstable IDs so they rank below semantic locators
86
+ }
87
+ if (c.strategy === 'class') {
88
+ if ((0, locator_restricted_1.isRestrictedClass)(val))
89
+ penalties += 1000;
90
+ if (locator_restricted_1.stateClasses.includes(valLower))
91
+ penalties += 1000;
92
+ if (/^[A-Z]/.test(val))
93
+ penalties += 1000;
94
+ }
95
+ const hasDecorative = locator_restricted_1.decorativeList.some((item) => valLower.includes(item) || c.selector.toLowerCase().includes(item));
96
+ if (hasDecorative) {
97
+ penalties += 500;
98
+ }
99
+ if (['true', 'false', '0', '1'].includes(valLower)) {
100
+ penalties += 500;
101
+ }
102
+ const isUuid = new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.uuidPattern, 'i').test(valLower) ||
103
+ new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.numericPattern).test(valLower) ||
104
+ new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.hexHashPattern, 'i').test(valLower) ||
105
+ new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.longAlphaNumPattern, 'i').test(valLower);
106
+ if (isUuid) {
107
+ penalties += 500;
108
+ }
109
+ candidates.push({
110
+ selector: c.selector,
111
+ locator: c.locator,
112
+ source: c.source,
113
+ strategy: c.strategy,
114
+ depth: c.depth,
115
+ baseScore: priorityScore,
116
+ semanticScore,
117
+ proximityScore,
118
+ similarityScore,
119
+ uniquenessScore: 0,
120
+ contextScore: proximityScore,
121
+ totalScore: 0,
122
+ unique: false,
123
+ valueToScore: c.valueToScore,
124
+ attrName: c.attrName,
125
+ accessibilityBonus,
126
+ stabilityBonus,
127
+ penalties
128
+ });
129
+ }
130
+ // Verify uniqueness and assign final score
131
+ for (const c of candidates) {
132
+ try {
133
+ const count = await page.locator(c.selector).count();
134
+ c.unique = count === 1;
135
+ }
136
+ catch {
137
+ // If counting selector fails or times out, mark as non-unique
138
+ c.unique = false;
139
+ }
140
+ c.uniquenessScore = c.unique ? 0 : -500;
141
+ // final score calculation
142
+ c.totalScore =
143
+ c.baseScore +
144
+ c.proximityScore +
145
+ c.semanticScore +
146
+ c.similarityScore +
147
+ (c.accessibilityBonus || 0) +
148
+ (c.stabilityBonus || 0) +
149
+ c.uniquenessScore -
150
+ (c.penalties || 0);
151
+ // Generate key for registry
152
+ let generatedKey = '';
153
+ const isInputEl = ['input', 'textarea', 'select'].includes(scanResult.targetTagName);
154
+ if (isInputEl) {
155
+ const testIdCand = candidates.find((cand) => cand.strategy === 'testId');
156
+ if (testIdCand && testIdCand.valueToScore) {
157
+ generatedKey = (0, key_builder_1.generateKeyFromText)(testIdCand.valueToScore, scanResult.targetRole);
158
+ }
159
+ else if (scanResult.targetName) {
160
+ generatedKey = (0, key_builder_1.generateKeyFromText)(scanResult.targetName, scanResult.targetRole);
161
+ }
162
+ else if (scanResult.targetType) {
163
+ generatedKey = (0, key_builder_1.generateKeyFromText)(scanResult.targetType + 'Input', scanResult.targetRole);
164
+ }
165
+ else {
166
+ generatedKey = scanResult.targetTagName + 'Input';
167
+ }
168
+ }
169
+ else {
170
+ const visibleText = scanResult.targetAccessibleName || scanResult.targetText || '';
171
+ if (visibleText && visibleText.trim().length > 0 && visibleText.length <= 50) {
172
+ generatedKey = (0, key_builder_1.generateKeyFromText)(visibleText.trim(), scanResult.targetRole);
173
+ }
174
+ else {
175
+ generatedKey = (0, key_builder_1.generateKeyFromCandidate)(c, scanResult.targetRole);
176
+ }
177
+ }
178
+ c.generatedKey = generatedKey;
179
+ c.nearbyText = scanResult.targetText;
180
+ c.accessibleName = scanResult.targetAccessibleName;
181
+ }
182
+ // If a stable unique candidate exists, heavily penalize unstable candidates to prioritize the stable ones
183
+ const hasStableUnique = candidates.some((c) => c.unique &&
184
+ (c.strategy === 'testId' ||
185
+ (c.strategy === 'id' && (0, id_stability_1.isStableId)(c.valueToScore || '')) ||
186
+ (c.strategy === 'dataAttribute' &&
187
+ (c.attrName === 'data-parent-id' ||
188
+ c.attrName === 'data-test-id' ||
189
+ c.attrName === 'data-testid' ||
190
+ c.attrName === 'datatestid'))));
191
+ if (hasStableUnique) {
192
+ for (const c of candidates) {
193
+ const isCandidateStable = c.strategy === 'testId' ||
194
+ (c.strategy === 'id' && (0, id_stability_1.isStableId)(c.valueToScore || '')) ||
195
+ (c.strategy === 'dataAttribute' &&
196
+ (c.attrName === 'data-parent-id' ||
197
+ c.attrName === 'data-test-id' ||
198
+ c.attrName === 'data-testid' ||
199
+ c.attrName === 'datatestid'));
200
+ if (!isCandidateStable) {
201
+ c.totalScore -= 600;
202
+ }
203
+ }
204
+ }
205
+ return candidates.sort((a, b) => b.totalScore - a.totalScore);
206
+ }
@@ -0,0 +1,39 @@
1
+ import type { SelectorStrategyType } from '../types';
2
+ export interface RawCandidate {
3
+ selector: string;
4
+ locator: string;
5
+ source: 'target' | 'child' | 'sibling' | 'parent' | 'ancestor';
6
+ strategy: SelectorStrategyType;
7
+ depth: number;
8
+ valueToScore?: string;
9
+ attrName?: string;
10
+ roleVal?: string;
11
+ }
12
+ export interface DomScanResult {
13
+ candidates: RawCandidate[];
14
+ targetText: string;
15
+ targetAccessibleName: string;
16
+ targetRole: string;
17
+ targetType: string;
18
+ targetName: string;
19
+ targetTagName: string;
20
+ targetTestId: string;
21
+ targetId: string;
22
+ targetParentId: string;
23
+ }
24
+ export interface DomScannerPayload {
25
+ exactUtilities: readonly string[];
26
+ tailwindPrefixes: readonly string[];
27
+ stateClasses: readonly string[];
28
+ stylePatterns: readonly string[];
29
+ }
30
+ /**
31
+ * In-browser evaluation function to scan the DOM search scope of a target element and collect raw locator candidates.
32
+ */
33
+ export declare function scanElementInBrowser(element: Element, data: DomScannerPayload): DomScanResult;
34
+ export declare const domScannerPayload: {
35
+ exactUtilities: string[];
36
+ tailwindPrefixes: string[];
37
+ stateClasses: string[];
38
+ stylePatterns: string[];
39
+ };
@@ -0,0 +1,429 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.domScannerPayload = void 0;
4
+ exports.scanElementInBrowser = scanElementInBrowser;
5
+ const locator_restricted_1 = require("./locator.restricted");
6
+ /**
7
+ * In-browser evaluation function to scan the DOM search scope of a target element and collect raw locator candidates.
8
+ */
9
+ function scanElementInBrowser(element, data) {
10
+ const collected = [];
11
+ const seen = new Set();
12
+ // Helper to check if a container contains multiple actions
13
+ const isMultiActionContainer = (container, target) => {
14
+ if (container === target)
15
+ return false;
16
+ const tag = container.tagName.toLowerCase();
17
+ if (['button', 'a', 'input', 'select', 'option'].includes(tag))
18
+ return false;
19
+ const actions = container.querySelectorAll('button, a, input, select, [role="button"], [role="link"]');
20
+ if (actions.length > 1) {
21
+ return true;
22
+ }
23
+ const containerText = (container.textContent || '').trim().replace(/\s+/g, ' ');
24
+ const targetText = (target.textContent || '').trim().replace(/\s+/g, ' ');
25
+ if (containerText && targetText && containerText !== targetText) {
26
+ let textChildrenCount = 0;
27
+ for (let i = 0; i < container.children.length; i++) {
28
+ const childText = (container.children[i].textContent || '').trim();
29
+ if (childText.length > 0) {
30
+ textChildrenCount++;
31
+ }
32
+ }
33
+ if (textChildrenCount > 1) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ };
39
+ // Collect elements in DOM Search Scope
40
+ const scopeElements = [];
41
+ const addElement = (el, source, depth) => {
42
+ if (!el || seen.has(el))
43
+ return;
44
+ seen.add(el);
45
+ scopeElements.push({ el, source, depth });
46
+ };
47
+ // 1. Target Element
48
+ addElement(element, 'target', 0);
49
+ // 2. Children (depth 1)
50
+ for (let i = 0; i < element.children.length; i++) {
51
+ addElement(element.children[i], 'child', 1);
52
+ }
53
+ // 3. Parent
54
+ const parentEl = element.parentElement;
55
+ addElement(parentEl, 'parent', 1);
56
+ // 4. Grandparent
57
+ const grandparentEl = parentEl ? parentEl.parentElement : null;
58
+ addElement(grandparentEl, 'ancestor', 1); // Grandparent is ancestor at depth 1
59
+ // 5. Ancestors (max depth 3)
60
+ let currAncestor = grandparentEl ? grandparentEl.parentElement : null;
61
+ let ancDepth = 2;
62
+ while (currAncestor && ancDepth <= 3) {
63
+ addElement(currAncestor, 'ancestor', ancDepth);
64
+ currAncestor = currAncestor.parentElement;
65
+ ancDepth++;
66
+ }
67
+ // 6. Previous siblings (3)
68
+ let prev = element.previousElementSibling;
69
+ let sCount = 0;
70
+ while (prev && sCount < 3) {
71
+ addElement(prev, 'sibling', 1);
72
+ prev = prev.previousElementSibling;
73
+ sCount++;
74
+ }
75
+ // 7. Next siblings (3)
76
+ let next = element.nextElementSibling;
77
+ sCount = 0;
78
+ while (next && sCount < 3) {
79
+ addElement(next, 'sibling', 1);
80
+ next = next.nextElementSibling;
81
+ sCount++;
82
+ }
83
+ // 8. Parent children
84
+ if (parentEl) {
85
+ for (let i = 0; i < parentEl.children.length; i++) {
86
+ addElement(parentEl.children[i], 'sibling', 1);
87
+ }
88
+ }
89
+ // 9. Closest heading
90
+ const headings = Array.from(element.ownerDocument.querySelectorAll('h1, h2, h3, h4, h5, h6'));
91
+ let closestHeading = null;
92
+ let minHDist = Infinity;
93
+ for (const h of headings) {
94
+ const pos = h.compareDocumentPosition(element);
95
+ if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
96
+ let dist = 0;
97
+ let curr = h;
98
+ while (curr && curr !== element) {
99
+ curr = (curr.nextElementSibling || curr.parentElement);
100
+ dist++;
101
+ if (dist > 50)
102
+ break;
103
+ }
104
+ if (dist < minHDist) {
105
+ minHDist = dist;
106
+ closestHeading = h;
107
+ }
108
+ }
109
+ }
110
+ if (closestHeading) {
111
+ addElement(closestHeading, 'ancestor', 3);
112
+ }
113
+ // 10. Closest label
114
+ let closestLabel = null;
115
+ if (element.id) {
116
+ closestLabel = element.ownerDocument.querySelector(`label[for="${element.id}"]`);
117
+ }
118
+ if (!closestLabel) {
119
+ let curr = element.parentElement;
120
+ while (curr) {
121
+ if (curr.tagName.toLowerCase() === 'label') {
122
+ closestLabel = curr;
123
+ break;
124
+ }
125
+ curr = curr.parentElement;
126
+ }
127
+ }
128
+ if (closestLabel) {
129
+ addElement(closestLabel, 'parent', 1);
130
+ }
131
+ // 11. Closest accessible element
132
+ let currAcc = element.parentElement;
133
+ while (currAcc) {
134
+ const hasRole = currAcc.getAttribute('role') ||
135
+ ['button', 'a', 'input', 'select', 'textarea'].includes(currAcc.tagName.toLowerCase());
136
+ const hasAccName = currAcc.getAttribute('aria-label') || currAcc.getAttribute('title');
137
+ if (hasRole || hasAccName) {
138
+ addElement(currAcc, 'ancestor', 2);
139
+ break;
140
+ }
141
+ currAcc = currAcc.parentElement;
142
+ }
143
+ // Helper: getRole
144
+ const getRoleAttr = (el) => {
145
+ let roleAttr = el.getAttribute('role')?.trim() || null;
146
+ const tagName = el.tagName.toLowerCase();
147
+ if (!roleAttr) {
148
+ if (tagName === 'button')
149
+ roleAttr = 'button';
150
+ else if (tagName === 'a')
151
+ roleAttr = 'link';
152
+ else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName))
153
+ roleAttr = 'heading';
154
+ else if (tagName === 'input') {
155
+ const type = el.getAttribute('type') || 'text';
156
+ if (type === 'checkbox')
157
+ roleAttr = 'checkbox';
158
+ else if (type === 'radio')
159
+ roleAttr = 'radio';
160
+ else if (['text', 'email', 'url', 'search', 'tel', 'password'].includes(type))
161
+ roleAttr = 'textbox';
162
+ else if (['button', 'submit', 'reset'].includes(type))
163
+ roleAttr = 'button';
164
+ }
165
+ else if (tagName === 'textarea')
166
+ roleAttr = 'textbox';
167
+ else if (tagName === 'select')
168
+ roleAttr = 'combobox';
169
+ else if (tagName === 'img')
170
+ roleAttr = 'img';
171
+ }
172
+ return roleAttr || '';
173
+ };
174
+ // Helper: getAccessibleName
175
+ const getAccessibleName = (el) => {
176
+ const ariaLabel = el.getAttribute('aria-label');
177
+ const alt = el.getAttribute('alt');
178
+ const title = el.getAttribute('title');
179
+ let textContent = '';
180
+ if (el.tagName.toLowerCase() === 'select') {
181
+ const firstOpt = el.querySelector('option');
182
+ textContent = firstOpt ? firstOpt.textContent || '' : '';
183
+ }
184
+ else {
185
+ textContent = el.textContent || '';
186
+ }
187
+ textContent = textContent.trim().replace(/\s+/g, ' ');
188
+ return (ariaLabel || alt || title || textContent || '').trim();
189
+ };
190
+ // Target information for context mapping
191
+ const targetTagName = element.tagName.toLowerCase();
192
+ const targetRole = getRoleAttr(element);
193
+ const targetAccessibleName = getAccessibleName(element);
194
+ const targetText = targetTagName === 'select'
195
+ ? (element.querySelector('option')?.textContent || '').trim().replace(/\s+/g, ' ')
196
+ : (element.textContent || '').trim().replace(/\s+/g, ' ');
197
+ const targetType = element.getAttribute('type') || '';
198
+ const targetName = element.getAttribute('name') || '';
199
+ const targetTestId = element.getAttribute('data-testid') ||
200
+ element.getAttribute('data-test-id') ||
201
+ element.getAttribute('testid') ||
202
+ '';
203
+ const targetId = element.id || '';
204
+ const targetParentId = element.getAttribute('data-parent-id') || '';
205
+ // Candidate Collection for all elements in scope
206
+ for (const item of scopeElements) {
207
+ const el = item.el;
208
+ const source = item.source;
209
+ const depth = item.depth;
210
+ const tagName = el.tagName.toLowerCase();
211
+ // Action Validation: Reject if multi-action container
212
+ if (isMultiActionContainer(el, element)) {
213
+ continue;
214
+ }
215
+ // 1. testId candidate
216
+ const testIdVal = el.getAttribute('data-testid') ?? el.getAttribute('data-test-id') ?? el.getAttribute('data-testId');
217
+ if (testIdVal) {
218
+ collected.push({
219
+ selector: `[data-testid="${testIdVal}"]`,
220
+ locator: `codegenPage.getByTestId('${testIdVal}')`,
221
+ source,
222
+ strategy: 'testId',
223
+ depth,
224
+ valueToScore: testIdVal
225
+ });
226
+ }
227
+ // 2. id candidate
228
+ const idVal = el.getAttribute('id');
229
+ if (idVal) {
230
+ collected.push({
231
+ selector: `#${idVal}`,
232
+ locator: `codegenPage.locator('#${idVal}')`,
233
+ source,
234
+ strategy: 'id',
235
+ depth,
236
+ valueToScore: idVal
237
+ });
238
+ }
239
+ // 3. dataAttribute candidates
240
+ const allAttrs = el.getAttributeNames();
241
+ const validDataAttrs = allAttrs.filter((attr) => attr.startsWith('data-') && !['data-testid', 'data-test-id', 'data-testid'].includes(attr.toLowerCase()));
242
+ for (const dataAttr of validDataAttrs) {
243
+ const val = el.getAttribute(dataAttr);
244
+ if (val) {
245
+ collected.push({
246
+ selector: `[${dataAttr}="${val}"]`,
247
+ locator: `codegenPage.locator('[${dataAttr}="${val}"]')`,
248
+ source,
249
+ strategy: 'dataAttribute',
250
+ depth,
251
+ valueToScore: val,
252
+ attrName: dataAttr
253
+ });
254
+ }
255
+ }
256
+ // 4. role candidate
257
+ const roleVal = getRoleAttr(el);
258
+ if (roleVal) {
259
+ const accName = getAccessibleName(el);
260
+ const escapedName = accName.replace(/'/g, "\\'");
261
+ collected.push({
262
+ selector: accName ? `internal:role=${roleVal}[name="${accName.replace(/"/g, '\\"')}"i]` : `role=${roleVal}`,
263
+ locator: accName
264
+ ? `codegenPage.getByRole('${roleVal}', { name: '${escapedName}' })`
265
+ : `codegenPage.getByRole('${roleVal}')`,
266
+ source,
267
+ strategy: 'role',
268
+ depth,
269
+ valueToScore: accName || roleVal,
270
+ roleVal
271
+ });
272
+ }
273
+ // 5. label candidate
274
+ const ariaLabel = el.getAttribute('aria-label');
275
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
276
+ let labelText = ariaLabel || ariaLabelledBy;
277
+ let labelAttr = 'aria-label';
278
+ if (ariaLabelledBy)
279
+ labelAttr = 'aria-labelledby';
280
+ if (!labelText && el.getAttribute('id')) {
281
+ const labelEl = el.ownerDocument.querySelector(`label[for="${el.getAttribute('id')}"]`);
282
+ if (labelEl && labelEl.textContent) {
283
+ labelText = labelEl.textContent.trim();
284
+ labelAttr = 'label';
285
+ }
286
+ }
287
+ if (labelText) {
288
+ collected.push({
289
+ selector: `internal:label="${labelText.replace(/"/g, '\\"')}"i`,
290
+ locator: `codegenPage.getByLabel('${labelText.replace(/'/g, "\\'")}')`,
291
+ source,
292
+ strategy: 'label',
293
+ depth,
294
+ valueToScore: labelText,
295
+ attrName: labelAttr
296
+ });
297
+ }
298
+ // 6. placeholder candidate
299
+ const placeholderVal = el.getAttribute('placeholder');
300
+ if (placeholderVal) {
301
+ collected.push({
302
+ selector: `internal:attr=[placeholder="${placeholderVal.replace(/"/g, '\\"')}"i]`,
303
+ locator: `codegenPage.getByPlaceholder('${placeholderVal.replace(/'/g, "\\'")}')`,
304
+ source,
305
+ strategy: 'placeholder',
306
+ depth,
307
+ valueToScore: placeholderVal
308
+ });
309
+ }
310
+ // 7. altText candidate
311
+ const altVal = el.getAttribute('alt');
312
+ if (altVal) {
313
+ collected.push({
314
+ selector: `internal:attr=[alt="${altVal.replace(/"/g, '\\"')}"i]`,
315
+ locator: `codegenPage.getByAltText('${altVal.replace(/'/g, "\\'")}')`,
316
+ source,
317
+ strategy: 'altText',
318
+ depth,
319
+ valueToScore: altVal
320
+ });
321
+ }
322
+ // 8. title candidate
323
+ const titleVal = el.getAttribute('title');
324
+ if (titleVal) {
325
+ collected.push({
326
+ selector: `internal:attr=[title="${titleVal.replace(/"/g, '\\"')}"i]`,
327
+ locator: `codegenPage.getByTitle('${titleVal.replace(/'/g, "\\'")}')`,
328
+ source,
329
+ strategy: 'title',
330
+ depth,
331
+ valueToScore: titleVal
332
+ });
333
+ }
334
+ // 9. text candidate
335
+ const textVal = (el.textContent || '').trim().replace(/\s+/g, ' ');
336
+ if (textVal) {
337
+ const cleanedText = textVal.replace(/^\d+[\s\.\-_]*/, '').trim();
338
+ if (cleanedText && !/\d/.test(cleanedText)) {
339
+ const escapedText = cleanedText.replace(/"/g, '\\"');
340
+ const textSelector = cleanedText.includes(' ') ? `text="${escapedText}"` : `text=${escapedText}`;
341
+ collected.push({
342
+ selector: textSelector,
343
+ locator: `codegenPage.getByText('${cleanedText.replace(/'/g, "\\'")}')`,
344
+ source,
345
+ strategy: 'text',
346
+ depth,
347
+ valueToScore: cleanedText
348
+ });
349
+ }
350
+ }
351
+ // 10. class candidates
352
+ const className = el.getAttribute('class');
353
+ if (className) {
354
+ const classes = className.split(/\s+/).filter(Boolean);
355
+ const isStyleClass = (cName) => {
356
+ if (cName.includes('[') && cName.includes(']'))
357
+ return true;
358
+ if (cName.includes(':'))
359
+ return true;
360
+ if (data.exactUtilities.includes(cName))
361
+ return true;
362
+ if (data.stateClasses.includes(cName.toLowerCase()))
363
+ return true;
364
+ if (data.tailwindPrefixes.some((prefix) => cName.startsWith(prefix)))
365
+ return true;
366
+ return data.stylePatterns.some((pat) => new RegExp(pat).test(cName));
367
+ };
368
+ for (const cls of classes) {
369
+ if (cls.startsWith('translate-x') ||
370
+ cls.startsWith('translate-y') ||
371
+ isStyleClass(cls) ||
372
+ /^[A-Z]/.test(cls) ||
373
+ cls.toLowerCase().includes('border') ||
374
+ cls.length > 50) {
375
+ continue;
376
+ }
377
+ collected.push({
378
+ selector: `.${cls}`,
379
+ locator: `codegenPage.locator('.${cls}')`,
380
+ source,
381
+ strategy: 'class',
382
+ depth,
383
+ valueToScore: cls
384
+ });
385
+ }
386
+ }
387
+ // 11. tag / css candidate
388
+ const semanticTags = ['button', 'a', 'input', 'select', 'textarea', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'form'];
389
+ if (semanticTags.includes(tagName)) {
390
+ collected.push({
391
+ selector: `${tagName}`,
392
+ locator: `codegenPage.locator('${tagName}')`,
393
+ source,
394
+ strategy: 'css',
395
+ depth,
396
+ valueToScore: tagName
397
+ });
398
+ }
399
+ // 12. xpath candidate
400
+ if (semanticTags.includes(tagName)) {
401
+ collected.push({
402
+ selector: `xpath=//${tagName}`,
403
+ locator: `codegenPage.locator('//${tagName}')`,
404
+ source,
405
+ strategy: 'xpath',
406
+ depth,
407
+ valueToScore: tagName
408
+ });
409
+ }
410
+ }
411
+ return {
412
+ candidates: collected,
413
+ targetText,
414
+ targetAccessibleName,
415
+ targetRole,
416
+ targetType,
417
+ targetName,
418
+ targetTagName,
419
+ targetTestId,
420
+ targetId,
421
+ targetParentId
422
+ };
423
+ }
424
+ exports.domScannerPayload = {
425
+ exactUtilities: locator_restricted_1.exactUtilities,
426
+ tailwindPrefixes: locator_restricted_1.tailwindPrefixes,
427
+ stateClasses: locator_restricted_1.stateClasses,
428
+ stylePatterns: locator_restricted_1.stylePatterns
429
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Checks whether an HTML element's ID attribute is stable and non-generated.
3
+ * Filters out framework auto-generated IDs (React, MUI, HeadlessUI, Ember, Angular),
4
+ * UUIDs, hashes, and purely numeric IDs.
5
+ */
6
+ export declare function isStableId(id: string): boolean;