feat: 优化定位转换器调整框架结构

- 新增 finder.py 重构定位转换器
- 优化 .gitignore 文件
- 其他优化
This commit is contained in:
2026-01-25 17:24:04 +08:00
parent 92d06dd9cf
commit 5a50eb8289
15 changed files with 342 additions and 126 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
.idea
# Virtual environments
.venv/
data/
logs/
report
screenshot/
temp/
xlsx/
uv.lock

View File

@@ -11,7 +11,7 @@
"""
from time import sleep
from selenium.webdriver import Chrome
from selenium.webdriver import Chrome,Edge
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from commons.modules import Browser
@@ -28,28 +28,28 @@ class LoginPage(KeyWordDriver):
password = '//*[@id="pass"]'
login_submit = '//*[@id="root"]/div[1]/div/div/form/div[3]/button'
# def __init__(self, browser: Browser):
# super().__init__(browser)
def __init__(self):
super().__init__()
def __init__(self,driver: WebDriver | None = None):
super().__init__(driver)
def login(self, email, password):
self.browser(Browser.EDGE)
# self.base_url("http://119.91.19.171:40065")
self.get(self.url)
self.input(1, self.email, email)
self.input(By.XPATH, self.password, password)
text = self.get_text(By.XPATH, self.email_title)
print(text)
self.click(By.XPATH, self.login_submit)
sleep(10)
# sleep(10)
# input()
if __name__ == '__main__':
from commons.settings import configs
# _driver =Edge()
_email = configs.username
_password = configs.password
login = LoginPage()
# login.browser(Browser.EDGE)
login.base_url(configs.base_url)
login.login(_email, _password)

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: main
@date: 2025/4/4 17:52
@desc:
"""
from time import sleep
import pytest
from selenium.webdriver import Chrome
from login_page import LoginPage
from commons.modules import Browser
@pytest.mark.parametrize("email, password", [("username", "password"), ("<EMAIL>", "<PASSWORD>")])
def test_login(driver, email, password):
login = LoginPage()
login.login(email, password)
# sleep(10)
def test_logout_1(login_ok):
login = LoginPage()
login.browser(Browser.CHROME)
login.get("https://www.baidu.com/")
print("logout")
# @pytest.mark.usefixtures("login_ok")
def test_logout_2():
login = LoginPage()
login.browser(Browser.CHROME)
login.get("https://www.baidu.com/")
print("logout")

View File

@@ -10,12 +10,12 @@
@desc:
"""
import pytest
from selenium.webdriver import Chrome
from selenium.webdriver import Chrome,Edge
@pytest.fixture()
def driver():
_driver = Chrome()
_driver = Edge()
yield _driver
_driver.quit()

40
POM/tests/test_login.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: main
@date: 2025/4/4 17:52
@desc:
"""
from time import sleep
import pytest
from selenium.webdriver import Chrome
from POM.page.login_page import LoginPage
from commons.modules import Browser
@pytest.mark.parametrize("email, password", [("ltcs@ltcs.com", "ltcs2024")])
def test_login(driver, email, password):
login = LoginPage(driver)
login.login(email, password)
# sleep(10)
def test_logout_1(driver):
login = LoginPage(driver)
# login.browser(Browser.CHROME)
login.get("/questions/10010000000000002")
print("logout")
sleep(10)
#
#
# # @pytest.mark.usefixtures("login_ok")
# def test_logout_2():
# login = LoginPage()
# login.browser(Browser.CHROME)
# login.get("https://www.baidu.com/")
# print("logout")

View File

@@ -20,7 +20,7 @@ from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.expected_conditions import visibility_of_element_located
__all__ = ["EC","custom_ec"]
logger = logging.getLogger(__name__)
@@ -97,3 +97,16 @@ def func_4(mark):
if __name__ == "__main__":
print(custom_ec)
from selenium.common.exceptions import StaleElementReferenceException
def luo_ji(locator: Tuple[str, str]):
_ = locator
def _predicate(driver):
try:
_ = driver
raise StaleElementReferenceException()
except StaleElementReferenceException:
return False
return _predicate

View File

