quantum-framework 0.18.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/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -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 +548 -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 +565 -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 +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -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_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -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 +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -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/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -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 +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -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 +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -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 +79 -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 +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -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/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 +164 -0
- quantum/core/xml_lines.py +81 -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 +25 -0
- quantum/runtime/action_handler.py +532 -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/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -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 +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -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 +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -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 +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -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/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -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/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Nodes for Testing Engine (qtest: namespace)
|
|
3
|
+
|
|
4
|
+
All testing-specific AST nodes for the Quantum Testing Engine.
|
|
5
|
+
These represent test suites, cases, assertions, browser interactions,
|
|
6
|
+
and advanced testing features that compile to pytest + Playwright Python code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from quantum.core.ast_nodes import QuantumNode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ============================================
|
|
17
|
+
# CORE TESTING NODES
|
|
18
|
+
# ============================================
|
|
19
|
+
|
|
20
|
+
class QTestSuiteNode(QuantumNode):
|
|
21
|
+
"""Represents <qtest:suite> - Top-level test suite container."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str):
|
|
24
|
+
self.name = name
|
|
25
|
+
self.browser: bool = False
|
|
26
|
+
self.parallel: bool = False
|
|
27
|
+
self.workers: int = 1
|
|
28
|
+
self.browsers: Optional[str] = None # comma-separated: "chromium,firefox"
|
|
29
|
+
self.device: Optional[str] = None
|
|
30
|
+
self.children: List[QuantumNode] = []
|
|
31
|
+
|
|
32
|
+
def add_child(self, child: QuantumNode):
|
|
33
|
+
self.children.append(child)
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
36
|
+
return {
|
|
37
|
+
"type": "test_suite",
|
|
38
|
+
"name": self.name,
|
|
39
|
+
"browser": self.browser,
|
|
40
|
+
"parallel": self.parallel,
|
|
41
|
+
"workers": self.workers,
|
|
42
|
+
"children_count": len(self.children),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def validate(self) -> List[str]:
|
|
46
|
+
errors = []
|
|
47
|
+
if not self.name:
|
|
48
|
+
errors.append("Test suite name is required")
|
|
49
|
+
for child in self.children:
|
|
50
|
+
if hasattr(child, 'validate'):
|
|
51
|
+
errors.extend(child.validate())
|
|
52
|
+
return errors
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class QTestCaseNode(QuantumNode):
|
|
56
|
+
"""Represents <qtest:case> - Individual test case."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, name: str):
|
|
59
|
+
self.name = name
|
|
60
|
+
self.browser: bool = False
|
|
61
|
+
self.auth: Optional[str] = None
|
|
62
|
+
self.device: Optional[str] = None
|
|
63
|
+
self.viewports: Optional[str] = None # comma-separated
|
|
64
|
+
self.children: List[QuantumNode] = []
|
|
65
|
+
|
|
66
|
+
def add_child(self, child: QuantumNode):
|
|
67
|
+
self.children.append(child)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"type": "test_case",
|
|
72
|
+
"name": self.name,
|
|
73
|
+
"browser": self.browser,
|
|
74
|
+
"auth": self.auth,
|
|
75
|
+
"children_count": len(self.children),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def validate(self) -> List[str]:
|
|
79
|
+
errors = []
|
|
80
|
+
if not self.name:
|
|
81
|
+
errors.append("Test case name is required")
|
|
82
|
+
for child in self.children:
|
|
83
|
+
if hasattr(child, 'validate'):
|
|
84
|
+
errors.extend(child.validate())
|
|
85
|
+
return errors
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ExpectNode(QuantumNode):
|
|
89
|
+
"""Represents <qtest:expect> - Assertion."""
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.selector: Optional[str] = None
|
|
93
|
+
self.visible: Optional[bool] = None
|
|
94
|
+
self.hidden: Optional[bool] = None
|
|
95
|
+
self.text: Optional[str] = None
|
|
96
|
+
self.text_contains: Optional[str] = None
|
|
97
|
+
self.equals: Optional[str] = None
|
|
98
|
+
self.count: Optional[int] = None
|
|
99
|
+
self.url: Optional[str] = None
|
|
100
|
+
self.url_contains: Optional[str] = None
|
|
101
|
+
self.var: Optional[str] = None
|
|
102
|
+
self.value: Optional[str] = None
|
|
103
|
+
self.enabled: Optional[bool] = None
|
|
104
|
+
self.disabled: Optional[bool] = None
|
|
105
|
+
self.checked: Optional[bool] = None
|
|
106
|
+
self.has_class: Optional[str] = None
|
|
107
|
+
self.has_attribute: Optional[str] = None
|
|
108
|
+
self.not_: bool = False # negate assertion
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
111
|
+
return {
|
|
112
|
+
"type": "test_expect",
|
|
113
|
+
"selector": self.selector,
|
|
114
|
+
"visible": self.visible,
|
|
115
|
+
"text": self.text,
|
|
116
|
+
"equals": self.equals,
|
|
117
|
+
"var": self.var,
|
|
118
|
+
"not": self.not_,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
def validate(self) -> List[str]:
|
|
122
|
+
errors = []
|
|
123
|
+
has_assertion = any([
|
|
124
|
+
self.visible is not None, self.hidden is not None,
|
|
125
|
+
self.text is not None, self.text_contains is not None,
|
|
126
|
+
self.equals is not None, self.count is not None,
|
|
127
|
+
self.url is not None, self.url_contains is not None,
|
|
128
|
+
self.value is not None, self.enabled is not None,
|
|
129
|
+
self.disabled is not None, self.checked is not None,
|
|
130
|
+
self.has_class is not None, self.has_attribute is not None,
|
|
131
|
+
])
|
|
132
|
+
if not has_assertion and not self.selector:
|
|
133
|
+
errors.append("Expect requires at least one assertion attribute or a selector")
|
|
134
|
+
if self.visible is True and self.hidden is True:
|
|
135
|
+
errors.append("Expect has contradictory assertions: visible=True and hidden=True")
|
|
136
|
+
return errors
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class MockNode_Testing(QuantumNode):
|
|
140
|
+
"""Represents <qtest:mock> - Mock services/queries."""
|
|
141
|
+
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self.query: Optional[str] = None
|
|
144
|
+
self.service: Optional[str] = None
|
|
145
|
+
self.auto_generate: bool = False
|
|
146
|
+
self.when: Optional[str] = None
|
|
147
|
+
self.return_value: Optional[str] = None # inline text content
|
|
148
|
+
self.children: List[QuantumNode] = []
|
|
149
|
+
|
|
150
|
+
def add_child(self, child: QuantumNode):
|
|
151
|
+
self.children.append(child)
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
154
|
+
return {
|
|
155
|
+
"type": "test_mock",
|
|
156
|
+
"query": self.query,
|
|
157
|
+
"service": self.service,
|
|
158
|
+
"auto_generate": self.auto_generate,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def validate(self) -> List[str]:
|
|
162
|
+
errors = []
|
|
163
|
+
if not self.query and not self.service:
|
|
164
|
+
errors.append("Mock requires 'query' or 'service' attribute")
|
|
165
|
+
return errors
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class FixtureNode_Testing(QuantumNode):
|
|
169
|
+
"""Represents <qtest:fixture> - Test data fixture."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, name: str):
|
|
172
|
+
self.name = name
|
|
173
|
+
self.file: Optional[str] = None
|
|
174
|
+
self.scope: str = "function" # function, class, module, session
|
|
175
|
+
self.data: Optional[str] = None # inline JSON content
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
178
|
+
return {
|
|
179
|
+
"type": "test_fixture",
|
|
180
|
+
"name": self.name,
|
|
181
|
+
"file": self.file,
|
|
182
|
+
"scope": self.scope,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def validate(self) -> List[str]:
|
|
186
|
+
errors = []
|
|
187
|
+
if not self.name:
|
|
188
|
+
errors.append("Fixture name is required")
|
|
189
|
+
if self.scope not in ('function', 'class', 'module', 'session'):
|
|
190
|
+
errors.append(f"Invalid fixture scope: {self.scope}")
|
|
191
|
+
return errors
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class SetupNode_Testing(QuantumNode):
|
|
195
|
+
"""Represents <qtest:setup> - Before-all hook."""
|
|
196
|
+
|
|
197
|
+
def __init__(self):
|
|
198
|
+
self.children: List[QuantumNode] = []
|
|
199
|
+
|
|
200
|
+
def add_child(self, child: QuantumNode):
|
|
201
|
+
self.children.append(child)
|
|
202
|
+
|
|
203
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
204
|
+
return {
|
|
205
|
+
"type": "test_setup",
|
|
206
|
+
"children_count": len(self.children),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
def validate(self) -> List[str]:
|
|
210
|
+
errors = []
|
|
211
|
+
for child in self.children:
|
|
212
|
+
if hasattr(child, 'validate'):
|
|
213
|
+
errors.extend(child.validate())
|
|
214
|
+
return errors
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TeardownNode_Testing(QuantumNode):
|
|
218
|
+
"""Represents <qtest:teardown> - After-all hook."""
|
|
219
|
+
|
|
220
|
+
def __init__(self):
|
|
221
|
+
self.children: List[QuantumNode] = []
|
|
222
|
+
|
|
223
|
+
def add_child(self, child: QuantumNode):
|
|
224
|
+
self.children.append(child)
|
|
225
|
+
|
|
226
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
227
|
+
return {
|
|
228
|
+
"type": "test_teardown",
|
|
229
|
+
"children_count": len(self.children),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def validate(self) -> List[str]:
|
|
233
|
+
errors = []
|
|
234
|
+
for child in self.children:
|
|
235
|
+
if hasattr(child, 'validate'):
|
|
236
|
+
errors.extend(child.validate())
|
|
237
|
+
return errors
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class BeforeEachNode(QuantumNode):
|
|
241
|
+
"""Represents <qtest:before-each> - Before-each test hook."""
|
|
242
|
+
|
|
243
|
+
def __init__(self):
|
|
244
|
+
self.children: List[QuantumNode] = []
|
|
245
|
+
|
|
246
|
+
def add_child(self, child: QuantumNode):
|
|
247
|
+
self.children.append(child)
|
|
248
|
+
|
|
249
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
250
|
+
return {
|
|
251
|
+
"type": "test_before_each",
|
|
252
|
+
"children_count": len(self.children),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def validate(self) -> List[str]:
|
|
256
|
+
errors = []
|
|
257
|
+
for child in self.children:
|
|
258
|
+
if hasattr(child, 'validate'):
|
|
259
|
+
errors.extend(child.validate())
|
|
260
|
+
return errors
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class AfterEachNode(QuantumNode):
|
|
264
|
+
"""Represents <qtest:after-each> - After-each test hook."""
|
|
265
|
+
|
|
266
|
+
def __init__(self):
|
|
267
|
+
self.children: List[QuantumNode] = []
|
|
268
|
+
|
|
269
|
+
def add_child(self, child: QuantumNode):
|
|
270
|
+
self.children.append(child)
|
|
271
|
+
|
|
272
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
273
|
+
return {
|
|
274
|
+
"type": "test_after_each",
|
|
275
|
+
"children_count": len(self.children),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
def validate(self) -> List[str]:
|
|
279
|
+
errors = []
|
|
280
|
+
for child in self.children:
|
|
281
|
+
if hasattr(child, 'validate'):
|
|
282
|
+
errors.extend(child.validate())
|
|
283
|
+
return errors
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class GenerateNode(QuantumNode):
|
|
287
|
+
"""Represents <qtest:generate> - Auto-generate tests."""
|
|
288
|
+
|
|
289
|
+
def __init__(self):
|
|
290
|
+
self.component: Optional[str] = None
|
|
291
|
+
self.coverage: Optional[str] = None # "full", "basic"
|
|
292
|
+
self.include: Optional[str] = None # comma-separated features
|
|
293
|
+
|
|
294
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
295
|
+
return {
|
|
296
|
+
"type": "test_generate",
|
|
297
|
+
"component": self.component,
|
|
298
|
+
"coverage": self.coverage,
|
|
299
|
+
"include": self.include,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def validate(self) -> List[str]:
|
|
303
|
+
errors = []
|
|
304
|
+
if not self.component:
|
|
305
|
+
errors.append("Generate requires 'component' attribute")
|
|
306
|
+
return errors
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class ScenarioNode(QuantumNode):
|
|
310
|
+
"""Represents <qtest:scenario> - BDD scenario."""
|
|
311
|
+
|
|
312
|
+
def __init__(self, name: str):
|
|
313
|
+
self.name = name
|
|
314
|
+
self.children: List[QuantumNode] = []
|
|
315
|
+
|
|
316
|
+
def add_child(self, child: QuantumNode):
|
|
317
|
+
self.children.append(child)
|
|
318
|
+
|
|
319
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
320
|
+
return {
|
|
321
|
+
"type": "test_scenario",
|
|
322
|
+
"name": self.name,
|
|
323
|
+
"children_count": len(self.children),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def validate(self) -> List[str]:
|
|
327
|
+
errors = []
|
|
328
|
+
if not self.name:
|
|
329
|
+
errors.append("Scenario name is required")
|
|
330
|
+
for child in self.children:
|
|
331
|
+
if hasattr(child, 'validate'):
|
|
332
|
+
errors.extend(child.validate())
|
|
333
|
+
return errors
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class GivenNode(QuantumNode):
|
|
337
|
+
"""Represents <qtest:given> - BDD given step."""
|
|
338
|
+
|
|
339
|
+
def __init__(self, text: str):
|
|
340
|
+
self.text = text
|
|
341
|
+
self.children: List[QuantumNode] = []
|
|
342
|
+
|
|
343
|
+
def add_child(self, child: QuantumNode):
|
|
344
|
+
self.children.append(child)
|
|
345
|
+
|
|
346
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
347
|
+
return {
|
|
348
|
+
"type": "test_given",
|
|
349
|
+
"text": self.text,
|
|
350
|
+
"children_count": len(self.children),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
def validate(self) -> List[str]:
|
|
354
|
+
errors = []
|
|
355
|
+
if not self.text:
|
|
356
|
+
errors.append("Given step text is required")
|
|
357
|
+
return errors
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class WhenNode(QuantumNode):
|
|
361
|
+
"""Represents <qtest:when> - BDD when step."""
|
|
362
|
+
|
|
363
|
+
def __init__(self, text: str):
|
|
364
|
+
self.text = text
|
|
365
|
+
self.children: List[QuantumNode] = []
|
|
366
|
+
|
|
367
|
+
def add_child(self, child: QuantumNode):
|
|
368
|
+
self.children.append(child)
|
|
369
|
+
|
|
370
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
371
|
+
return {
|
|
372
|
+
"type": "test_when",
|
|
373
|
+
"text": self.text,
|
|
374
|
+
"children_count": len(self.children),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
def validate(self) -> List[str]:
|
|
378
|
+
errors = []
|
|
379
|
+
if not self.text:
|
|
380
|
+
errors.append("When step text is required")
|
|
381
|
+
return errors
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class ThenNode(QuantumNode):
|
|
385
|
+
"""Represents <qtest:then> - BDD then step."""
|
|
386
|
+
|
|
387
|
+
def __init__(self, text: str):
|
|
388
|
+
self.text = text
|
|
389
|
+
self.children: List[QuantumNode] = []
|
|
390
|
+
|
|
391
|
+
def add_child(self, child: QuantumNode):
|
|
392
|
+
self.children.append(child)
|
|
393
|
+
|
|
394
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
395
|
+
return {
|
|
396
|
+
"type": "test_then",
|
|
397
|
+
"text": self.text,
|
|
398
|
+
"children_count": len(self.children),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
def validate(self) -> List[str]:
|
|
402
|
+
errors = []
|
|
403
|
+
if not self.text:
|
|
404
|
+
errors.append("Then step text is required")
|
|
405
|
+
return errors
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ============================================
|
|
409
|
+
# BROWSER TESTING NODES
|
|
410
|
+
# ============================================
|
|
411
|
+
|
|
412
|
+
class BrowserConfigNode(QuantumNode):
|
|
413
|
+
"""Represents <qtest:browser-config> - Browser configuration."""
|
|
414
|
+
|
|
415
|
+
def __init__(self):
|
|
416
|
+
self.engine: str = "chromium" # chromium, firefox, webkit
|
|
417
|
+
self.headless: bool = True
|
|
418
|
+
self.viewport_width: int = 1280
|
|
419
|
+
self.viewport_height: int = 720
|
|
420
|
+
self.locale: Optional[str] = None
|
|
421
|
+
self.timezone: Optional[str] = None
|
|
422
|
+
self.base_url: Optional[str] = None
|
|
423
|
+
self.timeout: int = 30000
|
|
424
|
+
self.video: bool = False
|
|
425
|
+
self.trace: bool = False
|
|
426
|
+
self.screenshot: Optional[str] = None # "on", "off", "only-on-failure"
|
|
427
|
+
|
|
428
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
429
|
+
return {
|
|
430
|
+
"type": "test_browser_config",
|
|
431
|
+
"engine": self.engine,
|
|
432
|
+
"headless": self.headless,
|
|
433
|
+
"base_url": self.base_url,
|
|
434
|
+
"timeout": self.timeout,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
def validate(self) -> List[str]:
|
|
438
|
+
errors = []
|
|
439
|
+
if self.engine not in ('chromium', 'firefox', 'webkit'):
|
|
440
|
+
errors.append(f"Invalid browser engine: {self.engine}")
|
|
441
|
+
return errors
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class NavigateNode(QuantumNode):
|
|
445
|
+
"""Represents <qtest:navigate> - Browser navigation."""
|
|
446
|
+
|
|
447
|
+
def __init__(self):
|
|
448
|
+
self.to: Optional[str] = None
|
|
449
|
+
self.back: bool = False
|
|
450
|
+
self.forward: bool = False
|
|
451
|
+
self.wait: Optional[str] = None # "load", "domcontentloaded", "networkidle"
|
|
452
|
+
|
|
453
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
454
|
+
return {
|
|
455
|
+
"type": "test_navigate",
|
|
456
|
+
"to": self.to,
|
|
457
|
+
"back": self.back,
|
|
458
|
+
"forward": self.forward,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
def validate(self) -> List[str]:
|
|
462
|
+
errors = []
|
|
463
|
+
if not self.to and not self.back and not self.forward:
|
|
464
|
+
errors.append("Navigate requires 'to', 'back', or 'forward'")
|
|
465
|
+
return errors
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class ClickNode(QuantumNode):
|
|
469
|
+
"""Represents <qtest:click> - Click action."""
|
|
470
|
+
|
|
471
|
+
def __init__(self):
|
|
472
|
+
self.selector: Optional[str] = None
|
|
473
|
+
self.text: Optional[str] = None
|
|
474
|
+
self.role: Optional[str] = None
|
|
475
|
+
self.test_id: Optional[str] = None
|
|
476
|
+
self.xpath: Optional[str] = None
|
|
477
|
+
|
|
478
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
479
|
+
return {
|
|
480
|
+
"type": "test_click",
|
|
481
|
+
"selector": self.selector,
|
|
482
|
+
"text": self.text,
|
|
483
|
+
"role": self.role,
|
|
484
|
+
"test_id": self.test_id,
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
def validate(self) -> List[str]:
|
|
488
|
+
errors = []
|
|
489
|
+
if not any([self.selector, self.text, self.role, self.test_id, self.xpath]):
|
|
490
|
+
errors.append("Click requires a target (selector, text, role, test_id, or xpath)")
|
|
491
|
+
return errors
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class DblClickNode(QuantumNode):
|
|
495
|
+
"""Represents <qtest:dblclick> - Double click action."""
|
|
496
|
+
|
|
497
|
+
def __init__(self):
|
|
498
|
+
self.selector: Optional[str] = None
|
|
499
|
+
self.text: Optional[str] = None
|
|
500
|
+
self.role: Optional[str] = None
|
|
501
|
+
self.test_id: Optional[str] = None
|
|
502
|
+
|
|
503
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
504
|
+
return {
|
|
505
|
+
"type": "test_dblclick",
|
|
506
|
+
"selector": self.selector,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
def validate(self) -> List[str]:
|
|
510
|
+
errors = []
|
|
511
|
+
if not any([self.selector, self.text, self.role, self.test_id]):
|
|
512
|
+
errors.append("DblClick requires a target")
|
|
513
|
+
return errors
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class RightClickNode(QuantumNode):
|
|
517
|
+
"""Represents <qtest:right-click> - Right click action."""
|
|
518
|
+
|
|
519
|
+
def __init__(self):
|
|
520
|
+
self.selector: Optional[str] = None
|
|
521
|
+
self.text: Optional[str] = None
|
|
522
|
+
self.role: Optional[str] = None
|
|
523
|
+
self.test_id: Optional[str] = None
|
|
524
|
+
|
|
525
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
526
|
+
return {
|
|
527
|
+
"type": "test_right_click",
|
|
528
|
+
"selector": self.selector,
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
def validate(self) -> List[str]:
|
|
532
|
+
errors = []
|
|
533
|
+
if not any([self.selector, self.text, self.role, self.test_id]):
|
|
534
|
+
errors.append("RightClick requires a target")
|
|
535
|
+
return errors
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class HoverNode(QuantumNode):
|
|
539
|
+
"""Represents <qtest:hover> - Hover action."""
|
|
540
|
+
|
|
541
|
+
def __init__(self):
|
|
542
|
+
self.selector: Optional[str] = None
|
|
543
|
+
self.text: Optional[str] = None
|
|
544
|
+
self.role: Optional[str] = None
|
|
545
|
+
self.test_id: Optional[str] = None
|
|
546
|
+
|
|
547
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
548
|
+
return {
|
|
549
|
+
"type": "test_hover",
|
|
550
|
+
"selector": self.selector,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
def validate(self) -> List[str]:
|
|
554
|
+
errors = []
|
|
555
|
+
if not any([self.selector, self.text, self.role, self.test_id]):
|
|
556
|
+
errors.append("Hover requires a target")
|
|
557
|
+
return errors
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class FillNode(QuantumNode):
|
|
561
|
+
"""Represents <qtest:fill> - Form fill action."""
|
|
562
|
+
|
|
563
|
+
def __init__(self):
|
|
564
|
+
self.selector: Optional[str] = None
|
|
565
|
+
self.value: str = ""
|
|
566
|
+
self.clear: bool = True
|
|
567
|
+
|
|
568
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
569
|
+
return {
|
|
570
|
+
"type": "test_fill",
|
|
571
|
+
"selector": self.selector,
|
|
572
|
+
"value": self.value,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
def validate(self) -> List[str]:
|
|
576
|
+
errors = []
|
|
577
|
+
if not self.selector:
|
|
578
|
+
errors.append("Fill requires 'selector' attribute")
|
|
579
|
+
return errors
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class TypeNode_Testing(QuantumNode):
|
|
583
|
+
"""Represents <qtest:type> - Type text with delay."""
|
|
584
|
+
|
|
585
|
+
def __init__(self):
|
|
586
|
+
self.selector: Optional[str] = None
|
|
587
|
+
self.text: str = ""
|
|
588
|
+
self.delay: int = 0 # ms between keystrokes
|
|
589
|
+
|
|
590
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
591
|
+
return {
|
|
592
|
+
"type": "test_type",
|
|
593
|
+
"selector": self.selector,
|
|
594
|
+
"text": self.text,
|
|
595
|
+
"delay": self.delay,
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
def validate(self) -> List[str]:
|
|
599
|
+
errors = []
|
|
600
|
+
if not self.selector:
|
|
601
|
+
errors.append("Type requires 'selector' attribute")
|
|
602
|
+
return errors
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class SelectNode_Testing(QuantumNode):
|
|
606
|
+
"""Represents <qtest:select> - Dropdown select."""
|
|
607
|
+
|
|
608
|
+
def __init__(self):
|
|
609
|
+
self.selector: Optional[str] = None
|
|
610
|
+
self.value: Optional[str] = None
|
|
611
|
+
self.values: Optional[str] = None # comma-separated for multi-select
|
|
612
|
+
|
|
613
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
614
|
+
return {
|
|
615
|
+
"type": "test_select",
|
|
616
|
+
"selector": self.selector,
|
|
617
|
+
"value": self.value,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
def validate(self) -> List[str]:
|
|
621
|
+
errors = []
|
|
622
|
+
if not self.selector:
|
|
623
|
+
errors.append("Select requires 'selector' attribute")
|
|
624
|
+
if not self.value and not self.values:
|
|
625
|
+
errors.append("Select requires 'value' or 'values' attribute")
|
|
626
|
+
return errors
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class CheckNode(QuantumNode):
|
|
630
|
+
"""Represents <qtest:check> - Check checkbox."""
|
|
631
|
+
|
|
632
|
+
def __init__(self):
|
|
633
|
+
self.selector: Optional[str] = None
|
|
634
|
+
|
|
635
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
636
|
+
return {
|
|
637
|
+
"type": "test_check",
|
|
638
|
+
"selector": self.selector,
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
def validate(self) -> List[str]:
|
|
642
|
+
errors = []
|
|
643
|
+
if not self.selector:
|
|
644
|
+
errors.append("Check requires 'selector' attribute")
|
|
645
|
+
return errors
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class UncheckNode(QuantumNode):
|
|
649
|
+
"""Represents <qtest:uncheck> - Uncheck checkbox."""
|
|
650
|
+
|
|
651
|
+
def __init__(self):
|
|
652
|
+
self.selector: Optional[str] = None
|
|
653
|
+
|
|
654
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
655
|
+
return {
|
|
656
|
+
"type": "test_uncheck",
|
|
657
|
+
"selector": self.selector,
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
def validate(self) -> List[str]:
|
|
661
|
+
errors = []
|
|
662
|
+
if not self.selector:
|
|
663
|
+
errors.append("Uncheck requires 'selector' attribute")
|
|
664
|
+
return errors
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class KeyboardNode(QuantumNode):
|
|
668
|
+
"""Represents <qtest:keyboard> - Keyboard actions."""
|
|
669
|
+
|
|
670
|
+
def __init__(self):
|
|
671
|
+
self.press: Optional[str] = None
|
|
672
|
+
self.type_text: Optional[str] = None
|
|
673
|
+
self.delay: int = 0
|
|
674
|
+
|
|
675
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
676
|
+
return {
|
|
677
|
+
"type": "test_keyboard",
|
|
678
|
+
"press": self.press,
|
|
679
|
+
"type_text": self.type_text,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
def validate(self) -> List[str]:
|
|
683
|
+
errors = []
|
|
684
|
+
if not self.press and not self.type_text:
|
|
685
|
+
errors.append("Keyboard requires 'press' or 'type' attribute")
|
|
686
|
+
return errors
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class DragNode(QuantumNode):
|
|
690
|
+
"""Represents <qtest:drag> - Drag & drop action."""
|
|
691
|
+
|
|
692
|
+
def __init__(self):
|
|
693
|
+
self.from_sel: Optional[str] = None
|
|
694
|
+
self.to_sel: Optional[str] = None
|
|
695
|
+
|
|
696
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
697
|
+
return {
|
|
698
|
+
"type": "test_drag",
|
|
699
|
+
"from": self.from_sel,
|
|
700
|
+
"to": self.to_sel,
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
def validate(self) -> List[str]:
|
|
704
|
+
errors = []
|
|
705
|
+
if not self.from_sel:
|
|
706
|
+
errors.append("Drag requires 'from' attribute")
|
|
707
|
+
if not self.to_sel:
|
|
708
|
+
errors.append("Drag requires 'to' attribute")
|
|
709
|
+
return errors
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class ScrollNode_Testing(QuantumNode):
|
|
713
|
+
"""Represents <qtest:scroll> - Scroll action."""
|
|
714
|
+
|
|
715
|
+
def __init__(self):
|
|
716
|
+
self.selector: Optional[str] = None
|
|
717
|
+
self.direction: str = "down" # up, down, left, right
|
|
718
|
+
self.amount: Optional[int] = None
|
|
719
|
+
self.to_element: Optional[str] = None
|
|
720
|
+
self.to_top: bool = False
|
|
721
|
+
|
|
722
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
723
|
+
return {
|
|
724
|
+
"type": "test_scroll",
|
|
725
|
+
"selector": self.selector,
|
|
726
|
+
"direction": self.direction,
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
def validate(self) -> List[str]:
|
|
730
|
+
return []
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class UploadNode(QuantumNode):
|
|
734
|
+
"""Represents <qtest:upload> - File upload test."""
|
|
735
|
+
|
|
736
|
+
def __init__(self):
|
|
737
|
+
self.selector: Optional[str] = None
|
|
738
|
+
self.file: Optional[str] = None
|
|
739
|
+
self.files: Optional[str] = None # comma-separated
|
|
740
|
+
|
|
741
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
742
|
+
return {
|
|
743
|
+
"type": "test_upload",
|
|
744
|
+
"selector": self.selector,
|
|
745
|
+
"file": self.file,
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
def validate(self) -> List[str]:
|
|
749
|
+
errors = []
|
|
750
|
+
if not self.selector:
|
|
751
|
+
errors.append("Upload requires 'selector' attribute")
|
|
752
|
+
if not self.file and not self.files:
|
|
753
|
+
errors.append("Upload requires 'file' or 'files' attribute")
|
|
754
|
+
return errors
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class DownloadNode(QuantumNode):
|
|
758
|
+
"""Represents <qtest:download> - Download test."""
|
|
759
|
+
|
|
760
|
+
def __init__(self):
|
|
761
|
+
self.trigger_click: Optional[str] = None
|
|
762
|
+
self.save_as: Optional[str] = None
|
|
763
|
+
|
|
764
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
765
|
+
return {
|
|
766
|
+
"type": "test_download",
|
|
767
|
+
"trigger_click": self.trigger_click,
|
|
768
|
+
"save_as": self.save_as,
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
def validate(self) -> List[str]:
|
|
772
|
+
errors = []
|
|
773
|
+
if not self.trigger_click:
|
|
774
|
+
errors.append("Download requires 'trigger_click' attribute")
|
|
775
|
+
return errors
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class WaitForNode(QuantumNode):
|
|
779
|
+
"""Represents <qtest:wait-for> - Wait strategy."""
|
|
780
|
+
|
|
781
|
+
def __init__(self):
|
|
782
|
+
self.selector: Optional[str] = None
|
|
783
|
+
self.state: Optional[str] = None # visible, hidden, attached, detached
|
|
784
|
+
self.url: Optional[str] = None
|
|
785
|
+
self.request: Optional[str] = None
|
|
786
|
+
self.response: Optional[str] = None
|
|
787
|
+
self.condition: Optional[str] = None
|
|
788
|
+
self.timeout: Optional[int] = None
|
|
789
|
+
self.has_text: Optional[str] = None
|
|
790
|
+
|
|
791
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
792
|
+
return {
|
|
793
|
+
"type": "test_wait_for",
|
|
794
|
+
"selector": self.selector,
|
|
795
|
+
"state": self.state,
|
|
796
|
+
"url": self.url,
|
|
797
|
+
"timeout": self.timeout,
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
def validate(self) -> List[str]:
|
|
801
|
+
errors = []
|
|
802
|
+
if not any([self.selector, self.url, self.request, self.response, self.condition]):
|
|
803
|
+
errors.append("WaitFor requires a wait target")
|
|
804
|
+
return errors
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
class ScreenshotNode(QuantumNode):
|
|
808
|
+
"""Represents <qtest:screenshot> - Take screenshot."""
|
|
809
|
+
|
|
810
|
+
def __init__(self):
|
|
811
|
+
self.name: Optional[str] = None
|
|
812
|
+
self.selector: Optional[str] = None
|
|
813
|
+
self.full_page: bool = False
|
|
814
|
+
|
|
815
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
816
|
+
return {
|
|
817
|
+
"type": "test_screenshot",
|
|
818
|
+
"name": self.name,
|
|
819
|
+
"selector": self.selector,
|
|
820
|
+
"full_page": self.full_page,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
def validate(self) -> List[str]:
|
|
824
|
+
return []
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
class InterceptNode(QuantumNode):
|
|
828
|
+
"""Represents <qtest:intercept> - Network interception."""
|
|
829
|
+
|
|
830
|
+
def __init__(self):
|
|
831
|
+
self.method: Optional[str] = None
|
|
832
|
+
self.url: Optional[str] = None
|
|
833
|
+
self.delay: Optional[int] = None
|
|
834
|
+
self.passthrough: bool = False
|
|
835
|
+
self.spy: bool = False
|
|
836
|
+
self.children: List[QuantumNode] = []
|
|
837
|
+
|
|
838
|
+
def add_child(self, child: QuantumNode):
|
|
839
|
+
self.children.append(child)
|
|
840
|
+
|
|
841
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
842
|
+
return {
|
|
843
|
+
"type": "test_intercept",
|
|
844
|
+
"method": self.method,
|
|
845
|
+
"url": self.url,
|
|
846
|
+
"spy": self.spy,
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
def validate(self) -> List[str]:
|
|
850
|
+
errors = []
|
|
851
|
+
if not self.url:
|
|
852
|
+
errors.append("Intercept requires 'url' attribute")
|
|
853
|
+
for child in self.children:
|
|
854
|
+
if hasattr(child, 'validate'):
|
|
855
|
+
errors.extend(child.validate())
|
|
856
|
+
return errors
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
class InterceptRespondNode(QuantumNode):
|
|
860
|
+
"""Represents <qtest:respond> - Mock response for intercepted request."""
|
|
861
|
+
|
|
862
|
+
def __init__(self):
|
|
863
|
+
self.status: int = 200
|
|
864
|
+
self.content_type: str = "application/json"
|
|
865
|
+
self.file: Optional[str] = None
|
|
866
|
+
self.body: Optional[str] = None # inline response body
|
|
867
|
+
|
|
868
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
869
|
+
return {
|
|
870
|
+
"type": "test_intercept_respond",
|
|
871
|
+
"status": self.status,
|
|
872
|
+
"content_type": self.content_type,
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
def validate(self) -> List[str]:
|
|
876
|
+
return []
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
class FrameNode_Testing(QuantumNode):
|
|
880
|
+
"""Represents <qtest:frame> - iFrame context."""
|
|
881
|
+
|
|
882
|
+
def __init__(self):
|
|
883
|
+
self.selector: Optional[str] = None
|
|
884
|
+
self.children: List[QuantumNode] = []
|
|
885
|
+
|
|
886
|
+
def add_child(self, child: QuantumNode):
|
|
887
|
+
self.children.append(child)
|
|
888
|
+
|
|
889
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
890
|
+
return {
|
|
891
|
+
"type": "test_frame",
|
|
892
|
+
"selector": self.selector,
|
|
893
|
+
"children_count": len(self.children),
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
def validate(self) -> List[str]:
|
|
897
|
+
errors = []
|
|
898
|
+
if not self.selector:
|
|
899
|
+
errors.append("Frame requires 'selector' attribute")
|
|
900
|
+
for child in self.children:
|
|
901
|
+
if hasattr(child, 'validate'):
|
|
902
|
+
errors.extend(child.validate())
|
|
903
|
+
return errors
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
class PopupNode(QuantumNode):
|
|
907
|
+
"""Represents <qtest:popup> - Popup/new window context."""
|
|
908
|
+
|
|
909
|
+
def __init__(self):
|
|
910
|
+
self.children: List[QuantumNode] = []
|
|
911
|
+
|
|
912
|
+
def add_child(self, child: QuantumNode):
|
|
913
|
+
self.children.append(child)
|
|
914
|
+
|
|
915
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
916
|
+
return {
|
|
917
|
+
"type": "test_popup",
|
|
918
|
+
"children_count": len(self.children),
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
def validate(self) -> List[str]:
|
|
922
|
+
errors = []
|
|
923
|
+
for child in self.children:
|
|
924
|
+
if hasattr(child, 'validate'):
|
|
925
|
+
errors.extend(child.validate())
|
|
926
|
+
return errors
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
class AuthNode(QuantumNode):
|
|
930
|
+
"""Represents <qtest:auth> - Authentication state."""
|
|
931
|
+
|
|
932
|
+
def __init__(self):
|
|
933
|
+
self.name: Optional[str] = None
|
|
934
|
+
self.reuse: bool = True
|
|
935
|
+
self.children: List[QuantumNode] = []
|
|
936
|
+
|
|
937
|
+
def add_child(self, child: QuantumNode):
|
|
938
|
+
self.children.append(child)
|
|
939
|
+
|
|
940
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
941
|
+
return {
|
|
942
|
+
"type": "test_auth",
|
|
943
|
+
"name": self.name,
|
|
944
|
+
"reuse": self.reuse,
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
def validate(self) -> List[str]:
|
|
948
|
+
errors = []
|
|
949
|
+
if not self.name:
|
|
950
|
+
errors.append("Auth requires 'name' attribute")
|
|
951
|
+
return errors
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
class SaveStateNode(QuantumNode):
|
|
955
|
+
"""Represents <qtest:save-state> - Save browser state."""
|
|
956
|
+
|
|
957
|
+
def __init__(self):
|
|
958
|
+
self.file: Optional[str] = None
|
|
959
|
+
|
|
960
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
961
|
+
return {
|
|
962
|
+
"type": "test_save_state",
|
|
963
|
+
"file": self.file,
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
def validate(self) -> List[str]:
|
|
967
|
+
errors = []
|
|
968
|
+
if not self.file:
|
|
969
|
+
errors.append("SaveState requires 'file' attribute")
|
|
970
|
+
return errors
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
class PauseNode(QuantumNode):
|
|
974
|
+
"""Represents <qtest:pause> - Debug pause."""
|
|
975
|
+
|
|
976
|
+
def __init__(self):
|
|
977
|
+
pass
|
|
978
|
+
|
|
979
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
980
|
+
return {"type": "test_pause"}
|
|
981
|
+
|
|
982
|
+
def validate(self) -> List[str]:
|
|
983
|
+
return []
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
# ============================================
|
|
987
|
+
# ADVANCED TESTING NODES
|
|
988
|
+
# ============================================
|
|
989
|
+
|
|
990
|
+
class SnapshotNode(QuantumNode):
|
|
991
|
+
"""Represents <qtest:snapshot> - Visual regression testing."""
|
|
992
|
+
|
|
993
|
+
def __init__(self):
|
|
994
|
+
self.name: Optional[str] = None
|
|
995
|
+
self.threshold: float = 0.1
|
|
996
|
+
self.ai_ignore: Optional[str] = None # comma-separated selectors to ignore
|
|
997
|
+
self.viewports: Optional[str] = None # comma-separated viewport sizes
|
|
998
|
+
self.mask_selectors: Optional[str] = None # comma-separated selectors to mask
|
|
999
|
+
|
|
1000
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1001
|
+
return {
|
|
1002
|
+
"type": "test_snapshot",
|
|
1003
|
+
"name": self.name,
|
|
1004
|
+
"threshold": self.threshold,
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
def validate(self) -> List[str]:
|
|
1008
|
+
return []
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
class FuzzNode(QuantumNode):
|
|
1012
|
+
"""Represents <qtest:fuzz> - Fuzz testing."""
|
|
1013
|
+
|
|
1014
|
+
def __init__(self):
|
|
1015
|
+
self.target: Optional[str] = None
|
|
1016
|
+
self.iterations: int = 100
|
|
1017
|
+
self.generators: Optional[str] = None
|
|
1018
|
+
self.children: List[QuantumNode] = []
|
|
1019
|
+
|
|
1020
|
+
def add_child(self, child: QuantumNode):
|
|
1021
|
+
self.children.append(child)
|
|
1022
|
+
|
|
1023
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1024
|
+
return {
|
|
1025
|
+
"type": "test_fuzz",
|
|
1026
|
+
"target": self.target,
|
|
1027
|
+
"iterations": self.iterations,
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
def validate(self) -> List[str]:
|
|
1031
|
+
errors = []
|
|
1032
|
+
if not self.target:
|
|
1033
|
+
errors.append("Fuzz requires 'target' attribute")
|
|
1034
|
+
for child in self.children:
|
|
1035
|
+
if hasattr(child, 'validate'):
|
|
1036
|
+
errors.extend(child.validate())
|
|
1037
|
+
return errors
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
class PropertyNode(QuantumNode):
|
|
1041
|
+
"""Represents <qtest:property> - Fuzz property assertion."""
|
|
1042
|
+
|
|
1043
|
+
def __init__(self, text: str):
|
|
1044
|
+
self.text = text
|
|
1045
|
+
|
|
1046
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1047
|
+
return {
|
|
1048
|
+
"type": "test_property",
|
|
1049
|
+
"text": self.text,
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
def validate(self) -> List[str]:
|
|
1053
|
+
errors = []
|
|
1054
|
+
if not self.text:
|
|
1055
|
+
errors.append("Property text is required")
|
|
1056
|
+
return errors
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
class FuzzFieldNode(QuantumNode):
|
|
1060
|
+
"""Represents <qtest:field> - Field fuzz configuration."""
|
|
1061
|
+
|
|
1062
|
+
def __init__(self):
|
|
1063
|
+
self.name: Optional[str] = None
|
|
1064
|
+
self.generator: Optional[str] = None # "string", "int", "email", "xss", "sqli"
|
|
1065
|
+
|
|
1066
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1067
|
+
return {
|
|
1068
|
+
"type": "test_fuzz_field",
|
|
1069
|
+
"name": self.name,
|
|
1070
|
+
"generator": self.generator,
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
def validate(self) -> List[str]:
|
|
1074
|
+
errors = []
|
|
1075
|
+
if not self.name:
|
|
1076
|
+
errors.append("Fuzz field 'name' is required")
|
|
1077
|
+
return errors
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
class PerfNode(QuantumNode):
|
|
1081
|
+
"""Represents <qtest:perf> - Performance testing."""
|
|
1082
|
+
|
|
1083
|
+
def __init__(self):
|
|
1084
|
+
self.baseline: Optional[str] = None
|
|
1085
|
+
self.regression_threshold: float = 0.1
|
|
1086
|
+
self.children: List[QuantumNode] = []
|
|
1087
|
+
|
|
1088
|
+
def add_child(self, child: QuantumNode):
|
|
1089
|
+
self.children.append(child)
|
|
1090
|
+
|
|
1091
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1092
|
+
return {
|
|
1093
|
+
"type": "test_perf",
|
|
1094
|
+
"baseline": self.baseline,
|
|
1095
|
+
"regression_threshold": self.regression_threshold,
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
def validate(self) -> List[str]:
|
|
1099
|
+
errors = []
|
|
1100
|
+
for child in self.children:
|
|
1101
|
+
if hasattr(child, 'validate'):
|
|
1102
|
+
errors.extend(child.validate())
|
|
1103
|
+
return errors
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
class PerfMetricNode(QuantumNode):
|
|
1107
|
+
"""Represents <qtest:metric> - Performance metric."""
|
|
1108
|
+
|
|
1109
|
+
def __init__(self):
|
|
1110
|
+
self.name: Optional[str] = None
|
|
1111
|
+
self.max_value: Optional[float] = None
|
|
1112
|
+
|
|
1113
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1114
|
+
return {
|
|
1115
|
+
"type": "test_perf_metric",
|
|
1116
|
+
"name": self.name,
|
|
1117
|
+
"max_value": self.max_value,
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
def validate(self) -> List[str]:
|
|
1121
|
+
errors = []
|
|
1122
|
+
if not self.name:
|
|
1123
|
+
errors.append("Metric name is required")
|
|
1124
|
+
return errors
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
class A11yNode(QuantumNode):
|
|
1128
|
+
"""Represents <qtest:a11y> - Accessibility testing."""
|
|
1129
|
+
|
|
1130
|
+
def __init__(self):
|
|
1131
|
+
self.standard: str = "wcag2aa" # wcag2a, wcag2aa, wcag2aaa
|
|
1132
|
+
self.check: Optional[str] = None # comma-separated specific checks
|
|
1133
|
+
self.ignore: Optional[str] = None # comma-separated rules to ignore
|
|
1134
|
+
|
|
1135
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1136
|
+
return {
|
|
1137
|
+
"type": "test_a11y",
|
|
1138
|
+
"standard": self.standard,
|
|
1139
|
+
"check": self.check,
|
|
1140
|
+
"ignore": self.ignore,
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
def validate(self) -> List[str]:
|
|
1144
|
+
errors = []
|
|
1145
|
+
if self.standard not in ('wcag2a', 'wcag2aa', 'wcag2aaa'):
|
|
1146
|
+
errors.append(f"Invalid a11y standard: {self.standard}")
|
|
1147
|
+
return errors
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class ChaosNode(QuantumNode):
|
|
1151
|
+
"""Represents <qtest:chaos> - Chaos testing container."""
|
|
1152
|
+
|
|
1153
|
+
def __init__(self):
|
|
1154
|
+
self.children: List[QuantumNode] = []
|
|
1155
|
+
|
|
1156
|
+
def add_child(self, child: QuantumNode):
|
|
1157
|
+
self.children.append(child)
|
|
1158
|
+
|
|
1159
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1160
|
+
return {
|
|
1161
|
+
"type": "test_chaos",
|
|
1162
|
+
"children_count": len(self.children),
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
def validate(self) -> List[str]:
|
|
1166
|
+
errors = []
|
|
1167
|
+
for child in self.children:
|
|
1168
|
+
if hasattr(child, 'validate'):
|
|
1169
|
+
errors.extend(child.validate())
|
|
1170
|
+
return errors
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
class ChaosFailNode(QuantumNode):
|
|
1174
|
+
"""Represents <qtest:fail> - Simulate service failure."""
|
|
1175
|
+
|
|
1176
|
+
def __init__(self):
|
|
1177
|
+
self.service: Optional[str] = None
|
|
1178
|
+
self.probability: float = 1.0
|
|
1179
|
+
self.delay: Optional[int] = None # ms
|
|
1180
|
+
|
|
1181
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1182
|
+
return {
|
|
1183
|
+
"type": "test_chaos_fail",
|
|
1184
|
+
"service": self.service,
|
|
1185
|
+
"probability": self.probability,
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
def validate(self) -> List[str]:
|
|
1189
|
+
errors = []
|
|
1190
|
+
if not self.service:
|
|
1191
|
+
errors.append("ChaosField requires 'service' attribute")
|
|
1192
|
+
return errors
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
class GeolocationNode(QuantumNode):
|
|
1196
|
+
"""Represents <qtest:geolocation> - Mock geolocation."""
|
|
1197
|
+
|
|
1198
|
+
def __init__(self):
|
|
1199
|
+
self.latitude: float = 0.0
|
|
1200
|
+
self.longitude: float = 0.0
|
|
1201
|
+
self.accuracy: float = 100.0
|
|
1202
|
+
|
|
1203
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1204
|
+
return {
|
|
1205
|
+
"type": "test_geolocation",
|
|
1206
|
+
"latitude": self.latitude,
|
|
1207
|
+
"longitude": self.longitude,
|
|
1208
|
+
"accuracy": self.accuracy,
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
def validate(self) -> List[str]:
|
|
1212
|
+
return []
|