synapse-hub 0.0.0-stage → 2.0.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.
- package/.agents/agents/failure-analyst.md +47 -0
- package/.agents/agents/live-explorer.md +41 -0
- package/.agents/agents/orchestrator.md +43 -0
- package/.agents/agents/pom-crafter.md +81 -0
- package/.agents/agents/quality-gatekeeper.md +47 -0
- package/.agents/agents/test-runner.md +43 -0
- package/.agents/rules/context-engineering.md +88 -0
- package/.agents/rules/git-push-protocol.md +101 -0
- package/.agents/rules/graphify.md +14 -0
- package/.agents/rules/lessons-learned.md +23 -0
- package/.agents/rules/mandatory-playwright-live.md +29 -0
- package/.agents/rules/skill-precedence.md +42 -0
- package/.agents/rules/systematic-debugging.md +99 -0
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +239 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +64 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +142 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +70 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +441 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +72 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +214 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +100 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -0
- package/.agents/skills/clean-pom-crafter/SKILL.md +75 -0
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -0
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -0
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +64 -0
- package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +879 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +123 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +103 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +54 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -0
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -0
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -0
- package/.agents/skills/file-and-report-validator/SKILL.md +90 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +114 -0
- package/.agents/skills/github-actions-templates/SKILL.md +334 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +78 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +76 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -0
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -0
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -0
- package/.agents/skills/lam-script-generator/SKILL.md +83 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +86 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -0
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -0
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -0
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +176 -0
- package/.agents/skills/self-healing-locator/SKILL.md +73 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +135 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +70 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +62 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +77 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -0
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +65 -0
- package/.agents/templates/completion_report.md +57 -0
- package/.agents/workflows/graphify.md +10 -0
- package/.agents/workflows/test-impact.md +15 -0
- package/.agents/workflows/verify.md +34 -0
- package/AGENTS.md +115 -0
- package/README.md +537 -2
- package/bin/agent-qa.js +90 -0
- package/package.json +59 -4
- package/skills_manifest.json +262 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +97 -0
- package/templates/PROJECT_STATE_TEMPLATE.md +20 -0
- package/tools/TOOLS_RUNBOOK.md +298 -0
- package/tools/agent_canary.py +501 -0
- package/tools/analyze_project.py +242 -0
- package/tools/api_drift_sentinel.py +208 -0
- package/tools/auto_heal.py +189 -0
- package/tools/browser_helpers.js +593 -0
- package/tools/bug_reporter.py +224 -0
- package/tools/cache_resolver.py +846 -0
- package/tools/check_env.py +288 -0
- package/tools/ci_healing_hook.py +128 -0
- package/tools/cli.py +402 -0
- package/tools/config_decoupler.py +253 -0
- package/tools/db_sandbox.py +341 -0
- package/tools/distill_log.py +152 -0
- package/tools/e2e_flow_prober.ps1 +65 -0
- package/tools/flaky_detector.py +222 -0
- package/tools/fuzz_payloads.py +197 -0
- package/tools/generate_allure_report.ps1 +30 -0
- package/tools/generate_api_matrix.py +193 -0
- package/tools/generate_tc_from_story.py +182 -0
- package/tools/generate_test_reports.ps1 +135 -0
- package/tools/git_pre_commit_guard.py +203 -0
- package/tools/heal_pr_automator.py +195 -0
- package/tools/heuristic_locator_matcher.py +202 -0
- package/tools/infer_linter.py +238 -0
- package/tools/lint_ai_docs.py +254 -0
- package/tools/lookup_tc.py +165 -0
- package/tools/lookup_xpath.py +167 -0
- package/tools/matrix_sharder.py +146 -0
- package/tools/mcp_excel_server.ps1 +251 -0
- package/tools/mcp_server.py +490 -0
- package/tools/micro_mutator.py +309 -0
- package/tools/network_mock_shield.py +170 -0
- package/tools/omni_emitter.py +500 -0
- package/tools/omni_parser.py +435 -0
- package/tools/omni_transpiler.py +168 -0
- package/tools/pinpoint_failure.py +88 -0
- package/tools/project_state.py +169 -0
- package/tools/repo_map.json +12 -0
- package/tools/repo_map.py +277 -0
- package/tools/repo_map.txt +7 -0
- package/tools/safety_firewall.py +245 -0
- package/tools/setup_project.py +265 -0
- package/tools/smart_visual_masker.py +134 -0
- package/tools/test_impact.py +321 -0
- package/tools/tms_sync_bridge.py +342 -0
- package/tools/trace_data_flow.py +240 -0
- package/tools/traffic_session_converter.py +256 -0
- package/tools/universal_ir.py +480 -0
- package/tools/validate_groovy_scripts.py +173 -0
- package/tools/verify_all.py +496 -0
- package/tools/verify_all.sh +4 -0
- package/tools/visual_diff_canvas.py +142 -0
- package/tools/visual_som_helper.py +425 -0
- package/tools/wcag_audit.py +214 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Omni-Emitter: Universal Polyglot Code Generator & Transpiler
|
|
4
|
+
------------------------------------------------------------
|
|
5
|
+
Generates 100% production-ready, idiomatically compliant automation code
|
|
6
|
+
from the Universal Intermediate Representation (universal_ir.ProjectIR).
|
|
7
|
+
Supports:
|
|
8
|
+
1. Playwright TypeScript (playwright-ts)
|
|
9
|
+
2. Playwright Python (playwright-python)
|
|
10
|
+
3. Selenium Java (selenium-java)
|
|
11
|
+
4. Katalon Groovy (katalon-groovy) - Clean POM Flat Architecture
|
|
12
|
+
5. Cypress JavaScript (cypress-js)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from tools.universal_ir import (
|
|
20
|
+
ProjectIR, PageClassIR, TestCaseIR, MethodIR, StepIR, LocatorIR,
|
|
21
|
+
ActionType, LocatorStrategy, AssertionType
|
|
22
|
+
)
|
|
23
|
+
except ImportError:
|
|
24
|
+
from universal_ir import (
|
|
25
|
+
ProjectIR, PageClassIR, TestCaseIR, MethodIR, StepIR, LocatorIR,
|
|
26
|
+
ActionType, LocatorStrategy, AssertionType
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def escape_js(s: str) -> str:
|
|
31
|
+
"""Escapes string for single-quoted JS/TS literals."""
|
|
32
|
+
return s.replace("\\", "\\\\").replace("'", "\\'")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def escape_java(s: str) -> str:
|
|
36
|
+
"""Escapes string for double-quoted Java/Groovy literals."""
|
|
37
|
+
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OmniEmitter:
|
|
41
|
+
"""Universal Code Emitter for Multi-Language Automation Frameworks."""
|
|
42
|
+
|
|
43
|
+
SUPPORTED_TARGETS = [
|
|
44
|
+
"playwright-ts",
|
|
45
|
+
"playwright-python",
|
|
46
|
+
"selenium-java",
|
|
47
|
+
"katalon-groovy",
|
|
48
|
+
"cypress-js"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def emit(cls, project: ProjectIR, target_framework: str) -> Dict[str, str]:
|
|
53
|
+
target = target_framework.lower().strip()
|
|
54
|
+
if target not in cls.SUPPORTED_TARGETS:
|
|
55
|
+
raise ValueError(f"Unsupported target framework '{target_framework}'. Supported: {cls.SUPPORTED_TARGETS}")
|
|
56
|
+
|
|
57
|
+
output_files: Dict[str, str] = {}
|
|
58
|
+
|
|
59
|
+
if target == "playwright-ts":
|
|
60
|
+
output_files.update(cls._emit_playwright_ts(project))
|
|
61
|
+
elif target == "playwright-python":
|
|
62
|
+
output_files.update(cls._emit_playwright_python(project))
|
|
63
|
+
elif target == "selenium-java":
|
|
64
|
+
output_files.update(cls._emit_selenium_java(project))
|
|
65
|
+
elif target == "katalon-groovy":
|
|
66
|
+
output_files.update(cls._emit_katalon_groovy(project))
|
|
67
|
+
elif target == "cypress-js":
|
|
68
|
+
output_files.update(cls._emit_cypress_js(project))
|
|
69
|
+
|
|
70
|
+
return output_files
|
|
71
|
+
|
|
72
|
+
# -------------------------------------------------------------------------
|
|
73
|
+
# 1. Playwright TypeScript Emitter
|
|
74
|
+
# -------------------------------------------------------------------------
|
|
75
|
+
@classmethod
|
|
76
|
+
def _emit_safe_interact_helper(cls) -> str:
|
|
77
|
+
return """import { Page, Locator } from "@playwright/test";
|
|
78
|
+
|
|
79
|
+
export interface SafeInteractOptions {
|
|
80
|
+
action?: 'click' | 'fill' | 'hover' | 'select' | 'upload' | 'check';
|
|
81
|
+
value?: string;
|
|
82
|
+
files?: string | string[];
|
|
83
|
+
fallbackToJsDispatch?: boolean;
|
|
84
|
+
ensureVisible?: boolean;
|
|
85
|
+
retryCount?: number;
|
|
86
|
+
stabilityTimeoutMs?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resilient Cross-Engine Interaction Polyfill
|
|
91
|
+
* Neutralizes layout rendering & event dispatch differences across Chromium, WebKit, and Gecko:
|
|
92
|
+
* 1. Scrolls target into view if needed (ensureVisible)
|
|
93
|
+
* 2. Catches WebKit/Safari overlay interception and gracefully fallbacks to JS dispatchEvent
|
|
94
|
+
* 3. Handles resilient retries during debounce or active animation
|
|
95
|
+
*/
|
|
96
|
+
export async function safeInteract(
|
|
97
|
+
page: Page,
|
|
98
|
+
target: Locator | string,
|
|
99
|
+
options: SafeInteractOptions = {}
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const {
|
|
102
|
+
action = 'click',
|
|
103
|
+
value = '',
|
|
104
|
+
files,
|
|
105
|
+
fallbackToJsDispatch = true,
|
|
106
|
+
ensureVisible = true,
|
|
107
|
+
retryCount = 2,
|
|
108
|
+
stabilityTimeoutMs = 5000
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
const locator = typeof target === 'string' ? page.locator(target) : target;
|
|
112
|
+
|
|
113
|
+
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
114
|
+
try {
|
|
115
|
+
if (ensureVisible) {
|
|
116
|
+
await locator.scrollIntoViewIfNeeded({ timeout: stabilityTimeoutMs }).catch(() => {});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (action === 'click') {
|
|
120
|
+
await locator.click({ timeout: stabilityTimeoutMs });
|
|
121
|
+
} else if (action === 'fill') {
|
|
122
|
+
await locator.fill(value, { timeout: stabilityTimeoutMs });
|
|
123
|
+
} else if (action === 'check') {
|
|
124
|
+
await locator.check({ timeout: stabilityTimeoutMs });
|
|
125
|
+
} else if (action === 'upload' && files) {
|
|
126
|
+
await locator.setInputFiles(files);
|
|
127
|
+
} else if (action === 'hover') {
|
|
128
|
+
await locator.hover({ timeout: stabilityTimeoutMs });
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
const isLastAttempt = attempt === retryCount;
|
|
133
|
+
const isIntercepted = err.message && (
|
|
134
|
+
err.message.includes('intercepted') ||
|
|
135
|
+
err.message.includes('another element') ||
|
|
136
|
+
err.message.includes('not visible') ||
|
|
137
|
+
err.message.includes('pointer-events')
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (fallbackToJsDispatch && isIntercepted) {
|
|
141
|
+
await locator.evaluate((el: any) => {
|
|
142
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
|
143
|
+
}).catch(() => {});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (isLastAttempt) {
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
await page.waitForTimeout(200);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def _emit_playwright_ts(cls, project: ProjectIR) -> Dict[str, str]:
|
|
158
|
+
files: Dict[str, str] = {}
|
|
159
|
+
files["helpers/safeInteract.ts"] = cls._emit_safe_interact_helper()
|
|
160
|
+
|
|
161
|
+
for page in project.pages:
|
|
162
|
+
code = [
|
|
163
|
+
'import { Page, Locator, expect } from "@playwright/test";',
|
|
164
|
+
'import { safeInteract } from "../helpers/safeInteract";',
|
|
165
|
+
"",
|
|
166
|
+
f"export class {page.name} {{",
|
|
167
|
+
" readonly page: Page;",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
for loc in page.locators:
|
|
171
|
+
code.append(f" readonly {loc.name}: Locator;")
|
|
172
|
+
|
|
173
|
+
code.extend([
|
|
174
|
+
"",
|
|
175
|
+
" constructor(page: Page) {",
|
|
176
|
+
" this.page = page;",
|
|
177
|
+
])
|
|
178
|
+
|
|
179
|
+
for loc in page.locators:
|
|
180
|
+
if loc.strategy == LocatorStrategy.ROLE:
|
|
181
|
+
role_parts = loc.value.split("[name='")
|
|
182
|
+
role_name = escape_js(role_parts[0])
|
|
183
|
+
acc_name = escape_js(role_parts[1].rstrip("']") if len(role_parts) > 1 else "")
|
|
184
|
+
code.append(f" this.{loc.name} = page.getByRole('{role_name}' as any, {{ name: '{acc_name}' }});")
|
|
185
|
+
elif loc.strategy == LocatorStrategy.TEST_ID:
|
|
186
|
+
code.append(f" this.{loc.name} = page.getByTestId('{escape_js(loc.value)}');")
|
|
187
|
+
elif loc.strategy == LocatorStrategy.LABEL:
|
|
188
|
+
code.append(f" this.{loc.name} = page.getByLabel('{escape_js(loc.value)}');")
|
|
189
|
+
elif loc.strategy == LocatorStrategy.PLACEHOLDER:
|
|
190
|
+
code.append(f" this.{loc.name} = page.getByPlaceholder('{escape_js(loc.value)}');")
|
|
191
|
+
elif loc.strategy == LocatorStrategy.TEXT:
|
|
192
|
+
code.append(f" this.{loc.name} = page.getByText('{escape_js(loc.value)}');")
|
|
193
|
+
else:
|
|
194
|
+
code.append(f" this.{loc.name} = page.locator('{escape_js(loc.value)}');")
|
|
195
|
+
|
|
196
|
+
code.append(" }")
|
|
197
|
+
|
|
198
|
+
for m in page.methods:
|
|
199
|
+
params_decl = ", ".join([f"{p}: string" for p in m.parameters])
|
|
200
|
+
code.extend([
|
|
201
|
+
"",
|
|
202
|
+
f" async {m.name}({params_decl}): Promise<void> {{",
|
|
203
|
+
])
|
|
204
|
+
if not m.steps:
|
|
205
|
+
code.append(" // Auto-generated method stub")
|
|
206
|
+
for s in m.steps:
|
|
207
|
+
if s.action == ActionType.GOTO:
|
|
208
|
+
code.append(f" await this.page.goto('{escape_js(s.value or '')}');")
|
|
209
|
+
elif s.action == ActionType.CLICK:
|
|
210
|
+
target = s.target or "element"
|
|
211
|
+
code.append(f" await safeInteract(this.page, this.{target}, {{ action: 'click', fallbackToJsDispatch: true, ensureVisible: true }});")
|
|
212
|
+
elif s.action == ActionType.FILL:
|
|
213
|
+
target = s.target or "input"
|
|
214
|
+
val = s.value or "value"
|
|
215
|
+
code.append(f" await expect(this.{target}).toBeEditable();")
|
|
216
|
+
code.append(f" await safeInteract(this.page, this.{target}, {{ action: 'fill', value: '{escape_js(val)}' }});")
|
|
217
|
+
elif s.action == ActionType.WAIT:
|
|
218
|
+
if s.is_sanitized_wait:
|
|
219
|
+
if s.target:
|
|
220
|
+
code.append(f" await this.page.evaluate((sel) => (window as any).__qa?.probeStability(sel), '{escape_js(s.target)}'); // Event-Driven Stability Probe")
|
|
221
|
+
else:
|
|
222
|
+
code.append(" await this.page.waitForLoadState('networkidle'); // Event-Driven Network Idle")
|
|
223
|
+
else:
|
|
224
|
+
code.append(f" await this.page.waitForTimeout({s.timeout_ms});")
|
|
225
|
+
elif s.action == ActionType.ASSERT:
|
|
226
|
+
code.append(" await expect(this.page).toBeDefined();")
|
|
227
|
+
else:
|
|
228
|
+
code.append(f" // {s.description}")
|
|
229
|
+
code.append(" }")
|
|
230
|
+
|
|
231
|
+
code.append("}\n")
|
|
232
|
+
files[f"pages/{page.name}.ts"] = "\n".join(code)
|
|
233
|
+
|
|
234
|
+
if project.test_cases:
|
|
235
|
+
t_code = [
|
|
236
|
+
'import { test, expect } from "@playwright/test";',
|
|
237
|
+
'import { safeInteract } from "../helpers/safeInteract";',
|
|
238
|
+
]
|
|
239
|
+
for page in project.pages:
|
|
240
|
+
t_code.append(f'import {{ {page.name} }} from "./pages/{page.name}";')
|
|
241
|
+
t_code.append("")
|
|
242
|
+
|
|
243
|
+
for tc in project.test_cases:
|
|
244
|
+
t_code.extend([
|
|
245
|
+
f"test('{escape_js(tc.id)}: {escape_js(tc.title)}', async ({{ page }}) => {{",
|
|
246
|
+
f" const uniqueCode = 'AUTO_' + Date.now();",
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
# Emit Hermetic Pre-flight Fixtures if present
|
|
250
|
+
if tc.hermetic_fixtures:
|
|
251
|
+
t_code.append(" // --- [Hermetic Test Pre-flight Fixture Setup] ---")
|
|
252
|
+
for hf in tc.hermetic_fixtures:
|
|
253
|
+
t_code.append(f" // Fixture: {hf.variable_name} ({hf.fixture_type})")
|
|
254
|
+
if hf.code_snippet:
|
|
255
|
+
for c_line in hf.code_snippet.splitlines():
|
|
256
|
+
t_code.append(f" {c_line}")
|
|
257
|
+
t_code.append("")
|
|
258
|
+
|
|
259
|
+
for page in project.pages:
|
|
260
|
+
var_name = page.name[0].lower() + page.name[1:]
|
|
261
|
+
t_code.append(f" const {var_name} = new {page.name}(page);")
|
|
262
|
+
|
|
263
|
+
for s in tc.steps:
|
|
264
|
+
if s.action == ActionType.GOTO:
|
|
265
|
+
t_code.append(f" await page.goto('{escape_js(s.value or '')}');")
|
|
266
|
+
elif s.action == ActionType.CLICK:
|
|
267
|
+
t_code.append(f" await safeInteract(page, '{escape_js(s.target or '')}', {{ action: 'click', fallbackToJsDispatch: true, ensureVisible: true }});")
|
|
268
|
+
elif s.action == ActionType.FILL:
|
|
269
|
+
t_code.append(f" await expect(page.locator('{escape_js(s.target or '')}')).toBeEditable();")
|
|
270
|
+
t_code.append(f" await safeInteract(page, '{escape_js(s.target or '')}', {{ action: 'fill', value: '{escape_js(s.value or '')}' }});")
|
|
271
|
+
elif s.action == ActionType.WAIT:
|
|
272
|
+
if s.is_sanitized_wait:
|
|
273
|
+
if s.target:
|
|
274
|
+
t_code.append(f" await page.evaluate((sel) => (window as any).__qa?.probeStability(sel), '{escape_js(s.target)}'); // Event-Driven Stability Probe")
|
|
275
|
+
else:
|
|
276
|
+
t_code.append(" await page.waitForLoadState('networkidle'); // Event-Driven Network Idle")
|
|
277
|
+
else:
|
|
278
|
+
t_code.append(f" await page.waitForTimeout({s.timeout_ms});")
|
|
279
|
+
elif s.action == ActionType.API_CALL:
|
|
280
|
+
t_code.append(f" await page.request.post('{escape_js(s.target or '')}', {{ data: {s.value or '{}'} }}); // {s.description}")
|
|
281
|
+
elif s.action == ActionType.ASSERT:
|
|
282
|
+
t_code.append(" await expect(page).toHaveURL(/.*\\/.*/);")
|
|
283
|
+
else:
|
|
284
|
+
t_code.append(f" // {s.description}")
|
|
285
|
+
|
|
286
|
+
t_code.append("});\n")
|
|
287
|
+
files[f"tests/{project.name}.spec.ts"] = "\n".join(t_code)
|
|
288
|
+
|
|
289
|
+
return files
|
|
290
|
+
|
|
291
|
+
# -------------------------------------------------------------------------
|
|
292
|
+
# 2. Playwright Python Emitter
|
|
293
|
+
# -------------------------------------------------------------------------
|
|
294
|
+
@classmethod
|
|
295
|
+
def _emit_playwright_python(cls, project: ProjectIR) -> Dict[str, str]:
|
|
296
|
+
files: Dict[str, str] = {}
|
|
297
|
+
|
|
298
|
+
for page in project.pages:
|
|
299
|
+
code = [
|
|
300
|
+
"from playwright.sync_api import Page, expect",
|
|
301
|
+
"",
|
|
302
|
+
f"class {page.name}:",
|
|
303
|
+
" def __init__(self, page: Page):",
|
|
304
|
+
" self.page = page",
|
|
305
|
+
]
|
|
306
|
+
for loc in page.locators:
|
|
307
|
+
if loc.strategy == LocatorStrategy.ROLE:
|
|
308
|
+
role_parts = loc.value.split("[name='")
|
|
309
|
+
r_name = escape_java(role_parts[0])
|
|
310
|
+
acc = escape_java(role_parts[1].rstrip("']") if len(role_parts) > 1 else "")
|
|
311
|
+
code.append(f' self.{loc.name} = page.get_by_role("{r_name}", name="{acc}")')
|
|
312
|
+
elif loc.strategy == LocatorStrategy.TEST_ID:
|
|
313
|
+
code.append(f' self.{loc.name} = page.get_by_test_id("{escape_java(loc.value)}")')
|
|
314
|
+
else:
|
|
315
|
+
code.append(f' self.{loc.name} = page.locator("{escape_java(loc.value)}")')
|
|
316
|
+
|
|
317
|
+
for m in page.methods:
|
|
318
|
+
params_decl = ", ".join(["self"] + m.parameters)
|
|
319
|
+
code.extend([
|
|
320
|
+
"",
|
|
321
|
+
f" def {m.name}({params_decl}):",
|
|
322
|
+
])
|
|
323
|
+
if not m.steps:
|
|
324
|
+
code.append(" pass")
|
|
325
|
+
for s in m.steps:
|
|
326
|
+
if s.action == ActionType.CLICK:
|
|
327
|
+
code.append(f" self.{s.target}.click()")
|
|
328
|
+
elif s.action == ActionType.FILL:
|
|
329
|
+
code.append(f' self.{s.target}.fill("{escape_java(s.value or "")}")')
|
|
330
|
+
else:
|
|
331
|
+
code.append(f" # {s.description}")
|
|
332
|
+
|
|
333
|
+
code.append("\n")
|
|
334
|
+
files[f"pages/{page.name}.py"] = "\n".join(code)
|
|
335
|
+
|
|
336
|
+
return files
|
|
337
|
+
|
|
338
|
+
# -------------------------------------------------------------------------
|
|
339
|
+
# 3. Selenium Java Emitter (Clean POM)
|
|
340
|
+
# -------------------------------------------------------------------------
|
|
341
|
+
@classmethod
|
|
342
|
+
def _emit_selenium_java(cls, project: ProjectIR) -> Dict[str, str]:
|
|
343
|
+
files: Dict[str, str] = {}
|
|
344
|
+
|
|
345
|
+
for page in project.pages:
|
|
346
|
+
code = [
|
|
347
|
+
"package pages;",
|
|
348
|
+
"",
|
|
349
|
+
"import org.openqa.selenium.By;",
|
|
350
|
+
"import org.openqa.selenium.WebDriver;",
|
|
351
|
+
"import org.openqa.selenium.support.ui.WebDriverWait;",
|
|
352
|
+
"import org.openqa.selenium.support.ui.ExpectedConditions;",
|
|
353
|
+
"import java.time.Duration;",
|
|
354
|
+
"",
|
|
355
|
+
f"public class {page.name} {{",
|
|
356
|
+
" private final WebDriver driver;",
|
|
357
|
+
" private final WebDriverWait wait;",
|
|
358
|
+
"",
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
for loc in page.locators:
|
|
362
|
+
if loc.strategy == LocatorStrategy.XPATH:
|
|
363
|
+
code.append(f' private final By {loc.name} = By.xpath("{escape_java(loc.value)}");')
|
|
364
|
+
elif loc.strategy == LocatorStrategy.ID:
|
|
365
|
+
code.append(f' private final By {loc.name} = By.id("{escape_java(loc.value)}");')
|
|
366
|
+
else:
|
|
367
|
+
code.append(f' private final By {loc.name} = By.cssSelector("{escape_java(loc.value)}");')
|
|
368
|
+
|
|
369
|
+
code.extend([
|
|
370
|
+
"",
|
|
371
|
+
f" public {page.name}(WebDriver driver) {{",
|
|
372
|
+
" this.driver = driver;",
|
|
373
|
+
" this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));",
|
|
374
|
+
" }",
|
|
375
|
+
])
|
|
376
|
+
|
|
377
|
+
for m in page.methods:
|
|
378
|
+
params_decl = ", ".join([f"String {p}" for p in m.parameters])
|
|
379
|
+
code.extend([
|
|
380
|
+
"",
|
|
381
|
+
f" public void {m.name}({params_decl}) {{",
|
|
382
|
+
])
|
|
383
|
+
if not m.steps:
|
|
384
|
+
code.append(" // Stub")
|
|
385
|
+
for s in m.steps:
|
|
386
|
+
if s.action == ActionType.CLICK:
|
|
387
|
+
code.append(f" wait.until(ExpectedConditions.elementToBeClickable({s.target})).click();")
|
|
388
|
+
elif s.action == ActionType.FILL:
|
|
389
|
+
code.append(f' wait.until(ExpectedConditions.visibilityOfElementLocated({s.target})).sendKeys("{escape_java(s.value or "")}");')
|
|
390
|
+
else:
|
|
391
|
+
code.append(f" // {s.description}")
|
|
392
|
+
code.append(" }")
|
|
393
|
+
|
|
394
|
+
code.append("}\n")
|
|
395
|
+
files[f"src/main/java/pages/{page.name}.java"] = "\n".join(code)
|
|
396
|
+
|
|
397
|
+
return files
|
|
398
|
+
|
|
399
|
+
# -------------------------------------------------------------------------
|
|
400
|
+
# 4. Katalon Groovy Emitter (Clean POM Flat Architecture)
|
|
401
|
+
# -------------------------------------------------------------------------
|
|
402
|
+
@classmethod
|
|
403
|
+
def _emit_katalon_groovy(cls, project: ProjectIR) -> Dict[str, str]:
|
|
404
|
+
files: Dict[str, str] = {}
|
|
405
|
+
|
|
406
|
+
for page in project.pages:
|
|
407
|
+
code = [
|
|
408
|
+
"package pages",
|
|
409
|
+
"",
|
|
410
|
+
"import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI",
|
|
411
|
+
"import internal.GlobalVariable",
|
|
412
|
+
"",
|
|
413
|
+
f"public class {page.name} extends BasePage {{",
|
|
414
|
+
"",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
for loc in page.locators:
|
|
418
|
+
code.append(f' public static final String {loc.name.upper()} = "{escape_java(loc.value)}"')
|
|
419
|
+
|
|
420
|
+
code.append("")
|
|
421
|
+
|
|
422
|
+
for m in page.methods:
|
|
423
|
+
params_decl = ", ".join([f"String {p}" for p in m.parameters])
|
|
424
|
+
code.extend([
|
|
425
|
+
f" public static void {m.name}({params_decl}) {{",
|
|
426
|
+
])
|
|
427
|
+
for s in m.steps:
|
|
428
|
+
if s.action == ActionType.CLICK:
|
|
429
|
+
code.append(f" clickJS({s.target.upper()})")
|
|
430
|
+
elif s.action == ActionType.FILL:
|
|
431
|
+
code.append(f' setTextSafe({s.target.upper()}, "{escape_java(s.value or "")}")')
|
|
432
|
+
elif s.action == ActionType.GOTO:
|
|
433
|
+
code.append(f' WebUI.navigateToUrl("{escape_java(s.value or "")}")')
|
|
434
|
+
else:
|
|
435
|
+
code.append(f" // {s.description}")
|
|
436
|
+
code.append(" }\n")
|
|
437
|
+
|
|
438
|
+
code.append("}\n")
|
|
439
|
+
files[f"Keywords/pages/{page.name}.groovy"] = "\n".join(code)
|
|
440
|
+
|
|
441
|
+
if project.test_cases:
|
|
442
|
+
for tc in project.test_cases:
|
|
443
|
+
s_code = [
|
|
444
|
+
"import static com.kms.katalon.core.testcase.TestCaseFactory.findTestCase",
|
|
445
|
+
"import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI",
|
|
446
|
+
"import pages.*",
|
|
447
|
+
"",
|
|
448
|
+
f"String uniqueCode = BasePage.uniqueCode('AUTO_')",
|
|
449
|
+
"",
|
|
450
|
+
]
|
|
451
|
+
for s in tc.steps:
|
|
452
|
+
if s.action == ActionType.GOTO:
|
|
453
|
+
s_code.append(f'WebUI.navigateToUrl("{escape_java(s.value or "")}")')
|
|
454
|
+
elif s.action == ActionType.ASSERT:
|
|
455
|
+
s_code.append('WebUI.verifyElementPresent(BasePage.findTestObject("..."), 10)')
|
|
456
|
+
else:
|
|
457
|
+
s_code.append(f'// {s.description}')
|
|
458
|
+
|
|
459
|
+
s_code.append("")
|
|
460
|
+
files[f"Scripts/{tc.id}_{tc.title}/Script.groovy"] = "\n".join(s_code)
|
|
461
|
+
|
|
462
|
+
return files
|
|
463
|
+
|
|
464
|
+
# -------------------------------------------------------------------------
|
|
465
|
+
# 5. Cypress JavaScript Emitter
|
|
466
|
+
# -------------------------------------------------------------------------
|
|
467
|
+
@classmethod
|
|
468
|
+
def _emit_cypress_js(cls, project: ProjectIR) -> Dict[str, str]:
|
|
469
|
+
files: Dict[str, str] = {}
|
|
470
|
+
|
|
471
|
+
for page in project.pages:
|
|
472
|
+
code = [
|
|
473
|
+
f"export class {page.name} {{",
|
|
474
|
+
]
|
|
475
|
+
for loc in page.locators:
|
|
476
|
+
code.extend([
|
|
477
|
+
f" get {loc.name}() {{",
|
|
478
|
+
f" return cy.get('{escape_js(loc.value)}');",
|
|
479
|
+
" }",
|
|
480
|
+
])
|
|
481
|
+
|
|
482
|
+
for m in page.methods:
|
|
483
|
+
params_decl = ", ".join(m.parameters)
|
|
484
|
+
code.extend([
|
|
485
|
+
"",
|
|
486
|
+
f" {m.name}({params_decl}) {{",
|
|
487
|
+
])
|
|
488
|
+
for s in m.steps:
|
|
489
|
+
if s.action == ActionType.CLICK:
|
|
490
|
+
code.append(f" this.{s.target}.click();")
|
|
491
|
+
elif s.action == ActionType.FILL:
|
|
492
|
+
code.append(f" this.{s.target}.type('{escape_js(s.value or '')}');")
|
|
493
|
+
else:
|
|
494
|
+
code.append(f" // {s.description}")
|
|
495
|
+
code.append(" }")
|
|
496
|
+
|
|
497
|
+
code.append("}\n")
|
|
498
|
+
files[f"cypress/pages/{page.name}.js"] = "\n".join(code)
|
|
499
|
+
|
|
500
|
+
return files
|