@@ -11,6 +11,7 @@
"""
import logging
from pathlib import Path
from time import sleep
import secrets # 原生库,用于生成安全的随机数
from typing import Optional, Callable, Union, Literal, Any, TypeVar
@@ -28,6 +29,8 @@ from commons.custom_expected_condition import EC, custom_ec
from commons.modules import Browser, Locator
from commons.settings import configs, EXPLICIT_WAIT_TIMEOUT, SCREENSHOT_DIR
from commons.webdriver_finder import BrowserFinder
from utils.finder import by_converter
logger = logging.getLogger(__name__)
WebDriverOrWebElement = Union[WebDriver, WebElement]
@@ -41,51 +44,35 @@ T = TypeVar("T")
# 筛选器 Filters
# 查找器 finder
# 转换器 converter
def webdriver_finder(browser: str | Browser = Browser.CHROME) -> WebDriver:
match browser:
case Browser.CHROME:
return Chrome()
case Browser.FIREFOX:
return Firefox()
case Browser.IE:
return Ie()
case Browser.SAFARI:
return Safari()
case Browser.EDGE:
return Edge()
case _:
return Chrome() # 默认情况
def by_converter(by_value: str | Locator):
try:
# 统一处理输入
if isinstance(by_value, str):
by = Locator(by_value.lower().replace(' ', ''))
# 创建对应的浏览器实例
by = {
Locator.ID: By.ID,
Locator.NAME: By.NAME,
Locator.CLASS: By.CLASS_NAME,
Locator.TAG: By.TAG_NAME,
Locator.LINK_TEXT: By.LINK_TEXT,
Locator.PARTIAL_LINK_TEXT: By.PARTIAL_LINK_TEXT,
Locator.CSS: By.CSS_SELECTOR,
Locator.XPATH: By.XPATH,
}.get(by_value, By.XPATH)
return by
except ValueError:
return By.XPATH
# def by_converter(by_value: str | Locator):
# try:
# # 统一处理输入
# if isinstance(by_value, str):
# by = Locator(by_value.lower().replace(' ', ''))
#
# # 创建对应的浏览器实例
# by = {
# Locator.ID: By.ID,
# Locator.NAME: By.NAME,
# Locator.CLASS: By.CLASS_NAME,
# Locator.TAG: By.TAG_NAME,
# Locator.LINK_TEXT: By.LINK_TEXT,
# Locator.PARTIAL_LINK_TEXT: By.PARTIAL_LINK_TEXT,
# Locator.CSS: By.CSS_SELECTOR,
# Locator.XPATH: By.XPATH,
# }.get(by_value, By.XPATH)
# return by
# except ValueError:
# return By.XPATH
class KeyWordDriver:
class CoreDriver:
# def __init__(self, browser: str | Browser):
# self.driver = self.webdriver_finder(browser)
def __init__(self):
self.driver: WebDriver | None = None
self._url: str | None = None
def __init__(self, driver: WebDriver | None = None):
self.driver: WebDriver | None = driver
self._url: str | None = configs.base_url
# self.temp_value = None
def base_url(self, url: str, *args, **kwargs):
@@ -93,28 +80,33 @@ class KeyWordDriver:
self._url = url
# return url
def browser(self, browser_name: str | Browser = Browser.CHROME, *args, **kwargs):
def browser(self, browser_name: str | Browser = Browser.CHROME, browser_dir: Path | str | None = None,
browser_driver: Path | str | None = None, *args, **kwargs):
browser_name = Browser(browser_name.lower().replace(' ', '')) if isinstance(browser_name, str) else browser_name
match browser_name:
case Browser.CHROME:
logger.info(f"启动{Browser.CHROME}浏览器")
self.driver = Chrome()
# return Chrome()
case Browser.FIREFOX:
logger.info(f"启动{Browser.FIREFOX}浏览器")
self.driver = Firefox()
case Browser.IE:
logger.info(f"启动{Browser.IE}浏览器")
self.driver = Ie()
case Browser.SAFARI:
logger.info(f"启动{Browser.SAFARI}浏览器")
self.driver = Safari()
case Browser.EDGE:
logger.info(f"启动{Browser.EDGE}浏览器")
self.driver = Edge()
case _:
logger.info(f"启动默认浏览器: {Browser.CHROME}")
self.driver = Chrome() # 默认情况
logger.info(f"启动 {browser_name.value} 浏览器")
self.driver = BrowserFinder(browser_name=browser_name).get_browser_driver(browser_dir, browser_driver)
# match browser_name:
# case Browser.CHROME:
# logger.info(f"启动{Browser.CHROME}浏览器")
# self.driver = Chrome()
# # return Chrome()
# case Browser.FIREFOX:
# logger.info(f"启动{Browser.FIREFOX}浏览器")
# self.driver = Firefox()
# case Browser.IE:
# logger.info(f"启动{Browser.IE}浏览器")
# self.driver = Ie()
# case Browser.SAFARI:
# logger.info(f"启动{Browser.SAFARI}浏览器")
# self.driver = Safari()
# case Browser.EDGE:
# logger.info(f"启动{Browser.EDGE}浏览器")
# self.driver = Edge()
# case _:
# logger.info(f"启动默认浏览器: {Browser.CHROME}")
# self.driver = Chrome() # 默认情况
def find_element(self, by: str = By.XPATH, value: Optional[str] = None, *args, **kwargs) -> WebElement:
by = by_converter(by)
@@ -137,7 +129,7 @@ class KeyWordDriver:
def explicit_wait(self, method: T, timeout: float = EXPLICIT_WAIT_TIMEOUT, *args, **kwargs):
"""
显示等待
显示等待AttributeError: 'WebDriver' object has no attribute 'send_keys'
:param method: 可调用对象名
:param timeout: 超时时间
:param args:
@@ -159,7 +151,7 @@ class KeyWordDriver:
def page_load_timeout(self, timeout: float, *args, **kwargs) -> None:
self.driver.set_page_load_timeout(timeout)
def get(self, url, *args, **kwargs):
def get(self, url:str, *args, **kwargs):
if self.driver is None:
self.browser(*args, **kwargs)
if not url.startswith("http"):
@@ -326,7 +318,8 @@ class KeyWordDriver:
if __name__ == '__main__':
from commons.settings import configs
el = KeyWordDriver()
el = CoreDriver()
el.base_url(configs.base_url)
# el.browser("chrome")
el.browser(Browser.EDGE)
@@ -338,4 +331,3 @@ if __name__ == '__main__':
type_value = el.get_attribute("", '//*[@id="root"]/div[1]/div/div/form/div[3]/button', "type")
print(type_value)
el.assert_text_equals("", '//*[@id="root"]/div[1]/div/div/form/div[3]/button', "登录")

