quantum-framework 0.9.0__py3-none-any.whl

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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,833 @@
1
+ """
2
+ Testing Engine - Code Generator
3
+
4
+ Transforms Testing AST into pytest Python code.
5
+
6
+ Pipeline: TestSuiteNode -> pytest class with test methods
7
+ """
8
+
9
+ from typing import List, Dict, Optional, Any
10
+
11
+ from quantum.runtime.terminal_templates import PyBuilder, py_string, py_id
12
+ from quantum.runtime.testing_templates import (
13
+ TEST_TEMPLATE, BROWSER_IMPORTS, sanitize_test_name, sanitize_class_name,
14
+ )
15
+
16
+ from quantum.core.features.testing_engine.src.ast_nodes import (
17
+ # Core
18
+ QTestSuiteNode, QTestCaseNode, ExpectNode, MockNode_Testing,
19
+ FixtureNode_Testing, SetupNode_Testing, TeardownNode_Testing,
20
+ BeforeEachNode, AfterEachNode, GenerateNode,
21
+ ScenarioNode, GivenNode, WhenNode, ThenNode,
22
+ # Browser
23
+ BrowserConfigNode, NavigateNode, ClickNode, DblClickNode,
24
+ RightClickNode, HoverNode, FillNode, TypeNode_Testing,
25
+ SelectNode_Testing, CheckNode, UncheckNode, KeyboardNode,
26
+ DragNode, ScrollNode_Testing, UploadNode, DownloadNode,
27
+ WaitForNode, ScreenshotNode, InterceptNode, InterceptRespondNode,
28
+ FrameNode_Testing, PopupNode, AuthNode, SaveStateNode, PauseNode,
29
+ # Advanced
30
+ SnapshotNode, FuzzNode, PropertyNode, FuzzFieldNode,
31
+ PerfNode, PerfMetricNode, A11yNode, ChaosNode, ChaosFailNode,
32
+ GeolocationNode,
33
+ )
34
+
35
+
36
+ class QTestCodeGenerator:
37
+ """Generates standalone pytest file from Testing AST nodes."""
38
+
39
+ def __init__(self):
40
+ self._has_browser_tests = False
41
+ self._fixtures: List[FixtureNode_Testing] = []
42
+ self._mocks: List[MockNode_Testing] = []
43
+ self._auth_states: List[AuthNode] = []
44
+ self._browser_config: Optional[BrowserConfigNode] = None
45
+
46
+ def generate(
47
+ self,
48
+ suites: List[QTestSuiteNode],
49
+ title: str = "Quantum Tests",
50
+ browser_config: Optional[BrowserConfigNode] = None,
51
+ fixtures: Optional[List[FixtureNode_Testing]] = None,
52
+ mocks: Optional[List[MockNode_Testing]] = None,
53
+ auth_states: Optional[List[AuthNode]] = None,
54
+ ) -> str:
55
+ """Generate a complete pytest file from test suites."""
56
+ self._browser_config = browser_config
57
+ self._fixtures = fixtures or []
58
+ self._mocks = mocks or []
59
+ self._auth_states = auth_states or []
60
+ self._has_browser_tests = False
61
+
62
+ # Detect if any suite/case uses browser
63
+ for suite in suites:
64
+ if suite.browser:
65
+ self._has_browser_tests = True
66
+ break
67
+ for child in suite.children:
68
+ if isinstance(child, QTestCaseNode) and child.browser:
69
+ self._has_browser_tests = True
70
+ break
71
+
72
+ py = PyBuilder()
73
+
74
+ # Generate fixtures
75
+ self._generate_fixtures(py)
76
+
77
+ # Generate mock fixtures
78
+ self._generate_mocks(py)
79
+
80
+ # Generate auth state fixtures
81
+ self._generate_auth_states(py)
82
+
83
+ # Generate browser config fixture
84
+ if self._has_browser_tests and self._browser_config:
85
+ self._generate_browser_config_fixture(py)
86
+
87
+ # Generate test classes from suites
88
+ for suite in suites:
89
+ self._generate_suite(py, suite)
90
+
91
+ # Build final output
92
+ extra_imports = ""
93
+ if self._has_browser_tests:
94
+ extra_imports = BROWSER_IMPORTS
95
+
96
+ test_code = py.build()
97
+
98
+ return TEST_TEMPLATE.format(
99
+ title=title,
100
+ filename=f"test_{sanitize_test_name(title)}.py",
101
+ extra_imports=extra_imports,
102
+ test_code=test_code,
103
+ )
104
+
105
+ # ------------------------------------------------------------------
106
+ # Fixtures
107
+ # ------------------------------------------------------------------
108
+
109
+ def _generate_fixtures(self, py: PyBuilder):
110
+ for fixture in self._fixtures:
111
+ scope = fixture.scope or 'function'
112
+ py.decorator(f'pytest.fixture(scope="{scope}")')
113
+ py.func_def(fixture.name)
114
+ if fixture.data:
115
+ py.line(f"return json.loads({py_string(fixture.data)})")
116
+ elif fixture.file:
117
+ py.line(f'with open({py_string(fixture.file)}) as f:')
118
+ py.indent()
119
+ py.line("return json.load(f)")
120
+ py.end_block()
121
+ else:
122
+ py.line("return {}")
123
+ py.end_block()
124
+ py.blank()
125
+
126
+ def _generate_mocks(self, py: PyBuilder):
127
+ for mock in self._mocks:
128
+ name = f"mock_{py_id(mock.query or mock.service or 'unknown')}"
129
+ py.decorator('pytest.fixture')
130
+ py.func_def(name)
131
+ py.line(f"mock = MagicMock()")
132
+ if mock.return_value:
133
+ py.line(f"mock.return_value = json.loads({py_string(mock.return_value)})")
134
+ py.line(f"return mock")
135
+ py.end_block()
136
+ py.blank()
137
+
138
+ def _generate_auth_states(self, py: PyBuilder):
139
+ for auth in self._auth_states:
140
+ if not auth.name:
141
+ continue
142
+ name = f"auth_{py_id(auth.name)}"
143
+ py.decorator('pytest.fixture')
144
+ py.func_def(name, 'browser')
145
+ py.line(f'context = browser.new_context(storage_state="{py_id(auth.name)}_state.json")')
146
+ py.line(f"page = context.new_page()")
147
+ # Generate auth steps
148
+ for child in auth.children:
149
+ self._generate_action(py, child, "page")
150
+ py.line(f'context.storage_state(path="{py_id(auth.name)}_state.json")')
151
+ py.line("yield page")
152
+ py.line("context.close()")
153
+ py.end_block()
154
+ py.blank()
155
+
156
+ def _generate_browser_config_fixture(self, py: PyBuilder):
157
+ config = self._browser_config
158
+ py.decorator('pytest.fixture(scope="session")')
159
+ py.func_def('browser_context_args', 'browser_context_args')
160
+ py.line("args = dict(browser_context_args or {})")
161
+ if config.viewport_width and config.viewport_height:
162
+ py.line(f'args["viewport"] = {{"width": {config.viewport_width}, "height": {config.viewport_height}}}')
163
+ if config.locale:
164
+ py.line(f'args["locale"] = {py_string(config.locale)}')
165
+ if config.timezone:
166
+ py.line(f'args["timezone_id"] = {py_string(config.timezone)}')
167
+ if config.video:
168
+ py.line('args["record_video_dir"] = "test-videos/"')
169
+ py.line("return args")
170
+ py.end_block()
171
+ py.blank()
172
+
173
+ if config.base_url:
174
+ py.decorator('pytest.fixture(scope="session")')
175
+ py.func_def('base_url')
176
+ py.line(f'return {py_string(config.base_url)}')
177
+ py.end_block()
178
+ py.blank()
179
+
180
+ # ------------------------------------------------------------------
181
+ # Suite generation
182
+ # ------------------------------------------------------------------
183
+
184
+ def _generate_suite(self, py: PyBuilder, suite: QTestSuiteNode):
185
+ class_name = sanitize_class_name(suite.name)
186
+ py.blank()
187
+ py.class_def(class_name)
188
+ py.docstring(f"Test suite: {suite.name}")
189
+ py.blank()
190
+
191
+ has_content = False
192
+
193
+ for child in suite.children:
194
+ if isinstance(child, SetupNode_Testing):
195
+ self._generate_setup(py, child)
196
+ has_content = True
197
+ elif isinstance(child, TeardownNode_Testing):
198
+ self._generate_teardown(py, child)
199
+ has_content = True
200
+ elif isinstance(child, BeforeEachNode):
201
+ self._generate_before_each(py, child, suite.browser)
202
+ has_content = True
203
+ elif isinstance(child, AfterEachNode):
204
+ self._generate_after_each(py, child, suite.browser)
205
+ has_content = True
206
+ elif isinstance(child, QTestCaseNode):
207
+ is_browser = child.browser or suite.browser
208
+ self._generate_test_case(py, child, is_browser)
209
+ has_content = True
210
+ elif isinstance(child, ScenarioNode):
211
+ self._generate_scenario(py, child, suite.browser)
212
+ has_content = True
213
+
214
+ if not has_content:
215
+ py.pass_stmt()
216
+
217
+ py.end_block()
218
+ py.blank()
219
+
220
+ # ------------------------------------------------------------------
221
+ # Hooks
222
+ # ------------------------------------------------------------------
223
+
224
+ def _generate_setup(self, py: PyBuilder, node: SetupNode_Testing):
225
+ py.decorator('classmethod')
226
+ py.method_def('setup_class', 'cls')
227
+ if node.children:
228
+ for child in node.children:
229
+ self._generate_action(py, child, "cls")
230
+ else:
231
+ py.pass_stmt()
232
+ py.end_block()
233
+ py.blank()
234
+
235
+ def _generate_teardown(self, py: PyBuilder, node: TeardownNode_Testing):
236
+ py.decorator('classmethod')
237
+ py.method_def('teardown_class', 'cls')
238
+ if node.children:
239
+ for child in node.children:
240
+ self._generate_action(py, child, "cls")
241
+ else:
242
+ py.pass_stmt()
243
+ py.end_block()
244
+ py.blank()
245
+
246
+ def _generate_before_each(self, py: PyBuilder, node: BeforeEachNode, browser: bool):
247
+ if browser:
248
+ py.method_def('setup_method', 'self, page: Page')
249
+ else:
250
+ py.method_def('setup_method', 'self')
251
+ if node.children:
252
+ for child in node.children:
253
+ self._generate_action(py, child, "page" if browser else "self")
254
+ else:
255
+ py.pass_stmt()
256
+ py.end_block()
257
+ py.blank()
258
+
259
+ def _generate_after_each(self, py: PyBuilder, node: AfterEachNode, browser: bool):
260
+ if browser:
261
+ py.method_def('teardown_method', 'self, page: Page')
262
+ else:
263
+ py.method_def('teardown_method', 'self')
264
+ if node.children:
265
+ for child in node.children:
266
+ self._generate_action(py, child, "page" if browser else "self")
267
+ else:
268
+ py.pass_stmt()
269
+ py.end_block()
270
+ py.blank()
271
+
272
+ # ------------------------------------------------------------------
273
+ # Test case generation
274
+ # ------------------------------------------------------------------
275
+
276
+ def _generate_test_case(self, py: PyBuilder, case: QTestCaseNode, is_browser: bool):
277
+ method_name = f"test_{sanitize_test_name(case.name)}"
278
+
279
+ if is_browser:
280
+ py.method_def(method_name, 'self, page: Page')
281
+ else:
282
+ py.method_def(method_name, 'self')
283
+
284
+ if not case.children:
285
+ py.pass_stmt()
286
+ py.end_block()
287
+ py.blank()
288
+ return
289
+
290
+ page_var = "page" if is_browser else None
291
+
292
+ for child in case.children:
293
+ self._generate_action(py, child, page_var)
294
+
295
+ py.end_block()
296
+ py.blank()
297
+
298
+ def _generate_scenario(self, py: PyBuilder, scenario: ScenarioNode, is_browser: bool):
299
+ method_name = f"test_{sanitize_test_name(scenario.name)}"
300
+
301
+ if is_browser:
302
+ py.method_def(method_name, 'self, page: Page')
303
+ else:
304
+ py.method_def(method_name, 'self')
305
+
306
+ page_var = "page" if is_browser else None
307
+
308
+ for child in scenario.children:
309
+ if isinstance(child, GivenNode):
310
+ py.comment(f"Given: {child.text}")
311
+ for step in child.children:
312
+ self._generate_action(py, step, page_var)
313
+ elif isinstance(child, WhenNode):
314
+ py.comment(f"When: {child.text}")
315
+ for step in child.children:
316
+ self._generate_action(py, step, page_var)
317
+ elif isinstance(child, ThenNode):
318
+ py.comment(f"Then: {child.text}")
319
+ for step in child.children:
320
+ self._generate_action(py, step, page_var)
321
+ else:
322
+ self._generate_action(py, child, page_var)
323
+
324
+ if not scenario.children:
325
+ py.pass_stmt()
326
+
327
+ py.end_block()
328
+ py.blank()
329
+
330
+ # ------------------------------------------------------------------
331
+ # Action dispatch
332
+ # ------------------------------------------------------------------
333
+
334
+ def _generate_action(self, py: PyBuilder, node, page_var: Optional[str] = None):
335
+ """Generate code for a single action node."""
336
+ if isinstance(node, ExpectNode):
337
+ self._gen_expect(py, node, page_var)
338
+ elif isinstance(node, NavigateNode):
339
+ self._gen_navigate(py, node, page_var)
340
+ elif isinstance(node, ClickNode):
341
+ self._gen_click(py, node, page_var)
342
+ elif isinstance(node, DblClickNode):
343
+ self._gen_dblclick(py, node, page_var)
344
+ elif isinstance(node, RightClickNode):
345
+ self._gen_right_click(py, node, page_var)
346
+ elif isinstance(node, HoverNode):
347
+ self._gen_hover(py, node, page_var)
348
+ elif isinstance(node, FillNode):
349
+ self._gen_fill(py, node, page_var)
350
+ elif isinstance(node, TypeNode_Testing):
351
+ self._gen_type(py, node, page_var)
352
+ elif isinstance(node, SelectNode_Testing):
353
+ self._gen_select(py, node, page_var)
354
+ elif isinstance(node, CheckNode):
355
+ self._gen_check(py, node, page_var)
356
+ elif isinstance(node, UncheckNode):
357
+ self._gen_uncheck(py, node, page_var)
358
+ elif isinstance(node, KeyboardNode):
359
+ self._gen_keyboard(py, node, page_var)
360
+ elif isinstance(node, DragNode):
361
+ self._gen_drag(py, node, page_var)
362
+ elif isinstance(node, ScrollNode_Testing):
363
+ self._gen_scroll(py, node, page_var)
364
+ elif isinstance(node, UploadNode):
365
+ self._gen_upload(py, node, page_var)
366
+ elif isinstance(node, DownloadNode):
367
+ self._gen_download(py, node, page_var)
368
+ elif isinstance(node, WaitForNode):
369
+ self._gen_wait_for(py, node, page_var)
370
+ elif isinstance(node, ScreenshotNode):
371
+ self._gen_screenshot(py, node, page_var)
372
+ elif isinstance(node, InterceptNode):
373
+ self._gen_intercept(py, node, page_var)
374
+ elif isinstance(node, FrameNode_Testing):
375
+ self._gen_frame(py, node, page_var)
376
+ elif isinstance(node, PopupNode):
377
+ self._gen_popup(py, node, page_var)
378
+ elif isinstance(node, SaveStateNode):
379
+ self._gen_save_state(py, node, page_var)
380
+ elif isinstance(node, PauseNode):
381
+ self._gen_pause(py, node, page_var)
382
+ elif isinstance(node, SnapshotNode):
383
+ self._gen_snapshot(py, node, page_var)
384
+ elif isinstance(node, FuzzNode):
385
+ self._gen_fuzz(py, node, page_var)
386
+ elif isinstance(node, PerfNode):
387
+ self._gen_perf(py, node, page_var)
388
+ elif isinstance(node, A11yNode):
389
+ self._gen_a11y(py, node, page_var)
390
+ elif isinstance(node, ChaosNode):
391
+ self._gen_chaos(py, node, page_var)
392
+ elif isinstance(node, GeolocationNode):
393
+ self._gen_geolocation(py, node, page_var)
394
+
395
+ # ------------------------------------------------------------------
396
+ # Expect generation
397
+ # ------------------------------------------------------------------
398
+
399
+ def _gen_expect(self, py: PyBuilder, node: ExpectNode, page_var: Optional[str]):
400
+ negate = node.not_
401
+
402
+ # Variable-based assertions (non-browser)
403
+ if node.var and node.equals is not None:
404
+ if negate:
405
+ py.line(f"assert {node.var} != {node.equals}")
406
+ else:
407
+ py.line(f"assert {node.var} == {node.equals}")
408
+ return
409
+
410
+ if node.var and node.text_contains is not None:
411
+ if negate:
412
+ py.line(f"assert {py_string(node.text_contains)} not in str({node.var})")
413
+ else:
414
+ py.line(f"assert {py_string(node.text_contains)} in str({node.var})")
415
+ return
416
+
417
+ # URL assertions
418
+ if node.url and page_var:
419
+ if negate:
420
+ py.line(f'expect({page_var}).not_to_have_url({py_string(node.url)})')
421
+ else:
422
+ py.line(f'expect({page_var}).to_have_url({py_string(node.url)})')
423
+ return
424
+
425
+ if node.url_contains and page_var:
426
+ import re
427
+ if negate:
428
+ py.line(f'expect({page_var}).not_to_have_url(re.compile({py_string(node.url_contains)}))')
429
+ else:
430
+ py.line(f'expect({page_var}).to_have_url(re.compile({py_string(node.url_contains)}))')
431
+ return
432
+
433
+ # Selector-based assertions (browser)
434
+ if node.selector and page_var:
435
+ locator = f'{page_var}.locator({py_string(node.selector)})'
436
+
437
+ if node.visible is not None:
438
+ if (node.visible and not negate) or (not node.visible and negate):
439
+ py.line(f"expect({locator}).to_be_visible()")
440
+ else:
441
+ py.line(f"expect({locator}).not_to_be_visible()")
442
+
443
+ if node.hidden is not None:
444
+ if (node.hidden and not negate) or (not node.hidden and negate):
445
+ py.line(f"expect({locator}).to_be_hidden()")
446
+ else:
447
+ py.line(f"expect({locator}).not_to_be_hidden()")
448
+
449
+ if node.text is not None:
450
+ if negate:
451
+ py.line(f"expect({locator}).not_to_have_text({py_string(node.text)})")
452
+ else:
453
+ py.line(f"expect({locator}).to_have_text({py_string(node.text)})")
454
+
455
+ if node.text_contains is not None:
456
+ if negate:
457
+ py.line(f"expect({locator}).not_to_contain_text({py_string(node.text_contains)})")
458
+ else:
459
+ py.line(f"expect({locator}).to_contain_text({py_string(node.text_contains)})")
460
+
461
+ if node.count is not None:
462
+ if negate:
463
+ py.line(f"expect({locator}).not_to_have_count({node.count})")
464
+ else:
465
+ py.line(f"expect({locator}).to_have_count({node.count})")
466
+
467
+ if node.value is not None:
468
+ if negate:
469
+ py.line(f"expect({locator}).not_to_have_value({py_string(node.value)})")
470
+ else:
471
+ py.line(f"expect({locator}).to_have_value({py_string(node.value)})")
472
+
473
+ if node.enabled is not None:
474
+ if (node.enabled and not negate) or (not node.enabled and negate):
475
+ py.line(f"expect({locator}).to_be_enabled()")
476
+ else:
477
+ py.line(f"expect({locator}).to_be_disabled()")
478
+
479
+ if node.disabled is not None:
480
+ if (node.disabled and not negate) or (not node.disabled and negate):
481
+ py.line(f"expect({locator}).to_be_disabled()")
482
+ else:
483
+ py.line(f"expect({locator}).to_be_enabled()")
484
+
485
+ if node.checked is not None:
486
+ if (node.checked and not negate) or (not node.checked and negate):
487
+ py.line(f"expect({locator}).to_be_checked()")
488
+ else:
489
+ py.line(f"expect({locator}).not_to_be_checked()")
490
+
491
+ if node.has_class is not None:
492
+ if negate:
493
+ py.line(f"expect({locator}).not_to_have_class(re.compile({py_string(node.has_class)}))")
494
+ else:
495
+ py.line(f"expect({locator}).to_have_class(re.compile({py_string(node.has_class)}))")
496
+
497
+ if node.has_attribute is not None:
498
+ if negate:
499
+ py.line(f"expect({locator}).not_to_have_attribute({py_string(node.has_attribute)})")
500
+ else:
501
+ py.line(f"expect({locator}).to_have_attribute({py_string(node.has_attribute)})")
502
+
503
+ # ------------------------------------------------------------------
504
+ # Browser action generation
505
+ # ------------------------------------------------------------------
506
+
507
+ def _gen_navigate(self, py: PyBuilder, node: NavigateNode, page_var: Optional[str]):
508
+ if not page_var:
509
+ return
510
+ if node.to:
511
+ wait_arg = f', wait_until={py_string(node.wait)}' if node.wait else ''
512
+ py.line(f'{page_var}.goto({py_string(node.to)}{wait_arg})')
513
+ elif node.back:
514
+ py.line(f'{page_var}.go_back()')
515
+ elif node.forward:
516
+ py.line(f'{page_var}.go_forward()')
517
+
518
+ def _get_locator(self, node, page_var: str) -> str:
519
+ """Build a Playwright locator expression from a node with selector/text/role/test_id."""
520
+ if hasattr(node, 'test_id') and node.test_id:
521
+ return f'{page_var}.get_by_test_id({py_string(node.test_id)})'
522
+ if hasattr(node, 'role') and node.role:
523
+ name_arg = f', name={py_string(node.text)}' if hasattr(node, 'text') and node.text else ''
524
+ return f'{page_var}.get_by_role({py_string(node.role)}{name_arg})'
525
+ if hasattr(node, 'text') and node.text and not (hasattr(node, 'selector') and node.selector):
526
+ return f'{page_var}.get_by_text({py_string(node.text)})'
527
+ if hasattr(node, 'xpath') and node.xpath:
528
+ return f'{page_var}.locator({py_string("xpath=" + node.xpath)})'
529
+ if hasattr(node, 'selector') and node.selector:
530
+ return f'{page_var}.locator({py_string(node.selector)})'
531
+ return f'{page_var}.locator("body")'
532
+
533
+ def _gen_click(self, py: PyBuilder, node: ClickNode, page_var: Optional[str]):
534
+ if not page_var:
535
+ return
536
+ locator = self._get_locator(node, page_var)
537
+ py.line(f'{locator}.click()')
538
+
539
+ def _gen_dblclick(self, py: PyBuilder, node: DblClickNode, page_var: Optional[str]):
540
+ if not page_var:
541
+ return
542
+ locator = self._get_locator(node, page_var)
543
+ py.line(f'{locator}.dblclick()')
544
+
545
+ def _gen_right_click(self, py: PyBuilder, node: RightClickNode, page_var: Optional[str]):
546
+ if not page_var:
547
+ return
548
+ locator = self._get_locator(node, page_var)
549
+ py.line(f'{locator}.click(button="right")')
550
+
551
+ def _gen_hover(self, py: PyBuilder, node: HoverNode, page_var: Optional[str]):
552
+ if not page_var:
553
+ return
554
+ locator = self._get_locator(node, page_var)
555
+ py.line(f'{locator}.hover()')
556
+
557
+ def _gen_fill(self, py: PyBuilder, node: FillNode, page_var: Optional[str]):
558
+ if not page_var:
559
+ return
560
+ sel = py_string(node.selector) if node.selector else '"input"'
561
+ if node.clear:
562
+ py.line(f'{page_var}.locator({sel}).fill({py_string(node.value)})')
563
+ else:
564
+ py.line(f'{page_var}.locator({sel}).type({py_string(node.value)})')
565
+
566
+ def _gen_type(self, py: PyBuilder, node: TypeNode_Testing, page_var: Optional[str]):
567
+ if not page_var:
568
+ return
569
+ sel = py_string(node.selector) if node.selector else '"input"'
570
+ delay_arg = f', delay={node.delay}' if node.delay else ''
571
+ py.line(f'{page_var}.locator({sel}).type({py_string(node.text)}{delay_arg})')
572
+
573
+ def _gen_select(self, py: PyBuilder, node: SelectNode_Testing, page_var: Optional[str]):
574
+ if not page_var:
575
+ return
576
+ sel = py_string(node.selector) if node.selector else '"select"'
577
+ if node.values:
578
+ values = [py_string(v.strip()) for v in node.values.split(',')]
579
+ py.line(f'{page_var}.locator({sel}).select_option([{", ".join(values)}])')
580
+ elif node.value:
581
+ py.line(f'{page_var}.locator({sel}).select_option({py_string(node.value)})')
582
+
583
+ def _gen_check(self, py: PyBuilder, node: CheckNode, page_var: Optional[str]):
584
+ if not page_var:
585
+ return
586
+ sel = py_string(node.selector) if node.selector else '"input"'
587
+ py.line(f'{page_var}.locator({sel}).check()')
588
+
589
+ def _gen_uncheck(self, py: PyBuilder, node: UncheckNode, page_var: Optional[str]):
590
+ if not page_var:
591
+ return
592
+ sel = py_string(node.selector) if node.selector else '"input"'
593
+ py.line(f'{page_var}.locator({sel}).uncheck()')
594
+
595
+ def _gen_keyboard(self, py: PyBuilder, node: KeyboardNode, page_var: Optional[str]):
596
+ if not page_var:
597
+ return
598
+ if node.press:
599
+ py.line(f'{page_var}.keyboard.press({py_string(node.press)})')
600
+ if node.type_text:
601
+ delay_arg = f', delay={node.delay}' if node.delay else ''
602
+ py.line(f'{page_var}.keyboard.type({py_string(node.type_text)}{delay_arg})')
603
+
604
+ def _gen_drag(self, py: PyBuilder, node: DragNode, page_var: Optional[str]):
605
+ if not page_var:
606
+ return
607
+ py.line(f'{page_var}.drag_and_drop({py_string(node.from_sel)}, {py_string(node.to_sel)})')
608
+
609
+ def _gen_scroll(self, py: PyBuilder, node: ScrollNode_Testing, page_var: Optional[str]):
610
+ if not page_var:
611
+ return
612
+ if node.to_element:
613
+ py.line(f'{page_var}.locator({py_string(node.to_element)}).scroll_into_view_if_needed()')
614
+ elif node.to_top:
615
+ py.line(f'{page_var}.evaluate("window.scrollTo(0, 0)")')
616
+ elif node.amount:
617
+ delta = node.amount if node.direction == 'down' else -node.amount
618
+ py.line(f'{page_var}.mouse.wheel(0, {delta})')
619
+ else:
620
+ py.line(f'{page_var}.mouse.wheel(0, 300)')
621
+
622
+ def _gen_upload(self, py: PyBuilder, node: UploadNode, page_var: Optional[str]):
623
+ if not page_var:
624
+ return
625
+ sel = py_string(node.selector) if node.selector else '"input[type=file]"'
626
+ if node.files:
627
+ files = [py_string(f.strip()) for f in node.files.split(',')]
628
+ py.line(f'{page_var}.locator({sel}).set_input_files([{", ".join(files)}])')
629
+ elif node.file:
630
+ py.line(f'{page_var}.locator({sel}).set_input_files({py_string(node.file)})')
631
+
632
+ def _gen_download(self, py: PyBuilder, node: DownloadNode, page_var: Optional[str]):
633
+ if not page_var:
634
+ return
635
+ py.line(f'with {page_var}.expect_download() as download_info:')
636
+ py.indent()
637
+ py.line(f'{page_var}.locator({py_string(node.trigger_click)}).click()')
638
+ py.end_block()
639
+ py.line('download = download_info.value')
640
+ if node.save_as:
641
+ py.line(f'download.save_as({py_string(node.save_as)})')
642
+
643
+ def _gen_wait_for(self, py: PyBuilder, node: WaitForNode, page_var: Optional[str]):
644
+ if not page_var:
645
+ return
646
+ if node.selector:
647
+ args = []
648
+ if node.state:
649
+ args.append(f'state={py_string(node.state)}')
650
+ if node.timeout:
651
+ args.append(f'timeout={node.timeout}')
652
+ args_str = ', '.join(args)
653
+ py.line(f'{page_var}.locator({py_string(node.selector)}).wait_for({args_str})')
654
+ elif node.url:
655
+ timeout_arg = f', timeout={node.timeout}' if node.timeout else ''
656
+ py.line(f'{page_var}.wait_for_url({py_string(node.url)}{timeout_arg})')
657
+ elif node.request:
658
+ timeout_arg = f', timeout={node.timeout}' if node.timeout else ''
659
+ py.line(f'{page_var}.expect_request({py_string(node.request)}{timeout_arg})')
660
+ elif node.response:
661
+ timeout_arg = f', timeout={node.timeout}' if node.timeout else ''
662
+ py.line(f'{page_var}.expect_response({py_string(node.response)}{timeout_arg})')
663
+ elif node.condition:
664
+ timeout_arg = f', timeout={node.timeout}' if node.timeout else ''
665
+ py.line(f'{page_var}.wait_for_function({py_string(node.condition)}{timeout_arg})')
666
+
667
+ def _gen_screenshot(self, py: PyBuilder, node: ScreenshotNode, page_var: Optional[str]):
668
+ if not page_var:
669
+ return
670
+ name = node.name or 'screenshot'
671
+ if node.selector:
672
+ py.line(f'{page_var}.locator({py_string(node.selector)}).screenshot(path={py_string(name + ".png")})')
673
+ else:
674
+ full_page_arg = ', full_page=True' if node.full_page else ''
675
+ py.line(f'{page_var}.screenshot(path={py_string(name + ".png")}{full_page_arg})')
676
+
677
+ def _gen_intercept(self, py: PyBuilder, node: InterceptNode, page_var: Optional[str]):
678
+ if not page_var:
679
+ return
680
+
681
+ # Find respond child
682
+ respond = None
683
+ for child in node.children:
684
+ if isinstance(child, InterceptRespondNode):
685
+ respond = child
686
+ break
687
+
688
+ url_pattern = py_string(node.url) if node.url else '"**/*"'
689
+
690
+ if node.spy:
691
+ py.line(f'requests_log = []')
692
+ py.line(f'{page_var}.on("request", lambda req: requests_log.append(req))')
693
+ elif respond:
694
+ body = respond.body or '{}'
695
+ py.line(f'def handle_route(route):')
696
+ py.indent()
697
+ if node.delay:
698
+ py.line(f'time.sleep({node.delay / 1000})')
699
+ py.line(f'route.fulfill(status={respond.status}, content_type={py_string(respond.content_type)}, body={py_string(body)})')
700
+ py.end_block()
701
+ py.line(f'{page_var}.route({url_pattern}, handle_route)')
702
+ elif node.passthrough:
703
+ py.line(f'{page_var}.route({url_pattern}, lambda route: route.continue_())')
704
+
705
+ def _gen_frame(self, py: PyBuilder, node: FrameNode_Testing, page_var: Optional[str]):
706
+ if not page_var:
707
+ return
708
+ py.line(f'frame = {page_var}.frame_locator({py_string(node.selector)})')
709
+ for child in node.children:
710
+ self._generate_action(py, child, "frame")
711
+
712
+ def _gen_popup(self, py: PyBuilder, node: PopupNode, page_var: Optional[str]):
713
+ if not page_var:
714
+ return
715
+ py.line(f'with {page_var}.expect_popup() as popup_info:')
716
+ py.indent()
717
+ if node.children:
718
+ self._generate_action(py, node.children[0], page_var)
719
+ else:
720
+ py.pass_stmt()
721
+ py.end_block()
722
+ py.line('popup = popup_info.value')
723
+ for child in node.children[1:]:
724
+ self._generate_action(py, child, "popup")
725
+
726
+ def _gen_save_state(self, py: PyBuilder, node: SaveStateNode, page_var: Optional[str]):
727
+ if not page_var:
728
+ return
729
+ py.line(f'{page_var}.context.storage_state(path={py_string(node.file)})')
730
+
731
+ def _gen_pause(self, py: PyBuilder, node: PauseNode, page_var: Optional[str]):
732
+ if page_var:
733
+ py.line(f'{page_var}.pause()')
734
+ else:
735
+ py.line('import pdb; pdb.set_trace()')
736
+
737
+ # ------------------------------------------------------------------
738
+ # Advanced testing generation
739
+ # ------------------------------------------------------------------
740
+
741
+ def _gen_snapshot(self, py: PyBuilder, node: SnapshotNode, page_var: Optional[str]):
742
+ if not page_var:
743
+ return
744
+ name = node.name or 'snapshot'
745
+ py.line(f'expect({page_var}).to_have_screenshot({py_string(name + ".png")}, threshold={node.threshold})')
746
+
747
+ def _gen_fuzz(self, py: PyBuilder, node: FuzzNode, page_var: Optional[str]):
748
+ py.line(f'import random, string')
749
+ py.blank()
750
+ py.comment(f"Fuzz testing: {node.target} ({node.iterations} iterations)")
751
+ py.for_block('_fuzz_i', f'range({node.iterations})')
752
+
753
+ # Generate random values for fields
754
+ for child in node.children:
755
+ if isinstance(child, FuzzFieldNode):
756
+ gen = child.generator or 'string'
757
+ if gen == 'string':
758
+ py.line(f'{py_id(child.name)} = "".join(random.choices(string.ascii_letters, k=random.randint(1, 100)))')
759
+ elif gen == 'int':
760
+ py.line(f'{py_id(child.name)} = random.randint(-1000000, 1000000)')
761
+ elif gen == 'email':
762
+ py.line(f'{py_id(child.name)} = "".join(random.choices(string.ascii_lowercase, k=8)) + "@test.com"')
763
+ elif gen == 'xss':
764
+ py.line(f'{py_id(child.name)} = random.choice(["<script>alert(1)</script>", "<img onerror=alert(1)>", "\\" onmouseover=\\"alert(1)"])')
765
+ elif gen == 'sqli':
766
+ py.line(f"""{py_id(child.name)} = random.choice(["' OR 1=1--", "'; DROP TABLE users;--", "1 UNION SELECT * FROM users"])""")
767
+ else:
768
+ py.line(f'{py_id(child.name)} = "fuzz_" + str(_fuzz_i)')
769
+
770
+ elif isinstance(child, PropertyNode):
771
+ py.line(f'assert {child.text} # Property check')
772
+
773
+ py.end_block()
774
+
775
+ def _gen_perf(self, py: PyBuilder, node: PerfNode, page_var: Optional[str]):
776
+ py.comment("Performance test")
777
+ py.line('_perf_start = time.time()')
778
+
779
+ for child in node.children:
780
+ if isinstance(child, PerfMetricNode):
781
+ if child.name == 'lcp' and page_var:
782
+ py.line(f'_lcp = {page_var}.evaluate("() => {{ const entries = performance.getEntriesByType(\'largest-contentful-paint\'); return entries.length ? entries[entries.length - 1].startTime : 0; }}")')
783
+ if child.max_value:
784
+ py.line(f'assert _lcp <= {child.max_value}, f"LCP {{_lcp}}ms exceeds {child.max_value}ms"')
785
+ elif child.name == 'fcp' and page_var:
786
+ py.line(f'_fcp = {page_var}.evaluate("() => {{ const entries = performance.getEntriesByType(\'paint\').filter(e => e.name === \'first-contentful-paint\'); return entries.length ? entries[0].startTime : 0; }}")')
787
+ if child.max_value:
788
+ py.line(f'assert _fcp <= {child.max_value}, f"FCP {{_fcp}}ms exceeds {child.max_value}ms"')
789
+ elif child.name and child.max_value:
790
+ py.line(f'# Metric: {child.name} max={child.max_value}')
791
+ else:
792
+ self._generate_action(py, child, page_var)
793
+
794
+ py.line('_perf_elapsed = (time.time() - _perf_start) * 1000')
795
+ if node.regression_threshold:
796
+ py.line(f'# Regression threshold: {node.regression_threshold * 100}%')
797
+
798
+ def _gen_a11y(self, py: PyBuilder, node: A11yNode, page_var: Optional[str]):
799
+ if not page_var:
800
+ return
801
+ py.comment(f"Accessibility check: {node.standard}")
802
+ py.line(f'_a11y_results = {page_var}.evaluate("""() => {{')
803
+ py.line(f' // Inject axe-core and run check')
804
+ py.line(f' return new Promise((resolve) => {{')
805
+ py.line(f' const script = document.createElement("script");')
806
+ py.line(f' script.src = "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.0/axe.min.js";')
807
+ py.line(f' script.onload = () => axe.run().then(resolve);')
808
+ py.line(f' document.head.appendChild(script);')
809
+ py.line(f' }});')
810
+ py.line(f'}}""")')
811
+ py.line(f'assert len(_a11y_results.get("violations", [])) == 0, f"Accessibility violations: {{_a11y_results.get(\'violations\', [])}}"')
812
+
813
+ def _gen_chaos(self, py: PyBuilder, node: ChaosNode, page_var: Optional[str]):
814
+ py.comment("Chaos testing")
815
+ has_fail = any(isinstance(child, ChaosFailNode) for child in node.children)
816
+ if has_fail:
817
+ py.line(f'import random')
818
+ for child in node.children:
819
+ if isinstance(child, ChaosFailNode):
820
+ py.comment(f"Simulate failure: {child.service} (probability: {child.probability})")
821
+ py.if_block(f'random.random() < {child.probability}')
822
+ if child.delay:
823
+ py.line(f'time.sleep({child.delay / 1000})')
824
+ py.line(f'# Service {child.service} is unavailable')
825
+ py.end_block()
826
+ else:
827
+ self._generate_action(py, child, page_var)
828
+
829
+ def _gen_geolocation(self, py: PyBuilder, node: GeolocationNode, page_var: Optional[str]):
830
+ if not page_var:
831
+ return
832
+ py.line(f'{page_var}.context.set_geolocation({{"latitude": {node.latitude}, "longitude": {node.longitude}, "accuracy": {node.accuracy}}})')
833
+ py.line(f'{page_var}.context.grant_permissions(["geolocation"])')