selenium测试
This commit is contained in:
parent
bcbf100356
commit
c6ea1854e5
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ dsl_schema.json
|
|||||||
sessions/*
|
sessions/*
|
||||||
artifacts/*
|
artifacts/*
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
tests/__pycache__/*.pyc
|
||||||
|
|||||||
@ -3,3 +3,11 @@
|
|||||||
Can not load UIAutomationCore.dll.
|
Can not load UIAutomationCore.dll.
|
||||||
1, You may need to install Windows Update KB971513 if your OS is Windows XP, see https://github.com/yinkaisheng/WindowsUpdateKB971513ForIUIAutomation
|
1, You may need to install Windows Update KB971513 if your OS is Windows XP, see https://github.com/yinkaisheng/WindowsUpdateKB971513ForIUIAutomation
|
||||||
2, You need to use an UIAutomationInitializerInThread object if use uiautomation in a thread, see demos/uiautomation_in_thread.py
|
2, You need to use an UIAutomationInitializerInThread object if use uiautomation in a thread, see demos/uiautomation_in_thread.py
|
||||||
|
2025-12-30 22:39:52.738 pydevd_safe_repr.py[343] _repr_obj -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:40:02.757 pydevd_safe_repr.py[343] _repr_obj -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:40:12.791 pydevd_safe_repr.py[343] _repr_obj -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:40:25.854 pydevd_safe_repr.py[343] _repr_obj -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:40:35.867 pydevd_safe_repr.py[343] _repr_obj -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:40:45.888 pydevd_safe_repr.py[343] _repr_obj -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:40:55.909 pydevd_resolver.py[193] _get_py_dictionary -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:41:05.933 pydevd_resolver.py[193] _get_py_dictionary ->
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# MIT License
|
# MIT License
|
||||||
# Copyright (c) 2024
|
# Copyright (c) 2024
|
||||||
"""执行层:基于 DSL 进行 UI 自动化,并支持可选视觉校验与结构化日志"""
|
"""执行层:基于 DSL 进行 UI 自动化,并支持可选视觉校验与结构化日志。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ from .schema import DSLSpec
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExecContext:
|
class ExecContext:
|
||||||
"""执行上下文"""
|
"""执行上下文。"""
|
||||||
|
|
||||||
allow_title: str
|
allow_title: str
|
||||||
dry_run: bool = False
|
dry_run: bool = False
|
||||||
@ -29,7 +29,7 @@ class ExecContext:
|
|||||||
|
|
||||||
|
|
||||||
def _match_window(allow_title: str) -> Optional[auto.Control]:
|
def _match_window(allow_title: str) -> Optional[auto.Control]:
|
||||||
"""仅在窗口标题匹配白名单时返回窗口,容忍标题前缀/包含"""
|
"""仅在窗口标题匹配白名单时返回窗口,容忍标题前缀或包含。"""
|
||||||
patterns = [allow_title]
|
patterns = [allow_title]
|
||||||
if " - " in allow_title:
|
if " - " in allow_title:
|
||||||
patterns.append(allow_title.split(" - ", 1)[0])
|
patterns.append(allow_title.split(" - ", 1)[0])
|
||||||
@ -43,7 +43,7 @@ def _match_window(allow_title: str) -> Optional[auto.Control]:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _ascend_to_top(node: auto.Control) -> auto.Control:
|
def _ascend_to_top(node: auto.Control) -> auto.Control:
|
||||||
"""向上寻找最可能的顶层窗口(Chrome 主窗口类名/WindowControl 优先)"""
|
"""向上寻找最可能的顶层窗口(优先返回类似 Chrome 的 WindowControl)。"""
|
||||||
best = node
|
best = node
|
||||||
cur = node
|
cur = node
|
||||||
while True:
|
while True:
|
||||||
@ -88,7 +88,7 @@ def _match_window(allow_title: str) -> Optional[auto.Control]:
|
|||||||
|
|
||||||
|
|
||||||
def _find_control(root: auto.Control, locator: Dict[str, Any], timeout: float) -> Optional[auto.Control]:
|
def _find_control(root: auto.Control, locator: Dict[str, Any], timeout: float) -> Optional[auto.Control]:
|
||||||
"""Find a control under root according to locator."""
|
"""根据 locator 在 root 下查找控件。"""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
try:
|
try:
|
||||||
print(
|
print(
|
||||||
@ -98,7 +98,7 @@ def _find_control(root: auto.Control, locator: Dict[str, Any], timeout: float) -
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _matches(ctrl: auto.Control) -> bool:
|
def _matches(ctrl: auto.Control) -> bool:
|
||||||
"""Simple property match without relying on uiautomation AndCondition."""
|
"""简单属性匹配,避免依赖 uiautomation 的 AndCondition。"""
|
||||||
try:
|
try:
|
||||||
name_val = locator.get("Name")
|
name_val = locator.get("Name")
|
||||||
name_contains = locator.get("Name__contains")
|
name_contains = locator.get("Name__contains")
|
||||||
@ -130,11 +130,11 @@ def _find_control(root: auto.Control, locator: Dict[str, Any], timeout: float) -
|
|||||||
if not locator:
|
if not locator:
|
||||||
print("10001")
|
print("10001")
|
||||||
return root
|
return root
|
||||||
# Check root itself first
|
# 先检查根节点自身
|
||||||
if _matches(root):
|
if _matches(root):
|
||||||
print("10002")
|
print("10002")
|
||||||
return root
|
return root
|
||||||
# Simple BFS when AndCondition is unavailable
|
# AndCondition 不可用时,使用简单 BFS
|
||||||
queue: List[Any] = [(root, 0)]
|
queue: List[Any] = [(root, 0)]
|
||||||
while queue:
|
while queue:
|
||||||
node, depth = queue.pop(0)
|
node, depth = queue.pop(0)
|
||||||
@ -184,7 +184,7 @@ def _find_control(root: auto.Control, locator: Dict[str, Any], timeout: float) -
|
|||||||
|
|
||||||
|
|
||||||
def _capture_screenshot(ctrl: Optional[auto.Control], out_path: Path) -> Optional[Path]:
|
def _capture_screenshot(ctrl: Optional[auto.Control], out_path: Path) -> Optional[Path]:
|
||||||
"""截取控件区域或全屏"""
|
"""截取控件区域或全屏。"""
|
||||||
try:
|
try:
|
||||||
with mss.mss() as sct:
|
with mss.mss() as sct:
|
||||||
if ctrl and getattr(ctrl, "BoundingRectangle", None):
|
if ctrl and getattr(ctrl, "BoundingRectangle", None):
|
||||||
@ -203,7 +203,7 @@ def _capture_screenshot(ctrl: Optional[auto.Control], out_path: Path) -> Optiona
|
|||||||
|
|
||||||
|
|
||||||
def _capture_tree(ctrl: Optional[auto.Control], max_depth: int = 3) -> List[Dict[str, Any]]:
|
def _capture_tree(ctrl: Optional[auto.Control], max_depth: int = 3) -> List[Dict[str, Any]]:
|
||||||
"""采集浅层 UIA 树摘要"""
|
"""采集浅层 UIA 树摘要。"""
|
||||||
if ctrl is None:
|
if ctrl is None:
|
||||||
return []
|
return []
|
||||||
nodes: List[Dict[str, Any]] = []
|
nodes: List[Dict[str, Any]] = []
|
||||||
@ -241,7 +241,7 @@ def _save_tree(ctrl: Optional[auto.Control], out_path: Path) -> Optional[Path]:
|
|||||||
|
|
||||||
|
|
||||||
def _image_similarity(full_img_path: Path, template_path: Path, threshold: float = 0.8) -> bool:
|
def _image_similarity(full_img_path: Path, template_path: Path, threshold: float = 0.8) -> bool:
|
||||||
"""简单模板匹配,相似度 >= 阈值视为通过"""
|
"""简单模板匹配,相似度 >= 阈值视为通过。"""
|
||||||
if not full_img_path.exists() or not template_path.exists():
|
if not full_img_path.exists() or not template_path.exists():
|
||||||
return False
|
return False
|
||||||
full = cv2.imread(str(full_img_path), cv2.IMREAD_COLOR)
|
full = cv2.imread(str(full_img_path), cv2.IMREAD_COLOR)
|
||||||
@ -254,7 +254,7 @@ def _image_similarity(full_img_path: Path, template_path: Path, threshold: float
|
|||||||
|
|
||||||
|
|
||||||
def _visual_check(expected: Dict[str, Any], ctrl: Optional[auto.Control], artifacts_dir: Path, step_idx: int, attempt: int) -> bool:
|
def _visual_check(expected: Dict[str, Any], ctrl: Optional[auto.Control], artifacts_dir: Path, step_idx: int, attempt: int) -> bool:
|
||||||
"""执行可选视觉校验:模板匹配"""
|
"""执行可选视觉校验:模板匹配。"""
|
||||||
template_path = expected.get("template_path")
|
template_path = expected.get("template_path")
|
||||||
threshold = float(expected.get("threshold", 0.8))
|
threshold = float(expected.get("threshold", 0.8))
|
||||||
if not template_path:
|
if not template_path:
|
||||||
@ -274,7 +274,7 @@ def _log_event(log_path: Path, record: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _render_value(val: Any, params: Dict[str, Any]) -> Any:
|
def _render_value(val: Any, params: Dict[str, Any]) -> Any:
|
||||||
"""简单占位符替换 ${param}"""
|
"""简单占位符替换 ${param}。"""
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
out = val
|
out = val
|
||||||
for k, v in params.items():
|
for k, v in params.items():
|
||||||
@ -290,7 +290,7 @@ def _render_value(val: Any, params: Dict[str, Any]) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
def _do_action(ctrl: auto.Control, step: Dict[str, Any], dry_run: bool) -> None:
|
def _do_action(ctrl: auto.Control, step: Dict[str, Any], dry_run: bool) -> None:
|
||||||
"""执行单步动作"""
|
"""执行单步动作。"""
|
||||||
action = step.get("action")
|
action = step.get("action")
|
||||||
text = step.get("text", "")
|
text = step.get("text", "")
|
||||||
send_enter = bool(step.get("send_enter"))
|
send_enter = bool(step.get("send_enter"))
|
||||||
@ -318,11 +318,13 @@ def _do_action(ctrl: auto.Control, step: Dict[str, Any], dry_run: bool) -> None:
|
|||||||
|
|
||||||
def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None:
|
def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None:
|
||||||
"""执行完整 DSL。
|
"""执行完整 DSL。
|
||||||
|
|
||||||
流程概览:
|
流程概览:
|
||||||
1. 先根据 allow_title 找到当前前台窗口作为根控件 root。
|
1. 先根据 allow_title 找到当前前台窗口作为根控件 root。
|
||||||
2. 逐步标准化 DSL:字段兼容、文本替换、等待策略等。
|
2. 逐步标准化 DSL:字段兼容、文本替换、等待策略等。
|
||||||
3. 对每个步骤依次查找目标控件 -> 视觉校验(可选)-> 执行动作/记录 dry-run。
|
3. 对每个步骤依次查找目标控件 -> 视觉校验(可选) -> 执行动作/记录 dry-run。
|
||||||
4. 每次尝试都会落盘截图、UI 树和日志,方便回溯。"""
|
4. 每次尝试都会落盘截图、UI 树和日志,方便回溯。
|
||||||
|
"""
|
||||||
# 给前台窗口切换预留时间,避免刚启动命令时窗口还未聚焦
|
# 给前台窗口切换预留时间,避免刚启动命令时窗口还未聚焦
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
root = _match_window(ctx.allow_title)
|
root = _match_window(ctx.allow_title)
|
||||||
@ -340,7 +342,7 @@ def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None:
|
|||||||
log_path = artifacts / "executor_log.jsonl"
|
log_path = artifacts / "executor_log.jsonl"
|
||||||
|
|
||||||
def _normalize_target(tgt: Dict[str, Any]) -> Dict[str, Any]:
|
def _normalize_target(tgt: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""规范 target 键名,兼容窗口标题匹配/包含等写法"""
|
"""规范 target 键名,兼容窗口标题匹配/包含等写法。"""
|
||||||
norm: Dict[str, Any] = {}
|
norm: Dict[str, Any] = {}
|
||||||
for k, v in tgt.items():
|
for k, v in tgt.items():
|
||||||
lk = k.lower()
|
lk = k.lower()
|
||||||
@ -365,7 +367,7 @@ def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None:
|
|||||||
return norm
|
return norm
|
||||||
|
|
||||||
def normalize_step(step: Dict[str, Any]) -> Dict[str, Any]:
|
def normalize_step(step: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""归一化字段,兼容不同 DSL 变体"""
|
"""归一化字段,兼容不同 DSL 变量。"""
|
||||||
out = _render_value(dict(step), spec.params)
|
out = _render_value(dict(step), spec.params)
|
||||||
if "target" not in out and "selector" in out:
|
if "target" not in out and "selector" in out:
|
||||||
out["target"] = out.get("selector")
|
out["target"] = out.get("selector")
|
||||||
@ -425,7 +427,7 @@ def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None:
|
|||||||
for item in iterable:
|
for item in iterable:
|
||||||
run_steps(step.get("steps", []))
|
run_steps(step.get("steps", []))
|
||||||
elif "if_condition" in step:
|
elif "if_condition" in step:
|
||||||
# if_condition:依据参数布尔值选择分支
|
# if_condition:依赖参数布尔值选择分支
|
||||||
cond = step["if_condition"]
|
cond = step["if_condition"]
|
||||||
if spec.params.get(cond):
|
if spec.params.get(cond):
|
||||||
run_steps(step.get("steps", []))
|
run_steps(step.get("steps", []))
|
||||||
@ -436,7 +438,7 @@ def execute_spec(spec: DSLSpec, ctx: ExecContext) -> None:
|
|||||||
target = step.get("target", {})
|
target = step.get("target", {})
|
||||||
timeout = float(step.get("waits", {}).get("appear", spec.waits.get("appear", 1.0)))
|
timeout = float(step.get("waits", {}).get("appear", spec.waits.get("appear", 1.0)))
|
||||||
if ctx.dry_run:
|
if ctx.dry_run:
|
||||||
timeout = min(timeout, 1) # 纯 dry-run 场景快速返回,避免长时间等待
|
timeout = min(timeout, 1) # 让 dry-run 场景快速返回,避免长时间等待
|
||||||
retry = step.get("retry_policy", spec.retry_policy)
|
retry = step.get("retry_policy", spec.retry_policy)
|
||||||
attempts = int(retry.get("max_attempts", 1))
|
attempts = int(retry.get("max_attempts", 1))
|
||||||
interval = float(retry.get("interval", 1.0))
|
interval = float(retry.get("interval", 1.0))
|
||||||
|
|||||||
91
dsl.yaml
Normal file
91
dsl.yaml
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
params:
|
||||||
|
baidu_url: www.baidu.com
|
||||||
|
search_text: "你好"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- id: wait_new_tab_chrome
|
||||||
|
action: wait_for
|
||||||
|
timeout_ms: 10000
|
||||||
|
target:
|
||||||
|
window_title: "新标签页 - Google Chrome"
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: WindowControl
|
||||||
|
|
||||||
|
- id: focus_address_bar
|
||||||
|
action: click
|
||||||
|
waits:
|
||||||
|
- type: wait_for
|
||||||
|
selector:
|
||||||
|
name: 地址和搜索栏
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: EditControl
|
||||||
|
timeout_ms: 5000
|
||||||
|
target:
|
||||||
|
name: 地址和搜索栏
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: EditControl
|
||||||
|
|
||||||
|
- id: type_baidu_url
|
||||||
|
action: type
|
||||||
|
text_param: baidu_url
|
||||||
|
send_enter: true
|
||||||
|
waits:
|
||||||
|
- type: wait_for
|
||||||
|
selector:
|
||||||
|
window_title: "百度一下,你就知道 - Google Chrome"
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: WindowControl
|
||||||
|
timeout_ms: 15000
|
||||||
|
target:
|
||||||
|
name: 地址和搜索栏
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: EditControl
|
||||||
|
|
||||||
|
- id: click_baidu_search_box
|
||||||
|
action: click
|
||||||
|
waits:
|
||||||
|
- type: wait_for
|
||||||
|
selector:
|
||||||
|
control_type: EditControl
|
||||||
|
timeout_ms: 5000
|
||||||
|
target:
|
||||||
|
control_type: EditControl
|
||||||
|
|
||||||
|
- id: type_search_text
|
||||||
|
action: type
|
||||||
|
text_param: search_text
|
||||||
|
send_enter: true
|
||||||
|
waits:
|
||||||
|
- type: wait_for
|
||||||
|
selector:
|
||||||
|
window_title_contains_param: search_text
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: WindowControl
|
||||||
|
timeout_ms: 15000
|
||||||
|
target:
|
||||||
|
control_type: EditControl
|
||||||
|
|
||||||
|
assertions:
|
||||||
|
- id: assert_baidu_home_opened
|
||||||
|
action: assert_exists
|
||||||
|
selector:
|
||||||
|
window_title: "百度一下,你就知道 - Google Chrome"
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: WindowControl
|
||||||
|
timeout_ms: 5000
|
||||||
|
|
||||||
|
- id: assert_search_result_page
|
||||||
|
action: assert_exists
|
||||||
|
selector:
|
||||||
|
window_title_contains_param: search_text
|
||||||
|
class_name: Chrome_WidgetWin_1
|
||||||
|
control_type: WindowControl
|
||||||
|
timeout_ms: 10000
|
||||||
|
|
||||||
|
retry_policy:
|
||||||
|
max_attempts: 2
|
||||||
|
interval: 1.0
|
||||||
|
|
||||||
|
waits:
|
||||||
|
appear: 5.0
|
||||||
|
disappear: 5.0
|
||||||
@ -10,3 +10,4 @@ psutil>=5.9.6
|
|||||||
numpy>=1.26.0
|
numpy>=1.26.0
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
|
selenium
|
||||||
2
tests/@AutomationLog.txt
Normal file
2
tests/@AutomationLog.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
2025-12-30 22:33:41.606 test-uiautomation.py[71] run -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
|
2025-12-30 22:34:54.215 test-uiautomation.py[71] run -> Find Control Timeout(10s): {Name: '帮助', ControlType: MenuItemControl}
|
||||||
91
tests/engine.py
Normal file
91
tests/engine.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import yaml
|
||||||
|
import time
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
|
||||||
|
class DSLEngine:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
self.wait = WebDriverWait(self.driver, 10)
|
||||||
|
|
||||||
|
def _parse_locator(self, locator_str):
|
||||||
|
"""
|
||||||
|
解析定位符字符串,例如 "id:username" -> (By.ID, "username")
|
||||||
|
"""
|
||||||
|
if not locator_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
by_map = {
|
||||||
|
"id": By.ID,
|
||||||
|
"xpath": By.XPATH,
|
||||||
|
"css": By.CSS_SELECTOR,
|
||||||
|
"name": By.NAME,
|
||||||
|
"class": By.CLASS_NAME,
|
||||||
|
"tag": By.TAG_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
method, value = locator_str.split(":", 1)
|
||||||
|
return by_map.get(method.lower()), value
|
||||||
|
except ValueError:
|
||||||
|
raise Exception(f"定位符格式错误: {locator_str},应为 '类型:值'")
|
||||||
|
|
||||||
|
def execute_step(self, step):
|
||||||
|
"""
|
||||||
|
执行单个 DSL 步骤
|
||||||
|
"""
|
||||||
|
action = step.get('action')
|
||||||
|
locator_str = step.get('locator')
|
||||||
|
value = step.get('value')
|
||||||
|
desc = step.get('desc', '无描述')
|
||||||
|
|
||||||
|
print(f"正在执行: [{action}] - {desc}")
|
||||||
|
|
||||||
|
# 解析定位符 (如果有)
|
||||||
|
locator = self._parse_locator(locator_str)
|
||||||
|
|
||||||
|
# === 动作分发 (Action Dispatch) ===
|
||||||
|
|
||||||
|
if action == "open":
|
||||||
|
self.driver.get(value)
|
||||||
|
|
||||||
|
elif action == "input":
|
||||||
|
el = self.wait.until(EC.visibility_of_element_located(locator))
|
||||||
|
el.clear()
|
||||||
|
el.send_keys(str(value))
|
||||||
|
|
||||||
|
elif action == "click":
|
||||||
|
el = self.wait.until(EC.element_to_be_clickable(locator))
|
||||||
|
el.click()
|
||||||
|
|
||||||
|
elif action == "wait":
|
||||||
|
time.sleep(float(value))
|
||||||
|
|
||||||
|
elif action == "assert_text":
|
||||||
|
el = self.wait.until(EC.visibility_of_element_located(locator))
|
||||||
|
actual_text = el.text
|
||||||
|
assert value in actual_text, f"断言失败: 期望 '{value}' 包含在 '{actual_text}' 中"
|
||||||
|
print(f" -> 断言通过: 发现 '{actual_text}'")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception(f"未知的动作指令: {action}")
|
||||||
|
|
||||||
|
def run_yaml(self, yaml_path):
|
||||||
|
"""
|
||||||
|
加载并运行 YAML 文件
|
||||||
|
"""
|
||||||
|
with open(yaml_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
print(f"=== 开始测试: {data.get('name')} ===")
|
||||||
|
|
||||||
|
for step in data.get('steps', []):
|
||||||
|
try:
|
||||||
|
self.execute_step(step)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [X] 步骤执行失败: {e}")
|
||||||
|
raise e # 抛出异常以终止测试
|
||||||
|
|
||||||
|
print("=== 测试全部通过 ===")
|
||||||
31
tests/main.py
Normal file
31
tests/main.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from engine import DSLEngine
|
||||||
|
import os
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. 设置 WebDriver (这里以 Chrome 为例)
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
# options.add_argument('--headless') # 如果需要无头模式可开启
|
||||||
|
driver = webdriver.Chrome(options=options)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2. 初始化 DSL 引擎
|
||||||
|
engine = DSLEngine(driver)
|
||||||
|
|
||||||
|
# 3. 指定 YAML 文件路径并运行
|
||||||
|
yaml_file = "test_case.yaml"
|
||||||
|
|
||||||
|
if os.path.exists(yaml_file):
|
||||||
|
engine.run_yaml(yaml_file)
|
||||||
|
else:
|
||||||
|
print(f"找不到文件: {yaml_file}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("测试过程中发生严重错误。")
|
||||||
|
finally:
|
||||||
|
# 4. 清理环境
|
||||||
|
# time.sleep(3) # 调试时可以暂停一下看结果
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
93
tests/test-uiautomation.py
Normal file
93
tests/test-uiautomation.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import uiautomation as auto
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
class AutomationEngine:
|
||||||
|
def __init__(self, config_path):
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
self.config = yaml.safe_load(f)
|
||||||
|
self.window = None
|
||||||
|
|
||||||
|
def find_control(self, parent, selector_dict):
|
||||||
|
"""根据字典动态查找控件"""
|
||||||
|
if not selector_dict:
|
||||||
|
return parent
|
||||||
|
|
||||||
|
args = selector_dict.copy()
|
||||||
|
|
||||||
|
# 1. 提取 ControlType,例如 "MenuItemControl"
|
||||||
|
# 如果 YAML 里没写,默认用 generic 的 Control
|
||||||
|
control_type = args.pop('ControlType', 'Control')
|
||||||
|
|
||||||
|
# 2. 动态获取查找方法,例如 parent.MenuItemControl(...)
|
||||||
|
# 这样比直接用 parent.Control(ControlType=...) 更符合库的设计
|
||||||
|
if hasattr(parent, control_type):
|
||||||
|
finder_method = getattr(parent, control_type)
|
||||||
|
else:
|
||||||
|
print(f"警告: 未知的 ControlType '{control_type}',回退到通用查找。")
|
||||||
|
finder_method = parent.Control
|
||||||
|
# 如果回退,需要把 ControlType 加回去作为属性过滤
|
||||||
|
if control_type != 'Control':
|
||||||
|
args['ControlType'] = control_type
|
||||||
|
|
||||||
|
# 3. 执行查找
|
||||||
|
return finder_method(**args)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
print(f"开始执行任务: {self.config['name']}")
|
||||||
|
|
||||||
|
subprocess.Popen(self.config['app'])
|
||||||
|
time.sleep(1) # 等待启动
|
||||||
|
|
||||||
|
# 查找主窗口
|
||||||
|
self.window = auto.WindowControl(**self.config['target_window'])
|
||||||
|
if not self.window.Exists(5):
|
||||||
|
raise Exception("❌ 主窗口未找到,请检查 ClassName 是否正确 (Win11记事本可能是 'Notepad' 但内部结构不同)")
|
||||||
|
|
||||||
|
self.window.SetTopmost(True)
|
||||||
|
print(f"✅ 锁定主窗口: {self.window.Name}")
|
||||||
|
|
||||||
|
for i, step in enumerate(self.config['steps']):
|
||||||
|
action = step.get('action')
|
||||||
|
desc = step.get('desc', action)
|
||||||
|
print(f"\n--- 步骤 {i+1}: {desc} ---")
|
||||||
|
|
||||||
|
# 确定父级
|
||||||
|
target_control = self.window
|
||||||
|
if 'parent' in step:
|
||||||
|
# 弹窗通常是顶层窗口,从 Root 找,searchDepth=1 表示只查桌面的一级子窗口
|
||||||
|
target_control = self.find_control(auto.GetRootControl(), step['parent'])
|
||||||
|
|
||||||
|
# 确定目标控件
|
||||||
|
if 'selector' in step:
|
||||||
|
target_control = self.find_control(target_control, step['selector'])
|
||||||
|
|
||||||
|
# !!! 关键调试信息 !!!
|
||||||
|
# 检查控件是否存在,如果不存在,打印详细信息并停止
|
||||||
|
if not target_control.Exists(3):
|
||||||
|
print(f"❌ 错误: 无法找到控件!")
|
||||||
|
print(f" 查找参数: {step.get('selector')}")
|
||||||
|
print(f" 父级控件: {target_control.GetParentControl()}")
|
||||||
|
# 可以在这里抛出异常停止脚本,方便调试
|
||||||
|
break
|
||||||
|
|
||||||
|
# 执行动作
|
||||||
|
if action == 'input':
|
||||||
|
target_control.Click()
|
||||||
|
target_control.SendKeys(step['value'])
|
||||||
|
print(f" 输入: {step['value']}")
|
||||||
|
|
||||||
|
elif action == 'click':
|
||||||
|
target_control.Click()
|
||||||
|
print(" 点击成功")
|
||||||
|
|
||||||
|
elif action == 'sleep':
|
||||||
|
time.sleep(step['value'])
|
||||||
|
print(f" 等待 {step['value']} 秒")
|
||||||
|
|
||||||
|
print("\n自动化结束")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
engine = AutomationEngine(r'D:\project\audoWin\tests\test_case.yaml')
|
||||||
|
engine.run()
|
||||||
30
tests/test_case.yaml
Normal file
30
tests/test_case.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# test_case.yaml
|
||||||
|
name: 标准用户登录测试
|
||||||
|
description: 测试SauceDemo网站的登录流程
|
||||||
|
steps:
|
||||||
|
- action: open
|
||||||
|
value: "https://www.saucedemo.com/"
|
||||||
|
desc: "打开首页"
|
||||||
|
|
||||||
|
- action: input
|
||||||
|
locator: "id:user-name"
|
||||||
|
value: "standard_user"
|
||||||
|
desc: "输入用户名"
|
||||||
|
|
||||||
|
- action: input
|
||||||
|
locator: "id:password"
|
||||||
|
value: "secret_sauce"
|
||||||
|
desc: "输入密码"
|
||||||
|
|
||||||
|
- action: click
|
||||||
|
locator: "id:login-button"
|
||||||
|
desc: "点击登录按钮"
|
||||||
|
|
||||||
|
- action: wait
|
||||||
|
value: 1
|
||||||
|
desc: "强制等待1秒(可选)"
|
||||||
|
|
||||||
|
- action: assert_text
|
||||||
|
locator: "class:title"
|
||||||
|
value: "Products"
|
||||||
|
desc: "验证页面标题包含Products"
|
||||||
Loading…
x
Reference in New Issue
Block a user