pw-core 1.2.0 → 1.3.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.
Files changed (86) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +777 -0
  4. package/dist/codegen/config/context-weights.d.ts +8 -0
  5. package/dist/codegen/config/context-weights.js +11 -0
  6. package/dist/codegen/config/ignored-attributes.d.ts +1 -0
  7. package/dist/codegen/config/ignored-attributes.js +4 -0
  8. package/dist/codegen/config/ignored-classes.d.ts +1 -0
  9. package/dist/codegen/config/ignored-classes.js +144 -0
  10. package/dist/codegen/config/ignored-values.d.ts +1 -0
  11. package/dist/codegen/config/ignored-values.js +4 -0
  12. package/dist/codegen/config/index.d.ts +29 -0
  13. package/dist/codegen/config/index.js +17 -0
  14. package/dist/codegen/config/strategy-order.d.ts +1 -0
  15. package/dist/codegen/config/strategy-order.js +18 -0
  16. package/dist/codegen/config/weights.d.ts +15 -0
  17. package/dist/codegen/config/weights.js +18 -0
  18. package/dist/codegen/floating-panel/client.d.ts +1 -0
  19. package/dist/codegen/floating-panel/client.js +248 -0
  20. package/dist/codegen/floating-panel/index.d.ts +4 -0
  21. package/dist/codegen/floating-panel/index.js +20 -0
  22. package/dist/codegen/floating-panel/manager.d.ts +19 -0
  23. package/dist/codegen/floating-panel/manager.js +90 -0
  24. package/dist/codegen/floating-panel/template.d.ts +2 -0
  25. package/dist/codegen/floating-panel/template.js +58 -0
  26. package/dist/codegen/floating-panel/types.d.ts +7 -0
  27. package/dist/codegen/floating-panel/types.js +2 -0
  28. package/dist/codegen/generator/action.validator.d.ts +5 -0
  29. package/dist/codegen/generator/action.validator.js +27 -0
  30. package/dist/codegen/generator/index.d.ts +13 -0
  31. package/dist/codegen/generator/index.js +724 -0
  32. package/dist/codegen/generator/locator.restricted.d.ts +36 -0
  33. package/dist/codegen/generator/locator.restricted.js +127 -0
  34. package/dist/codegen/generator/locator.validator.d.ts +8 -0
  35. package/dist/codegen/generator/locator.validator.js +38 -0
  36. package/dist/codegen/hover-tracker/client.d.ts +1 -0
  37. package/dist/codegen/hover-tracker/client.js +75 -0
  38. package/dist/codegen/hover-tracker/index.d.ts +3 -0
  39. package/dist/codegen/hover-tracker/index.js +19 -0
  40. package/dist/codegen/hover-tracker/manager.d.ts +11 -0
  41. package/dist/codegen/hover-tracker/manager.js +28 -0
  42. package/dist/codegen/hover-tracker/types.d.ts +3 -0
  43. package/dist/codegen/hover-tracker/types.js +2 -0
  44. package/dist/codegen/index.d.ts +35 -0
  45. package/dist/codegen/index.js +1147 -0
  46. package/dist/codegen/key-utils.d.ts +23 -0
  47. package/dist/codegen/key-utils.js +68 -0
  48. package/dist/codegen/scorer/base-score.d.ts +1 -0
  49. package/dist/codegen/scorer/base-score.js +7 -0
  50. package/dist/codegen/scorer/context-score.d.ts +2 -0
  51. package/dist/codegen/scorer/context-score.js +27 -0
  52. package/dist/codegen/scorer/index.d.ts +5 -0
  53. package/dist/codegen/scorer/index.js +23 -0
  54. package/dist/codegen/scorer/locator.ranking.d.ts +15 -0
  55. package/dist/codegen/scorer/locator.ranking.js +83 -0
  56. package/dist/codegen/scorer/semantic-score.d.ts +1 -0
  57. package/dist/codegen/scorer/semantic-score.js +47 -0
  58. package/dist/codegen/types.d.ts +35 -0
  59. package/dist/codegen/types.js +2 -0
  60. package/dist/component/table.js +14 -10
  61. package/dist/constants/methods.d.ts +2 -0
  62. package/dist/constants/methods.js +33 -0
  63. package/dist/constants/strategies.d.ts +2 -0
  64. package/dist/constants/strategies.js +47 -0
  65. package/dist/page/actions/locator-actions.js +15 -4
  66. package/dist/page/assertions/verify-chain.d.ts +39 -32
  67. package/dist/page/assertions/verify-chain.js +18 -4
  68. package/dist/page/assertions/verify-helpers.js +12 -3
  69. package/dist/page/config.d.ts +109 -71
  70. package/dist/page/config.js +10 -31
  71. package/dist/page/locators/dynamic-locator-resolver.js +29 -9
  72. package/dist/page/locators/resolver.d.ts +15 -15
  73. package/dist/page/locators/resolver.js +151 -32
  74. package/dist/page/registry.d.ts +43 -6
  75. package/dist/page/registry.js +90 -18
  76. package/dist/page/typed-page.d.ts +33 -29
  77. package/dist/page/typed-page.js +40 -11
  78. package/dist/page/types/proxy-methods.d.ts +69 -54
  79. package/dist/page/types/validation.d.ts +48 -1
  80. package/dist/page/utils/caller-location.js +13 -9
  81. package/dist/page/utils/formatter.js +6 -3
  82. package/dist/types/methods.d.ts +4 -0
  83. package/dist/types/methods.js +2 -0
  84. package/dist/types/strategies.d.ts +17 -0
  85. package/dist/types/strategies.js +2 -0
  86. package/package.json +8 -1
