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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- 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"])')
|