@qa-gentic/stlc-agents 1.0.26 → 1.0.28
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/ARCHITECTURE-ADO.md +350 -0
- package/ARCHITECTURE-JIRA.md +203 -0
- package/QUICKSTART-ADO.md +400 -0
- package/QUICKSTART-JIRA.md +334 -0
- package/README.md +49 -0
- package/package.json +18 -6
- package/skills/migrate-framework/SKILL.md +207 -0
- package/src/stlc_agents/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/_migrate.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/cli.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/detector.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/mapper.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/reporter.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/_migrate.py +1398 -0
- package/src/stlc_agents/agent_migration/cli.py +217 -0
- package/src/stlc_agents/agent_migration/detector.py +81 -0
- package/src/stlc_agents/agent_migration/mapper.py +439 -0
- package/src/stlc_agents/agent_migration/reporter.py +86 -0
- package/src/stlc_agents/agent_migration/server.py +267 -0
- package/src/stlc_agents/agent_migration/transformer/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/config_merger.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/healer_injector.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/import_fixer.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/js_to_ts.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/local_var_hoister.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/locator_moderniser.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/locator_registrar.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/spec_to_bdd.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/config_merger.py +513 -0
- package/src/stlc_agents/agent_migration/transformer/healer_injector.py +1143 -0
- package/src/stlc_agents/agent_migration/transformer/import_fixer.py +419 -0
- package/src/stlc_agents/agent_migration/transformer/js_to_ts.py +413 -0
- package/src/stlc_agents/agent_migration/transformer/local_var_hoister.py +378 -0
- package/src/stlc_agents/agent_migration/transformer/locator_moderniser.py +132 -0
- package/src/stlc_agents/agent_migration/transformer/locator_registrar.py +328 -0
- package/src/stlc_agents/agent_migration/transformer/spec_to_bdd.py +820 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +926 -91
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/agent_runner.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/models.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/webhook_bridge.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transform plain-string locator files into the agent-friendly format.
|
|
3
|
+
|
|
4
|
+
Input (migrated raw format):
|
|
5
|
+
export const dashboardLocators = {
|
|
6
|
+
userAvatarButton: "navbar_avatar-btn",
|
|
7
|
+
saveButton: 'button.save-button',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
Output (agent-friendly):
|
|
11
|
+
export const dashboardLocators = {
|
|
12
|
+
userAvatarButton: { selector: "navbar_avatar-btn", intent: "user avatar button", stability: 80 },
|
|
13
|
+
saveButton: { selector: "button.save-button", intent: "save button", stability: 30 },
|
|
14
|
+
} as const;
|
|
15
|
+
export type DashboardLocatorKey = keyof typeof dashboardLocators;
|
|
16
|
+
|
|
17
|
+
Arrow-function / method-shorthand entries are kept unchanged because they
|
|
18
|
+
produce dynamic selectors that cannot be statically registered.
|
|
19
|
+
|
|
20
|
+
Text-only values (display strings, URLs, error messages) are also kept as-is.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _camel_to_words(name: str) -> str:
|
|
31
|
+
"""userAvatarButton → 'user avatar button'."""
|
|
32
|
+
s = re.sub(r"([A-Z])", r" \1", name)
|
|
33
|
+
return s.strip().lower()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _infer_stability(selector: str) -> int:
|
|
37
|
+
"""Assign a 0-100 confidence score based on selector robustness."""
|
|
38
|
+
if "[data-testid" in selector or "data-testid=" in selector:
|
|
39
|
+
return 100
|
|
40
|
+
if selector.startswith("#"):
|
|
41
|
+
return 80
|
|
42
|
+
# Bare test-id strings: alphanumeric + hyphens/underscores only (no CSS operators)
|
|
43
|
+
if re.match(r"^[a-z_][\w-]*$", selector):
|
|
44
|
+
return 80
|
|
45
|
+
if "[aria-label=" in selector or "[placeholder=" in selector:
|
|
46
|
+
return 70
|
|
47
|
+
if "[dusk=" in selector or "[data-" in selector:
|
|
48
|
+
return 70
|
|
49
|
+
if selector.startswith(".") or any(c in selector for c in ">+~"):
|
|
50
|
+
return 30
|
|
51
|
+
return 50
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_pure_selector(value: str) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Return True only for values that look like CSS / test-id selectors.
|
|
57
|
+
Excludes: URLs, plain text labels, error messages.
|
|
58
|
+
"""
|
|
59
|
+
if value.startswith("http://") or value.startswith("https://"):
|
|
60
|
+
return False
|
|
61
|
+
# Contains a space AND no CSS-selector chars → probably display text
|
|
62
|
+
if " " in value and not any(c in value for c in "#.[>+~:=*^$|@(\"'"):
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _quote(value: str, original_quote: str) -> str:
|
|
68
|
+
"""Pick inner quote avoiding the same char that appears in value."""
|
|
69
|
+
if '"' in value and "'" not in value:
|
|
70
|
+
return "'"
|
|
71
|
+
if "'" in value and '"' not in value:
|
|
72
|
+
return '"'
|
|
73
|
+
# Prefer double-quotes; escape would be needed only if value has both — rare.
|
|
74
|
+
return '"'
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Per-object body transformer
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def _transform_body(block_name: str, body: str) -> tuple[str, list[str]]:
|
|
82
|
+
"""
|
|
83
|
+
Transform the body text (content between the outer { }) of a locator object.
|
|
84
|
+
Returns (transformed_body, list_of_change_descriptions).
|
|
85
|
+
"""
|
|
86
|
+
changes: list[str] = []
|
|
87
|
+
lines = body.split("\n")
|
|
88
|
+
result: list[str] = []
|
|
89
|
+
i = 0
|
|
90
|
+
|
|
91
|
+
while i < len(lines):
|
|
92
|
+
raw = lines[i]
|
|
93
|
+
stripped = raw.rstrip()
|
|
94
|
+
|
|
95
|
+
# ── Pattern 1: single-line string property ──────────────────────────
|
|
96
|
+
# ` key: "value",` or ` key: 'value',`
|
|
97
|
+
m1 = re.match(r"^( {2,})(\w+)\s*:\s*(['\"])(.*?)\3,?\s*$", stripped)
|
|
98
|
+
if m1:
|
|
99
|
+
indent, key, q, value = m1.group(1), m1.group(2), m1.group(3), m1.group(4)
|
|
100
|
+
if _is_pure_selector(value):
|
|
101
|
+
intent = _camel_to_words(key)
|
|
102
|
+
stability = _infer_stability(value)
|
|
103
|
+
iq = _quote(value, q)
|
|
104
|
+
result.append(
|
|
105
|
+
f"{indent}{key}: {{ selector: {iq}{value}{iq},"
|
|
106
|
+
f' intent: "{intent}", stability: {stability} }},'
|
|
107
|
+
)
|
|
108
|
+
changes.append(f"Registered locator: {block_name}.{key}")
|
|
109
|
+
i += 1
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# ── Pattern 2: multi-line string property ────────────────────────────
|
|
113
|
+
# ` key:\n "value",`
|
|
114
|
+
m2 = re.match(r"^( {2,})(\w+)\s*:\s*$", stripped)
|
|
115
|
+
if m2 and (i + 1) < len(lines):
|
|
116
|
+
next_line = lines[i + 1].strip()
|
|
117
|
+
mval = re.match(r"^(['\"])(.*?)\1,?\s*$", next_line)
|
|
118
|
+
if mval:
|
|
119
|
+
indent, key = m2.group(1), m2.group(2)
|
|
120
|
+
q, value = mval.group(1), mval.group(2)
|
|
121
|
+
if _is_pure_selector(value):
|
|
122
|
+
intent = _camel_to_words(key)
|
|
123
|
+
stability = _infer_stability(value)
|
|
124
|
+
iq = _quote(value, q)
|
|
125
|
+
result.append(
|
|
126
|
+
f"{indent}{key}: {{ selector: {iq}{value}{iq},"
|
|
127
|
+
f' intent: "{intent}", stability: {stability} }},'
|
|
128
|
+
)
|
|
129
|
+
changes.append(f"Registered multi-line locator: {block_name}.{key}")
|
|
130
|
+
i += 2
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# ── Pattern 3: multi-line string-concat property ─────────────────
|
|
134
|
+
# ```
|
|
135
|
+
# key:
|
|
136
|
+
# "a" +
|
|
137
|
+
# "b" +
|
|
138
|
+
# "c",
|
|
139
|
+
# ```
|
|
140
|
+
# Collect adjacent quoted strings joined by `+` until we hit the
|
|
141
|
+
# entry's trailing comma. Emits one `{ selector: "a" + "b" + "c",
|
|
142
|
+
# intent, stability }` entry. Stability is computed from the
|
|
143
|
+
# concatenated string so multi-line selectors get the same score
|
|
144
|
+
# they'd receive if written on one line.
|
|
145
|
+
indent, key = m2.group(1), m2.group(2)
|
|
146
|
+
concat_re = re.compile(r"^(['\"])(.*?)\1\s*(\+)?\s*$")
|
|
147
|
+
parts: list[tuple[str, str]] = [] # (quote, value)
|
|
148
|
+
j = i + 1
|
|
149
|
+
done = False
|
|
150
|
+
while j < len(lines):
|
|
151
|
+
seg = lines[j].strip().rstrip(",")
|
|
152
|
+
terminates_here = lines[j].rstrip().endswith(",")
|
|
153
|
+
seg_m = concat_re.match(seg)
|
|
154
|
+
if not seg_m:
|
|
155
|
+
break
|
|
156
|
+
parts.append((seg_m.group(1), seg_m.group(2)))
|
|
157
|
+
j += 1
|
|
158
|
+
if terminates_here:
|
|
159
|
+
done = True
|
|
160
|
+
break
|
|
161
|
+
if done and len(parts) >= 2:
|
|
162
|
+
joined_value = "".join(p[1] for p in parts)
|
|
163
|
+
if _is_pure_selector(joined_value):
|
|
164
|
+
intent = _camel_to_words(key)
|
|
165
|
+
stability = _infer_stability(joined_value)
|
|
166
|
+
# Preserve the original quoted-string form so authors keep
|
|
167
|
+
# their choice of quote and line-by-line layout signal —
|
|
168
|
+
# the runtime concatenation is identical.
|
|
169
|
+
rendered = " + ".join(f"{q}{v}{q}" for q, v in parts)
|
|
170
|
+
result.append(
|
|
171
|
+
f"{indent}{key}: {{ selector: {rendered},"
|
|
172
|
+
f' intent: "{intent}", stability: {stability} }},'
|
|
173
|
+
)
|
|
174
|
+
changes.append(f"Registered concat locator: {block_name}.{key}")
|
|
175
|
+
i = j
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
result.append(raw)
|
|
179
|
+
i += 1
|
|
180
|
+
|
|
181
|
+
return "\n".join(result), changes
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# File-level transformer
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
# Matches the opening of a locator constant:
|
|
189
|
+
# export const xxxLocators = {
|
|
190
|
+
_BLOCK_OPEN_RE = re.compile(
|
|
191
|
+
r"(export\s+const\s+(\w+Locators)\s*=\s*\{)[ \t]*\n",
|
|
192
|
+
re.MULTILINE,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# After transformation we append: } as const;\nexport type ...
|
|
196
|
+
_AS_CONST_RE = re.compile(r"\}\s*;?\s*$")
|
|
197
|
+
_TYPE_EXPORT_RE = re.compile(r"export type \w+LocatorKey = keyof typeof \w+Locators;")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def transform_locator_file(content: str) -> tuple[str, list[str]]:
|
|
201
|
+
"""
|
|
202
|
+
Transform all locator-constant blocks in a file.
|
|
203
|
+
Returns (transformed_content, changes).
|
|
204
|
+
"""
|
|
205
|
+
if "selector:" in content and "intent:" in content:
|
|
206
|
+
# Already in agent-friendly format — idempotent
|
|
207
|
+
return content, []
|
|
208
|
+
|
|
209
|
+
all_changes: list[str] = []
|
|
210
|
+
output_parts: list[str] = []
|
|
211
|
+
cursor = 0
|
|
212
|
+
|
|
213
|
+
for m in _BLOCK_OPEN_RE.finditer(content):
|
|
214
|
+
block_start = m.start()
|
|
215
|
+
body_start = m.end() # first char after the opening `{\n`
|
|
216
|
+
block_name = m.group(2)
|
|
217
|
+
|
|
218
|
+
# Find the matching closing brace with depth counting
|
|
219
|
+
depth = 1
|
|
220
|
+
j = body_start
|
|
221
|
+
while j < len(content) and depth > 0:
|
|
222
|
+
c = content[j]
|
|
223
|
+
if c == "{":
|
|
224
|
+
depth += 1
|
|
225
|
+
elif c == "}":
|
|
226
|
+
depth -= 1
|
|
227
|
+
j += 1
|
|
228
|
+
# j now points to one past the closing `}`
|
|
229
|
+
body = content[body_start : j - 1] # exclude the final `}`
|
|
230
|
+
suffix = content[j - 1 :] # `}...` to end (or next block)
|
|
231
|
+
|
|
232
|
+
# Transform the body
|
|
233
|
+
new_body, changes = _transform_body(block_name, body)
|
|
234
|
+
all_changes.extend(changes)
|
|
235
|
+
|
|
236
|
+
# Build a pascal-case type name: dashboardLocators → Dashboard
|
|
237
|
+
pascal = block_name[0].upper() + block_name[1:]
|
|
238
|
+
pascal = pascal.replace("Locators", "")
|
|
239
|
+
type_line = f"export type {pascal}LocatorKey = keyof typeof {block_name};"
|
|
240
|
+
|
|
241
|
+
# Everything before this block
|
|
242
|
+
output_parts.append(content[cursor:block_start])
|
|
243
|
+
|
|
244
|
+
# Reconstructed block
|
|
245
|
+
output_parts.append(m.group(1) + "\n") # `export const xxx = {`
|
|
246
|
+
output_parts.append(new_body)
|
|
247
|
+
# Close: `} as const;`
|
|
248
|
+
output_parts.append("} as const;\n")
|
|
249
|
+
if changes: # only add type export if we actually transformed something
|
|
250
|
+
output_parts.append(type_line + "\n")
|
|
251
|
+
|
|
252
|
+
# Skip over the original closing `};` in suffix
|
|
253
|
+
# suffix starts with `}` — find the end of `};` or `}`
|
|
254
|
+
tail_m = re.match(r"\}\s*;?\s*\n?", suffix)
|
|
255
|
+
tail_skip = tail_m.end() if tail_m else 1
|
|
256
|
+
cursor = j - 1 + tail_skip
|
|
257
|
+
|
|
258
|
+
# Append remaining file content (after last block)
|
|
259
|
+
output_parts.append(content[cursor:])
|
|
260
|
+
|
|
261
|
+
return "".join(output_parts), all_changes
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Post-migration: fix selector accesses in page objects / step files
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
def collect_object_keys(locator_file_content: str) -> dict[str, set[str]]:
|
|
269
|
+
"""
|
|
270
|
+
Parse an already-transformed locator file and return a mapping of
|
|
271
|
+
{ locator_name → set_of_keys_that_are_objects }.
|
|
272
|
+
|
|
273
|
+
Only keys with `{ selector: ... }` values are object-valued.
|
|
274
|
+
String-valued keys (display text, URLs) are excluded.
|
|
275
|
+
"""
|
|
276
|
+
result: dict[str, set[str]] = {}
|
|
277
|
+
_OBJ_KEY_RE = re.compile(r"^\s{2,}(\w+)\s*:\s*\{[^}]*selector:", re.MULTILINE)
|
|
278
|
+
|
|
279
|
+
for m_block in _BLOCK_OPEN_RE.finditer(locator_file_content):
|
|
280
|
+
block_name = m_block.group(2)
|
|
281
|
+
body_start = m_block.end()
|
|
282
|
+
depth = 1
|
|
283
|
+
j = body_start
|
|
284
|
+
while j < len(locator_file_content) and depth > 0:
|
|
285
|
+
c = locator_file_content[j]
|
|
286
|
+
if c == "{":
|
|
287
|
+
depth += 1
|
|
288
|
+
elif c == "}":
|
|
289
|
+
depth -= 1
|
|
290
|
+
j += 1
|
|
291
|
+
body = locator_file_content[body_start : j - 1]
|
|
292
|
+
object_keys = {km.group(1) for km in _OBJ_KEY_RE.finditer(body)}
|
|
293
|
+
result[block_name] = object_keys
|
|
294
|
+
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def fix_selector_accesses(
|
|
299
|
+
content: str,
|
|
300
|
+
object_keys_by_name: dict[str, set[str]],
|
|
301
|
+
) -> tuple[str, list[str]]:
|
|
302
|
+
"""
|
|
303
|
+
In a page-object / step file, rewrite `xxxLocators.key` → `xxxLocators.key.selector`
|
|
304
|
+
for every key that is now a structured object `{ selector, intent, stability }`.
|
|
305
|
+
|
|
306
|
+
Leaves:
|
|
307
|
+
- string-valued keys unchanged (display text, URLs)
|
|
308
|
+
- function-call entries unchanged: xxxLocators.fn(arg)
|
|
309
|
+
- already-fixed accesses: xxxLocators.key.selector
|
|
310
|
+
"""
|
|
311
|
+
changes: list[str] = []
|
|
312
|
+
for loc_name, obj_keys in object_keys_by_name.items():
|
|
313
|
+
if not obj_keys:
|
|
314
|
+
continue
|
|
315
|
+
# Build alternation of all object keys for this locator group
|
|
316
|
+
keys_alt = "|".join(re.escape(k) for k in sorted(obj_keys, key=len, reverse=True))
|
|
317
|
+
pattern = re.compile(
|
|
318
|
+
rf"\b({re.escape(loc_name)})\.({keys_alt})\b"
|
|
319
|
+
r"(?!\.(?:selector|intent|stability))" # not already .selector/.intent/.stability
|
|
320
|
+
r"(?!\s*\()", # not a function call
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def _replace(m: re.Match, ln=loc_name) -> str:
|
|
324
|
+
changes.append(f"Added .selector: {m.group(0)}")
|
|
325
|
+
return f"{m.group(1)}.{m.group(2)}.selector"
|
|
326
|
+
|
|
327
|
+
content = pattern.sub(_replace, content)
|
|
328
|
+
return content, changes
|