@@ -0,0 +1,724 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isStableId = isStableId;
4
+ exports.generateKeyFromText = generateKeyFromText;
5
+ exports.generateKeyFromCandidate = generateKeyFromCandidate;
6
+ exports.generateSmartLocator = generateSmartLocator;
7
+ const scorer_1 = require("../scorer");
8
+ const locator_restricted_1 = require("./locator.restricted");
9
+ const key_utils_1 = require("../key-utils");
10
+ const locator_validator_1 = require("./locator.validator");
11
+ function isStableId(id) {
12
+ if (!id)
13
+ return false;
14
+ const lower = id.toLowerCase();
15
+ // React/MUI/HeadlessUI/Ember auto-generated patterns
16
+ if (lower.startsWith(':r') && /\d/.test(lower))
17
+ return false;
18
+ if (lower.startsWith('mui-'))
19
+ return false;
20
+ if (lower.startsWith('headlessui-'))
21
+ return false;
22
+ if (lower.startsWith('ember-'))
23
+ return false;
24
+ if (lower.startsWith('ng-'))
25
+ return false;
26
+ // UUID pattern
27
+ const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
28
+ if (uuidRegex.test(lower))
29
+ return false;
30
+ // Long hex hashes or random looking alpha-numeric (e.g. f4c2d91, 8d72b3ef)
31
+ const hexHashRegex = /^[0-9a-f]{7,8}$/i;
32
+ if (hexHashRegex.test(lower))
33
+ return false;
34
+ // Purely numeric or ends with a long random number (e.g. button-12847291)
35
+ if (/^\d+$/.test(lower))
36
+ return false;
37
+ if (/\d{5,}$/.test(lower))
38
+ return false;
39
+ return true;
40
+ }
41
+ function generateKeyFromText(text, role) {
42
+ if (!text)
43
+ return '';
44
+ const roleSuffix = role === 'button' ? 'Btn' : undefined;
45
+ return (0, key_utils_1.toRegistryKey)(text, { roleSuffix });
46
+ }
47
+ /**
48
+ * Derive a camelCase key from a candidate's attribute/selector value.
49
+ * E.g. data-parent-id="why-pw-core" → whyPwCore
50
+ */
51
+ function generateKeyFromCandidate(candidate, role) {
52
+ const val = candidate.valueToScore || '';
53
+ if (!val)
54
+ return '';
55
+ // For data-attribute, id, testId strategies — use the attribute value directly
56
+ if (['dataAttribute', 'id', 'idAttribute', 'testId'].includes(candidate.strategy)) {
57
+ return generateKeyFromText(val, role);
58
+ }
59
+ // For role/label/text — use the text value
60
+ if (['role', 'label', 'text', 'placeholder', 'altText', 'title'].includes(candidate.strategy)) {
61
+ return generateKeyFromText(val, role);
62
+ }
63
+ return '';
64
+ }
65
+ async function generateSmartLocator(page, selector) {
66
+ if (!selector)
67
+ return null;
68
+ const locator = page.locator(selector);
69
+ const count = await locator.count().catch(() => 0);
70
+ if (count === 0)
71
+ return null;
72
+ // Evaluate candidate collection and DOM context extraction inside the browser page
73
+ const result = await locator
74
+ .first()
75
+ .evaluate((element, data) => {
76
+ const collected = [];
77
+ const seen = new Set();
78
+ const getElementAttributes = (el) => {
79
+ const attrs = {};
80
+ const names = el.getAttributeNames();
81
+ for (const name of names) {
82
+ attrs[name] = el.getAttribute(name) || '';
83
+ }
84
+ return attrs;
85
+ };
86
+ // Helper to check if a container contains multiple actions
87
+ const isMultiActionContainer = (container, target) => {
88
+ if (container === target)
89
+ return false;
90
+ const tag = container.tagName.toLowerCase();
91
+ if (['button', 'a', 'input', 'select', 'option'].includes(tag))
92
+ return false;
93
+ const actions = container.querySelectorAll('button, a, input, select, [role="button"], [role="link"]');
94
+ if (actions.length > 1) {
95
+ return true;
96
+ }
97
+ const containerText = (container.textContent || '').trim().replace(/\s+/g, ' ');
98
+ const targetText = (target.textContent || '').trim().replace(/\s+/g, ' ');
99
+ if (containerText && targetText && containerText !== targetText) {
100
+ let textChildrenCount = 0;
101
+ for (let i = 0; i < container.children.length; i++) {
102
+ const childText = (container.children[i].textContent || '').trim();
103
+ if (childText.length > 0) {
104
+ textChildrenCount++;
105
+ }
106
+ }
107
+ if (textChildrenCount > 1) {
108
+ return true;
109
+ }
110
+ }
111
+ return false;
112
+ };
113
+ // Collect elements in DOM Search Scope
114
+ const scopeElements = [];
115
+ const addElement = (el, source, depth) => {
116
+ if (!el || seen.has(el))
117
+ return;
118
+ seen.add(el);
119
+ scopeElements.push({ el, source, depth });
120
+ };
121
+ // 1. Target Element
122
+ addElement(element, 'target', 0);
123
+ // 2. Children (depth 1)
124
+ for (let i = 0; i < element.children.length; i++) {
125
+ addElement(element.children[i], 'child', 1);
126
+ }
127
+ // 3. Parent
128
+ const parentEl = element.parentElement;
129
+ addElement(parentEl, 'parent', 1);
130
+ // 4. Grandparent
131
+ const grandparentEl = parentEl ? parentEl.parentElement : null;
132
+ addElement(grandparentEl, 'ancestor', 1); // Grandparent is ancestor at depth 1
133
+ // 5. Ancestors (max depth 3)
134
+ let currAncestor = grandparentEl ? grandparentEl.parentElement : null;
135
+ let ancDepth = 2;
136
+ while (currAncestor && ancDepth <= 3) {
137
+ addElement(currAncestor, 'ancestor', ancDepth);
138
+ currAncestor = currAncestor.parentElement;
139
+ ancDepth++;
140
+ }
141
+ // 6. Previous siblings (3)
142
+ let prev = element.previousElementSibling;
143
+ let sCount = 0;
144
+ while (prev && sCount < 3) {
145
+ addElement(prev, 'sibling', 1);
146
+ prev = prev.previousElementSibling;
147
+ sCount++;
148
+ }
149
+ // 7. Next siblings (3)
150
+ let next = element.nextElementSibling;
151
+ sCount = 0;
152
+ while (next && sCount < 3) {
153
+ addElement(next, 'sibling', 1);
154
+ next = next.nextElementSibling;
155
+ sCount++;
156
+ }
157
+ // 8. Parent children
158
+ if (parentEl) {
159
+ for (let i = 0; i < parentEl.children.length; i++) {
160
+ addElement(parentEl.children[i], 'sibling', 1);
161
+ }
162
+ }
163
+ // 9. Closest heading
164
+ const headings = Array.from(element.ownerDocument.querySelectorAll('h1, h2, h3, h4, h5, h6'));
165
+ let closestHeading = null;
166
+ let minHDist = Infinity;
167
+ for (const h of headings) {
168
+ const pos = h.compareDocumentPosition(element);
169
+ if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
170
+ let dist = 0;
171
+ let curr = h;
172
+ while (curr && curr !== element) {
173
+ curr = (curr.nextElementSibling || curr.parentElement);
174
+ dist++;
175
+ if (dist > 50)
176
+ break;
177
+ }
178
+ if (dist < minHDist) {
179
+ minHDist = dist;
180
+ closestHeading = h;
181
+ }
182
+ }
183
+ }
184
+ if (closestHeading) {
185
+ addElement(closestHeading, 'ancestor', 3);
186
+ }
187
+ // 10. Closest label
188
+ let closestLabel = null;
189
+ if (element.id) {
190
+ closestLabel = element.ownerDocument.querySelector(`label[for="${element.id}"]`);
191
+ }
192
+ if (!closestLabel) {
193
+ let curr = element.parentElement;
194
+ while (curr) {
195
+ if (curr.tagName.toLowerCase() === 'label') {
196
+ closestLabel = curr;
197
+ break;
198
+ }
199
+ curr = curr.parentElement;
200
+ }
201
+ }
202
+ if (closestLabel) {
203
+ addElement(closestLabel, 'parent', 1);
204
+ }
205
+ // 11. Closest accessible element
206
+ let currAcc = element.parentElement;
207
+ while (currAcc) {
208
+ const hasRole = currAcc.getAttribute('role') ||
209
+ ['button', 'a', 'input', 'select', 'textarea'].includes(currAcc.tagName.toLowerCase());
210
+ const hasAccName = currAcc.getAttribute('aria-label') || currAcc.getAttribute('title');
211
+ if (hasRole || hasAccName) {
212
+ addElement(currAcc, 'ancestor', 2);
213
+ break;
214
+ }
215
+ currAcc = currAcc.parentElement;
216
+ }
217
+ // Helper: getRole
218
+ const getRoleAttr = (el) => {
219
+ let roleAttr = el.getAttribute('role')?.trim() || null;
220
+ const tagName = el.tagName.toLowerCase();
221
+ if (!roleAttr) {
222
+ if (tagName === 'button')
223
+ roleAttr = 'button';
224
+ else if (tagName === 'a')
225
+ roleAttr = 'link';
226
+ else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName))
227
+ roleAttr = 'heading';
228
+ else if (tagName === 'input') {
229
+ const type = el.getAttribute('type') || 'text';
230
+ if (type === 'checkbox')
231
+ roleAttr = 'checkbox';
232
+ else if (type === 'radio')
233
+ roleAttr = 'radio';
234
+ else if (['text', 'email', 'url', 'search', 'tel', 'password'].includes(type))
235
+ roleAttr = 'textbox';
236
+ else if (['button', 'submit', 'reset'].includes(type))
237
+ roleAttr = 'button';
238
+ }
239
+ else if (tagName === 'textarea')
240
+ roleAttr = 'textbox';
241
+ else if (tagName === 'select')
242
+ roleAttr = 'combobox';
243
+ else if (tagName === 'img')
244
+ roleAttr = 'img';
245
+ }
246
+ return roleAttr || '';
247
+ };
248
+ // Helper: getAccessibleName
249
+ const getAccessibleName = (el) => {
250
+ const ariaLabel = el.getAttribute('aria-label');
251
+ const alt = el.getAttribute('alt');
252
+ const title = el.getAttribute('title');
253
+ let textContent = '';
254
+ if (el.tagName.toLowerCase() === 'select') {
255
+ const firstOpt = el.querySelector('option');
256
+ textContent = firstOpt ? (firstOpt.textContent || '') : '';
257
+ }
258
+ else {
259
+ textContent = el.textContent || '';
260
+ }
261
+ textContent = textContent.trim().replace(/\s+/g, ' ');
262
+ return (ariaLabel || alt || title || textContent || '').trim();
263
+ };
264
+ // Target information for context mapping
265
+ const targetTagName = element.tagName.toLowerCase();
266
+ const targetRole = getRoleAttr(element);
267
+ const targetAccessibleName = getAccessibleName(element);
268
+ const targetText = targetTagName === 'select'
269
+ ? (element.querySelector('option')?.textContent || '').trim().replace(/\s+/g, ' ')
270
+ : (element.textContent || '').trim().replace(/\s+/g, ' ');
271
+ const targetType = element.getAttribute('type') || '';
272
+ const targetName = element.getAttribute('name') || '';
273
+ const targetTestId = element.getAttribute('data-testid') || element.getAttribute('data-test-id') || element.getAttribute('testid') || '';
274
+ const targetId = element.id || '';
275
+ const targetParentId = element.getAttribute('data-parent-id') || '';
276
+ // Candidate Collection for all elements in scope
277
+ for (const item of scopeElements) {
278
+ const el = item.el;
279
+ const source = item.source;
280
+ const depth = item.depth;
281
+ const tagName = el.tagName.toLowerCase();
282
+ // Phase 1 Action Validation: Reject if multi-action container
283
+ if (isMultiActionContainer(el, element)) {
284
+ continue;
285
+ }
286
+ // 1. testId candidate
287
+ const testIdVal = el.getAttribute('data-testid') ?? el.getAttribute('data-test-id') ?? el.getAttribute('data-testId');
288
+ if (testIdVal) {
289
+ collected.push({
290
+ selector: `[data-testid="${testIdVal}"]`,
291
+ locator: `codegenPage.getByTestId('${testIdVal}')`,
292
+ source,
293
+ strategy: 'testId',
294
+ depth,
295
+ valueToScore: testIdVal
296
+ });
297
+ }
298
+ // 2. id candidate
299
+ const idVal = el.getAttribute('id');
300
+ if (idVal) {
301
+ collected.push({
302
+ selector: `#${idVal}`,
303
+ locator: `codegenPage.locator('#${idVal}')`,
304
+ source,
305
+ strategy: 'id',
306
+ depth,
307
+ valueToScore: idVal
308
+ });
309
+ }
310
+ // 3. dataAttribute candidates
311
+ const allAttrs = el.getAttributeNames();
312
+ const validDataAttrs = allAttrs.filter((attr) => attr.startsWith('data-') && !['data-testid', 'data-test-id', 'data-testid'].includes(attr.toLowerCase()));
313
+ for (const dataAttr of validDataAttrs) {
314
+ const val = el.getAttribute(dataAttr);
315
+ if (val) {
316
+ collected.push({
317
+ selector: `[${dataAttr}="${val}"]`,
318
+ locator: `codegenPage.locator('[${dataAttr}="${val}"]')`,
319
+ source,
320
+ strategy: 'dataAttribute',
321
+ depth,
322
+ valueToScore: val,
323
+ attrName: dataAttr
324
+ });
325
+ }
326
+ }
327
+ // 4. role candidate
328
+ const roleVal = getRoleAttr(el);
329
+ if (roleVal) {
330
+ const accName = getAccessibleName(el);
331
+ const escapedName = accName.replace(/'/g, "\\'");
332
+ collected.push({
333
+ selector: accName ? `internal:role=${roleVal}[name="${accName.replace(/"/g, '\\"')}"i]` : `role=${roleVal}`,
334
+ locator: accName
335
+ ? `codegenPage.getByRole('${roleVal}', { name: '${escapedName}' })`
336
+ : `codegenPage.getByRole('${roleVal}')`,
337
+ source,
338
+ strategy: 'role',
339
+ depth,
340
+ valueToScore: accName || roleVal,
341
+ roleVal
342
+ });
343
+ }
344
+ // 5. label candidate
345
+ const ariaLabel = el.getAttribute('aria-label');
346
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
347
+ let labelText = ariaLabel || ariaLabelledBy;
348
+ let labelAttr = 'aria-label';
349
+ if (ariaLabelledBy)
350
+ labelAttr = 'aria-labelledby';
351
+ if (!labelText && el.getAttribute('id')) {
352
+ const labelEl = el.ownerDocument.querySelector(`label[for="${el.getAttribute('id')}"]`);
353
+ if (labelEl && labelEl.textContent) {
354
+ labelText = labelEl.textContent.trim();
355
+ labelAttr = 'label';
356
+ }
357
+ }
358
+ if (labelText) {
359
+ collected.push({
360
+ selector: `internal:label="${labelText.replace(/"/g, '\\"')}"i`,
361
+ locator: `codegenPage.getByLabel('${labelText.replace(/'/g, "\\'")}')`,
362
+ source,
363
+ strategy: 'label',
364
+ depth,
365
+ valueToScore: labelText,
366
+ attrName: labelAttr
367
+ });
368
+ }
369
+ // 6. placeholder candidate
370
+ const placeholderVal = el.getAttribute('placeholder');
371
+ if (placeholderVal) {
372
+ collected.push({
373
+ selector: `internal:attr=[placeholder="${placeholderVal.replace(/"/g, '\\"')}"i]`,
374
+ locator: `codegenPage.getByPlaceholder('${placeholderVal.replace(/'/g, "\\'")}')`,
375
+ source,
376
+ strategy: 'placeholder',
377
+ depth,
378
+ valueToScore: placeholderVal
379
+ });
380
+ }
381
+ // 7. altText candidate
382
+ const altVal = el.getAttribute('alt');
383
+ if (altVal) {
384
+ collected.push({
385
+ selector: `internal:attr=[alt="${altVal.replace(/"/g, '\\"')}"i]`,
386
+ locator: `codegenPage.getByAltText('${altVal.replace(/'/g, "\\'")}')`,
387
+ source,
388
+ strategy: 'altText',
389
+ depth,
390
+ valueToScore: altVal
391
+ });
392
+ }
393
+ // 8. title candidate
394
+ const titleVal = el.getAttribute('title');
395
+ if (titleVal) {
396
+ collected.push({
397
+ selector: `internal:attr=[title="${titleVal.replace(/"/g, '\\"')}"i]`,
398
+ locator: `codegenPage.getByTitle('${titleVal.replace(/'/g, "\\'")}')`,
399
+ source,
400
+ strategy: 'title',
401
+ depth,
402
+ valueToScore: titleVal
403
+ });
404
+ }
405
+ // 9. text candidate
406
+ const textVal = (el.textContent || '').trim().replace(/\s+/g, ' ');
407
+ if (textVal) {
408
+ const cleanedText = textVal.replace(/^\d+[\s\.\-_]*/, '').trim();
409
+ if (cleanedText && !/\d/.test(cleanedText)) {
410
+ const escapedText = cleanedText.replace(/"/g, '\\"');
411
+ const textSelector = cleanedText.includes(' ') ? `text="${escapedText}"` : `text=${escapedText}`;
412
+ collected.push({
413
+ selector: textSelector,
414
+ locator: `codegenPage.getByText('${cleanedText.replace(/'/g, "\\'")}')`,
415
+ source,
416
+ strategy: 'text',
417
+ depth,
418
+ valueToScore: cleanedText
419
+ });
420
+ }
421
+ }
422
+ // 10. class candidates
423
+ const className = el.getAttribute('class');
424
+ if (className) {
425
+ const classes = className.split(/\s+/).filter(Boolean);
426
+ const isStyleClass = (cName) => {
427
+ if (cName.includes('[') && cName.includes(']'))
428
+ return true;
429
+ if (cName.includes(':'))
430
+ return true;
431
+ if (data.exactUtilities.includes(cName))
432
+ return true;
433
+ if (data.stateClasses.includes(cName.toLowerCase()))
434
+ return true;
435
+ if (data.tailwindPrefixes.some((prefix) => cName.startsWith(prefix)))
436
+ return true;
437
+ return data.stylePatterns.some((pat) => new RegExp(pat).test(cName));
438
+ };
439
+ for (const cls of classes) {
440
+ if (cls.startsWith('translate-x') ||
441
+ cls.startsWith('translate-y') ||
442
+ isStyleClass(cls) ||
443
+ /^[A-Z]/.test(cls) ||
444
+ cls.toLowerCase().includes('border') ||
445
+ cls.length > 50) {
446
+ continue;
447
+ }
448
+ collected.push({
449
+ selector: `.${cls}`,
450
+ locator: `codegenPage.locator('.${cls}')`,
451
+ source,
452
+ strategy: 'class',
453
+ depth,
454
+ valueToScore: cls
455
+ });
456
+ }
457
+ }
458
+ // 11. tag / css candidate
459
+ const semanticTags = ['button', 'a', 'input', 'select', 'textarea', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'form'];
460
+ if (semanticTags.includes(tagName)) {
461
+ collected.push({
462
+ selector: `${tagName}`,
463
+ locator: `codegenPage.locator('${tagName}')`,
464
+ source,
465
+ strategy: 'css',
466
+ depth,
467
+ valueToScore: tagName
468
+ });
469
+ }
470
+ // 12. xpath candidate
471
+ if (semanticTags.includes(tagName)) {
472
+ collected.push({
473
+ selector: `xpath=//${tagName}`,
474
+ locator: `codegenPage.locator('//${tagName}')`,
475
+ source,
476
+ strategy: 'xpath',
477
+ depth,
478
+ valueToScore: tagName
479
+ });
480
+ }
481
+ }
482
+ return {
483
+ candidates: collected,
484
+ targetText,
485
+ targetAccessibleName,
486
+ targetRole,
487
+ targetType,
488
+ targetName,
489
+ targetTagName,
490
+ targetTestId,
491
+ targetId,
492
+ targetParentId
493
+ };
494
+ }, { exactUtilities: locator_restricted_1.exactUtilities, tailwindPrefixes: locator_restricted_1.tailwindPrefixes, stateClasses: locator_restricted_1.stateClasses, stylePatterns: locator_restricted_1.stylePatterns }, { timeout: 1000 })
495
+ .catch(() => null);
496
+ if (!result || !result.candidates || result.candidates.length === 0)
497
+ return null;
498
+ const normalize = (value) => {
499
+ return value
500
+ .toLowerCase()
501
+ .replace(/[-_\s]/g, '')
502
+ .replace(/[^\w]/g, '');
503
+ };
504
+ const targetNormalized = normalize(result.targetText || result.targetAccessibleName || '');
505
+ // Filter and Score Candidates on Node.js side
506
+ const candidates = [];
507
+ for (const c of result.candidates) {
508
+ const val = c.valueToScore || '';
509
+ const valLower = val.toLowerCase();
510
+ // No long values not more than 50 chars
511
+ if (val.length > 50) {
512
+ continue;
513
+ }
514
+ // Reject unstable text
515
+ if (c.strategy === 'text') {
516
+ if (Object.values(locator_restricted_1.PATTERNS.restrictedTexts).some((keyword) => valLower.includes(keyword)) ||
517
+ val.length > 40) {
518
+ continue;
519
+ }
520
+ }
521
+ // Calculate priority
522
+ const priorityScore = (0, scorer_1.getPriorityScore)(c);
523
+ // Calculate Proximity Bonus
524
+ let proximityScore = 0;
525
+ if (c.source === 'target')
526
+ proximityScore = 300;
527
+ else if (c.source === 'child')
528
+ proximityScore = 250;
529
+ else if (c.source === 'sibling')
530
+ proximityScore = 250;
531
+ else if (c.source === 'parent')
532
+ proximityScore = 200;
533
+ else if (c.source === 'ancestor') {
534
+ if (c.depth === 1)
535
+ proximityScore = 150; // Grandparent
536
+ else
537
+ proximityScore = 100; // Ancestor depth 3
538
+ }
539
+ // Calculate Similarity Bonus
540
+ const candidateNormalized = normalize(val);
541
+ const similarityScore = targetNormalized && targetNormalized === candidateNormalized ? 200 : 0;
542
+ // Calculate Semantic Bonus
543
+ let semanticScore = 0;
544
+ if (/[a-zA-Z]/.test(valLower)) {
545
+ const hasHyphen = valLower.includes('-');
546
+ const hasUnderscore = valLower.includes('_');
547
+ const hasCamelCase = /[a-z][A-Z]/.test(val);
548
+ const wordsCount = valLower.split(/[-_\s]+/).filter(Boolean).length;
549
+ if (hasHyphen || hasUnderscore || hasCamelCase || wordsCount > 1) {
550
+ semanticScore = 100;
551
+ }
552
+ }
553
+ // Calculate Accessibility Bonus
554
+ const accessibilityBonus = c.strategy === 'label' || c.strategy === 'ariaLabel' ? 150 : 0;
555
+ // Calculate Stability Bonus
556
+ const isStable = c.strategy === 'testId' ||
557
+ (c.strategy === 'id' && isStableId(val)) ||
558
+ (c.strategy === 'dataAttribute' &&
559
+ (c.attrName === 'data-page' ||
560
+ c.attrName === 'data-section' ||
561
+ c.attrName === 'data-parent-id' ||
562
+ c.attrName === 'data-test-id' ||
563
+ c.attrName === 'data-testid' ||
564
+ c.attrName === 'datatestid'));
565
+ const stabilityBonus = isStable ? 100 : 0;
566
+ // Calculate Penalties
567
+ let penalties = 0;
568
+ if (c.strategy === 'role' && !c.selector.includes('name=')) {
569
+ penalties += 350; // penalize role without name so placeholder/label/text are preferred
570
+ }
571
+ if (c.strategy === 'id' && !isStableId(val)) {
572
+ penalties += 600; // heavily penalize unstable IDs so they rank below semantic locators
573
+ }
574
+ if (c.strategy === 'class') {
575
+ if ((0, locator_restricted_1.isRestrictedClass)(val))
576
+ penalties += 1000;
577
+ if (locator_restricted_1.stateClasses.includes(valLower))
578
+ penalties += 1000;
579
+ if (/^[A-Z]/.test(val))
580
+ penalties += 1000;
581
+ }
582
+ const hasDecorative = locator_restricted_1.decorativeList.some((item) => valLower.includes(item) || c.selector.toLowerCase().includes(item));
583
+ if (hasDecorative) {
584
+ penalties += 500;
585
+ }
586
+ if (['true', 'false', '0', '1'].includes(valLower)) {
587
+ penalties += 500;
588
+ }
589
+ const isUuid = new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.uuidPattern, 'i').test(valLower) ||
590
+ new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.numericPattern).test(valLower) ||
591
+ new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.hexHashPattern, 'i').test(valLower) ||
592
+ new RegExp(locator_restricted_1.PATTERNS.unstableIdentifiers.longAlphaNumPattern, 'i').test(valLower);
593
+ if (isUuid) {
594
+ penalties += 500;
595
+ }
596
+ candidates.push({
597
+ selector: c.selector,
598
+ locator: c.locator,
599
+ source: c.source,
600
+ strategy: c.strategy,
601
+ depth: c.depth,
602
+ baseScore: priorityScore,
603
+ semanticScore: semanticScore,
604
+ proximityScore: proximityScore,
605
+ similarityScore: similarityScore,
606
+ uniquenessScore: 0,
607
+ contextScore: proximityScore,
608
+ totalScore: 0,
609
+ unique: false,
610
+ valueToScore: c.valueToScore,
611
+ attrName: c.attrName,
612
+ accessibilityBonus,
613
+ stabilityBonus,
614
+ penalties
615
+ });
616
+ }
617
+ // Verify uniqueness and assign final score
618
+ for (const c of candidates) {
619
+ try {
620
+ const count = await page.locator(c.selector).count();
621
+ c.unique = count === 1;
622
+ }
623
+ catch (e) {
624
+ c.unique = false;
625
+ }
626
+ c.uniquenessScore = c.unique ? 0 : -500;
627
+ // final score calculation
628
+ const extra = c;
629
+ c.totalScore =
630
+ c.baseScore +
631
+ c.proximityScore +
632
+ c.semanticScore +
633
+ c.similarityScore +
634
+ (extra.accessibilityBonus || 0) +
635
+ (extra.stabilityBonus || 0) +
636
+ c.uniquenessScore -
637
+ (extra.penalties || 0);
638
+ // Generate key for registry
639
+ let generatedKey = '';
640
+ const isInputEl = ['input', 'textarea', 'select'].includes(result.targetTagName);
641
+ if (isInputEl) {
642
+ const testIdCand = candidates.find(cand => cand.strategy === 'testId');
643
+ if (testIdCand && testIdCand.valueToScore) {
644
+ generatedKey = generateKeyFromText(testIdCand.valueToScore, result.targetRole);
645
+ }
646
+ else if (result.targetName) {
647
+ generatedKey = generateKeyFromText(result.targetName, result.targetRole);
648
+ }
649
+ else if (result.targetType) {
650
+ generatedKey = generateKeyFromText(result.targetType + 'Input', result.targetRole);
651
+ }
652
+ else {
653
+ generatedKey = result.targetTagName + 'Input';
654
+ }
655
+ }
656
+ else {
657
+ const visibleText = result.targetAccessibleName || result.targetText || '';
658
+ if (visibleText && visibleText.trim().length > 0 && visibleText.length <= 50) {
659
+ generatedKey = generateKeyFromText(visibleText.trim(), result.targetRole);
660
+ }
661
+ else {
662
+ generatedKey = generateKeyFromCandidate(c, result.targetRole);
663
+ }
664
+ }
665
+ c.generatedKey = generatedKey;
666
+ c.nearbyText = result.targetText;
667
+ c.accessibleName = result.targetAccessibleName;
668
+ }
669
+ // If a stable unique candidate exists, heavily penalize unstable candidates to prioritize the stable ones
670
+ const hasStableUnique = candidates.some((c) => c.unique &&
671
+ (c.strategy === 'testId' ||
672
+ (c.strategy === 'id' && isStableId(c.valueToScore || '')) ||
673
+ (c.strategy === 'dataAttribute' &&
674
+ (c.attrName === 'data-parent-id' ||
675
+ c.attrName === 'data-test-id' ||
676
+ c.attrName === 'data-testid' ||
677
+ c.attrName === 'datatestid'))));
678
+ if (hasStableUnique) {
679
+ for (const c of candidates) {
680
+ const isCandidateStable = c.strategy === 'testId' ||
681
+ (c.strategy === 'id' && isStableId(c.valueToScore || '')) ||
682
+ (c.strategy === 'dataAttribute' &&
683
+ (c.attrName === 'data-parent-id' ||
684
+ c.attrName === 'data-test-id' ||
685
+ c.attrName === 'data-testid' ||
686
+ c.attrName === 'datatestid'));
687
+ if (!isCandidateStable) {
688
+ c.totalScore -= 600;
689
+ }
690
+ }
691
+ }
692
+ const sorted = candidates.sort((a, b) => b.totalScore - a.totalScore);
693
+ const assignExtraAttrs = (c) => {
694
+ if (c) {
695
+ c.targetTestId = result.targetTestId;
696
+ c.targetId = result.targetId;
697
+ c.targetParentId = result.targetParentId;
698
+ }
699
+ return c;
700
+ };
701
+ let decidedCandidate = sorted.find((c) => c.unique && c.source === 'target');
702
+ let isUnique = true;
703
+ if (!decidedCandidate) {
704
+ decidedCandidate = sorted.find((c) => c.source === 'target') || sorted[0];
705
+ isUnique = false;
706
+ }
707
+ if (decidedCandidate) {
708
+ const validated = (0, locator_validator_1.validateAndSelectLocator)(decidedCandidate, sorted, result.targetText);
709
+ if (validated !== decidedCandidate) {
710
+ decidedCandidate = validated;
711
+ isUnique = validated.unique;
712
+ }
713
+ }
714
+ if (decidedCandidate) {
715
+ if (!isUnique) {
716
+ if (!decidedCandidate.selector.includes('>> nth=')) {
717
+ decidedCandidate.selector = `${decidedCandidate.selector} >> nth=0`;
718
+ decidedCandidate.locator = `${decidedCandidate.locator}.first()`;
719
+ }
720
+ }
721
+ return assignExtraAttrs(decidedCandidate);
722
+ }
723
+ return null;
724
+ }