View File

@@ -19,6 +19,7 @@ class Browser(str, Enum):
IE = "ie"
SAFARI = "safari"
EDGE = "edge"
DEFAULT = "default"
class Locator(str, Enum):

View File

@@ -43,6 +43,11 @@ EXCHANGER = Path(ROOT_PATH, "extract.toml")
# 自增ID
ID_PATH = Path(ROOT_PATH, "id.toml")
browser_dir: Path | str | None = None
chrome_driver: Path | str | None = None
temp_user_data_dir: Path | str
# 默认配置
DEFAULT_CONF = {
"SCREENSHOT_DIR": SCREENSHOT_DIR,
@@ -103,7 +108,6 @@ class Settings:
new_conf = DEFAULT_CONF | result
for key, value in new_conf.items():
self.__setattr__(key, value)
return self
@@ -134,4 +138,3 @@ configs = Settings()
if __name__ == '__main__':
...
print(configs.items())

View File

@@ -9,17 +9,12 @@
@date: 2025/4/2 21:57
@desc:
"""
import json
import logging
from itertools import chain
import inspect
import allure
from pytest_xlsx.file import XlsxItem
import pytest
from selenium.webdriver import Chrome
from POM.login_page import LoginPage
from POM.page.login_page import LoginPage
from commons.driver import KeyWordDriver
from utils.file_processors.file_handle import FileHandle
from commons.templates import Template

26
excel_handle.py Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: excel_handle
@date: 2025/3/30 15:09
@desc:
"""
from pathlib import Path
class ExcelHandler:
def __init__(self, path: Path) -> None:
self.path = path
def load(self):
pass
def save(self):
pass

View File

@@ -4,8 +4,7 @@ version = "0.1.0"
description = "Web自动化测试框架"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
]
dependencies = []
[dependency-groups]
dev = [

30
settings.toml Normal file
View File

@@ -0,0 +1,30 @@
# settings.toml - 示例配置文件
# 如果此文件存在,这里的配置将覆盖代码中的默认值
base_url = "http://production.example.com:8080" # 覆盖默认的 base_url
screenshot = "data/screenshots" # 相对于项目根目录的路径
[database]
host = "192.168.1.100" # 覆盖数据库主机
port = 3307 # 覆盖数据库端口
user = "prod_user"
password = "prod_password_secret"
database = "production_db"
[allure]
epic = "项目 V2核心功能"
feature = "用户管理模块"
[rsa]
public = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyourPublicKeyHere...
-----END PUBLIC KEY-----"""
private = """-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,yourEncryptedPrivateKeyInfoHere...
-----END RSA PRIVATE KEY-----"""
# 注意cases_dir, exchanger, id_path, test_dir 也可以在这里覆盖
# cases_dir = "MyTestCases/Answer" # 如果是相对路径,是相对于 root_path
# test_dir = "/absolute/path/to/tests" # 也可以是绝对路径

Binary file not shown.

139
utils/finder.py Normal file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: finder
@date: 2026/1/25 16:56
@desc:
"""
from typing import Literal, Final
from selenium.webdriver.common.by import By
ByType = Literal[
# By(selenium)
"id", "xpath", "link text", "partial link text", "name", "tag name", "class name", "css selector",
# 自定义常用简写 (Shortcuts)
"lt","plt", "class", "css",
]
class FinderConverter:
"""
定位查找转换工具类
提供策略的归一化处理、简写映射及动态自定义注册
"""
# 预设的常用简写
_BUILTIN_SHORTCUTS: Final = {
"lt": By.LINK_TEXT,
"plt": By.PARTIAL_LINK_TEXT,
"class": By.CLASS_NAME,
"css": By.CSS_SELECTOR,
}
def __init__(self):
self._finder_map: dict[str, str] = {}
self._map_cache: dict[str, str] = {}
self._initialize()
@staticmethod
def _normalize(text: str) -> str:
"""
统一清洗逻辑:转小写、去除空格、下划线、横杠
"""
if not isinstance(text, str):
raise TypeError(f"Locator strategy must be a string, got {type(text).__name__} instead.")
return text.lower().strip().replace('_', '').replace(' ', '').replace('-', '')
def _initialize(self) -> None:
"""初始化基础映射表"""
# 1. 动态加载 AppiumBy 常量值
for attr_name in dir(By):
if attr_name.startswith("_"):
continue
attr_value = getattr(By, attr_name)
if isinstance(attr_value, str):
# "class name" -> classname,"class_name" -> classname
self._finder_map[self._normalize(attr_value)] = attr_value
# 2. 加载内置简写(会覆盖同名的策略)
self._finder_map.update(self._BUILTIN_SHORTCUTS)
# 3. 备份初始状态
self._map_cache = self._finder_map.copy()
def convert(self, by_value: ByType | str) -> str:
"""
将模糊或简写的定位方式转换为 Appium 标准定位字符串
:raises ValueError: 当定位方式不支持时抛出
"""
if not by_value or not isinstance(by_value, str):
raise ValueError(f"Invalid selector type: {type(by_value)}. Expected a string.")
clean_key = self._normalize(by_value)
target = self._finder_map.get(clean_key)
if target is None:
raise ValueError(f"Unsupported locator strategy: '{by_value}'.")
return target
def register_custom_finder(self, alias: str, target: str) -> None:
"""注册自定义定位策略"""
self._finder_map[self._normalize(alias)] = target
def clear_custom_finders(self) -> None:
"""重置回初始官方/内置状态"""
self._finder_map = self._map_cache.copy()
def get_all_finders(self) -> list[str]:
"""返回当前所有支持的策略 key用于调试"""
return list(self._finder_map.keys())
# 导出单例,方便直接使用
converter = FinderConverter()
by_converter = converter.convert
register_custom_finder = converter.register_custom_finder
__all__ = ["by_converter", "register_custom_finder"]
if __name__ == '__main__':
# 1. 测试标准转换与内置简写
print(f"ID 转换: {by_converter('id')}") # 输出: id
print(f"Class 简写转换: {by_converter('class')}") # 输出: accessibility id
print(f"CSS 简写转换: {by_converter('css')}") # 输出: css selector
# 2. 测试强大的归一化容错 (空格、下划线、横杠、大小写)
print(f"类链容错: {by_converter(' link_text ')}") # 输出: link text
print(f"PARTIAL_LINK_TEXT 容错: {by_converter('PARTIAL_LINK_TEXT')}") # 输出: partial link text
# 3. 测试自定义注册
register_custom_finder("my_text", "-my uiautomator")
print(f"自定义注册测试: {by_converter('my_text')}") # 输出: -my uiautomator
# 4. 测试重置功能
converter.clear_custom_finders()
print("已重置自定义查找器")
try:
by_converter("my_text")
except ValueError as e:
print(f"验证重置成功 (捕获异常): {e}")
# 5. 查看当前全量支持的归一化后的 Key
print(f"当前支持的策略总数: {len(converter.get_all_finders())}")
print(f"前 5 个策略示例: {converter.get_all_finders()[:5]}")
# 6. 增加类型非法测试
print("\n--- 异常类型测试 ---")
try:
by_converter(123) # 传入数字
except TypeError as e:
print(f"验证类型拦截成功: {e}")
try:
by_converter(None) # 传入 None
except TypeError as e:
print(f"验证空值拦截成功: {e}")