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 time import sleep
from selenium.webdriver import Chrome from selenium.webdriver import Chrome,Edge
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from commons.modules import Browser from commons.modules import Browser
@@ -28,28 +28,28 @@ class LoginPage(KeyWordDriver):
password = '//*[@id="pass"]' password = '//*[@id="pass"]'
login_submit = '//*[@id="root"]/div[1]/div/div/form/div[3]/button' login_submit = '//*[@id="root"]/div[1]/div/div/form/div[3]/button'
# def __init__(self, browser: Browser): def __init__(self,driver: WebDriver | None = None):
# super().__init__(browser) super().__init__(driver)
def __init__(self):
super().__init__()
def login(self, email, password): def login(self, email, password):
self.browser(Browser.EDGE)
# self.base_url("http://119.91.19.171:40065")
self.get(self.url) self.get(self.url)
self.input(1, self.email, email) self.input(1, self.email, email)
self.input(By.XPATH, self.password, password) self.input(By.XPATH, self.password, password)
text = self.get_text(By.XPATH, self.email_title) text = self.get_text(By.XPATH, self.email_title)
print(text) print(text)
self.click(By.XPATH, self.login_submit) self.click(By.XPATH, self.login_submit)
sleep(10) # sleep(10)
# input()
if __name__ == '__main__': if __name__ == '__main__':
from commons.settings import configs from commons.settings import configs
# _driver =Edge()
_email = configs.username _email = configs.username
_password = configs.password _password = configs.password
login = LoginPage() login = LoginPage()
# login.browser(Browser.EDGE)
login.base_url(configs.base_url) login.base_url(configs.base_url)
login.login(_email, _password) 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: @desc:
""" """
import pytest import pytest
from selenium.webdriver import Chrome from selenium.webdriver import Chrome,Edge
@pytest.fixture() @pytest.fixture()
def driver(): def driver():
_driver = Chrome() _driver = Edge()
yield _driver yield _driver
_driver.quit() _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.remote.webdriver import WebElement
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.expected_conditions import visibility_of_element_located
__all__ = ["EC","custom_ec"] __all__ = ["EC","custom_ec"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -97,3 +97,16 @@ def func_4(mark):
if __name__ == "__main__": if __name__ == "__main__":
print(custom_ec) 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 import logging
from pathlib import Path
from time import sleep from time import sleep
import secrets # 原生库,用于生成安全的随机数 import secrets # 原生库,用于生成安全的随机数
from typing import Optional, Callable, Union, Literal, Any, TypeVar 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.modules import Browser, Locator
from commons.settings import configs, EXPLICIT_WAIT_TIMEOUT, SCREENSHOT_DIR 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__) logger = logging.getLogger(__name__)
WebDriverOrWebElement = Union[WebDriver, WebElement] WebDriverOrWebElement = Union[WebDriver, WebElement]
@@ -41,51 +44,35 @@ T = TypeVar("T")
# 筛选器 Filters # 筛选器 Filters
# 查找器 finder # 查找器 finder
# 转换器 converter # 转换器 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): # def by_converter(by_value: str | Locator):
try: # try:
# 统一处理输入 # # 统一处理输入
if isinstance(by_value, str): # if isinstance(by_value, str):
by = Locator(by_value.lower().replace(' ', '')) # by = Locator(by_value.lower().replace(' ', ''))
#
# 创建对应的浏览器实例 # # 创建对应的浏览器实例
by = { # by = {
Locator.ID: By.ID, # Locator.ID: By.ID,
Locator.NAME: By.NAME, # Locator.NAME: By.NAME,
Locator.CLASS: By.CLASS_NAME, # Locator.CLASS: By.CLASS_NAME,
Locator.TAG: By.TAG_NAME, # Locator.TAG: By.TAG_NAME,
Locator.LINK_TEXT: By.LINK_TEXT, # Locator.LINK_TEXT: By.LINK_TEXT,
Locator.PARTIAL_LINK_TEXT: By.PARTIAL_LINK_TEXT, # Locator.PARTIAL_LINK_TEXT: By.PARTIAL_LINK_TEXT,
Locator.CSS: By.CSS_SELECTOR, # Locator.CSS: By.CSS_SELECTOR,
Locator.XPATH: By.XPATH, # Locator.XPATH: By.XPATH,
}.get(by_value, By.XPATH) # }.get(by_value, By.XPATH)
return by # return by
except ValueError: # except ValueError:
return By.XPATH # return By.XPATH
class KeyWordDriver: class CoreDriver:
# def __init__(self, browser: str | Browser): def __init__(self, driver: WebDriver | None = None):
# self.driver = self.webdriver_finder(browser) self.driver: WebDriver | None = driver
def __init__(self): self._url: str | None = configs.base_url
self.driver: WebDriver | None = None
self._url: str | None = None
# self.temp_value = None # self.temp_value = None
def base_url(self, url: str, *args, **kwargs): def base_url(self, url: str, *args, **kwargs):
@@ -93,28 +80,33 @@ class KeyWordDriver:
self._url = url self._url = url
# return 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 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_name.value} 浏览器")
logger.info(f"启动{Browser.CHROME}浏览器")
self.driver = Chrome() self.driver = BrowserFinder(browser_name=browser_name).get_browser_driver(browser_dir, browser_driver)
# return Chrome() # match browser_name:
case Browser.FIREFOX: # case Browser.CHROME:
logger.info(f"启动{Browser.FIREFOX}浏览器") # logger.info(f"启动{Browser.CHROME}浏览器")
self.driver = Firefox() # self.driver = Chrome()
case Browser.IE: # # return Chrome()
logger.info(f"启动{Browser.IE}浏览器") # case Browser.FIREFOX:
self.driver = Ie() # logger.info(f"启动{Browser.FIREFOX}浏览器")
case Browser.SAFARI: # self.driver = Firefox()
logger.info(f"启动{Browser.SAFARI}浏览器") # case Browser.IE:
self.driver = Safari() # logger.info(f"启动{Browser.IE}浏览器")
case Browser.EDGE: # self.driver = Ie()
logger.info(f"启动{Browser.EDGE}浏览器") # case Browser.SAFARI:
self.driver = Edge() # logger.info(f"启动{Browser.SAFARI}浏览器")
case _: # self.driver = Safari()
logger.info(f"启动默认浏览器: {Browser.CHROME}") # case Browser.EDGE:
self.driver = Chrome() # 默认情况 # 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: def find_element(self, by: str = By.XPATH, value: Optional[str] = None, *args, **kwargs) -> WebElement:
by = by_converter(by) by = by_converter(by)
@@ -137,7 +129,7 @@ class KeyWordDriver:
def explicit_wait(self, method: T, timeout: float = EXPLICIT_WAIT_TIMEOUT, *args, **kwargs): def explicit_wait(self, method: T, timeout: float = EXPLICIT_WAIT_TIMEOUT, *args, **kwargs):
""" """
显示等待 显示等待AttributeError: 'WebDriver' object has no attribute 'send_keys'
:param method: 可调用对象名 :param method: 可调用对象名
:param timeout: 超时时间 :param timeout: 超时时间
:param args: :param args:
@@ -159,7 +151,7 @@ class KeyWordDriver:
def page_load_timeout(self, timeout: float, *args, **kwargs) -> None: def page_load_timeout(self, timeout: float, *args, **kwargs) -> None:
self.driver.set_page_load_timeout(timeout) 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: if self.driver is None:
self.browser(*args, **kwargs) self.browser(*args, **kwargs)
if not url.startswith("http"): if not url.startswith("http"):
@@ -326,7 +318,8 @@ class KeyWordDriver:
if __name__ == '__main__': if __name__ == '__main__':
from commons.settings import configs from commons.settings import configs
el = KeyWordDriver()
el = CoreDriver()
el.base_url(configs.base_url) el.base_url(configs.base_url)
# el.browser("chrome") # el.browser("chrome")
el.browser(Browser.EDGE) 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") type_value = el.get_attribute("", '//*[@id="root"]/div[1]/div/div/form/div[3]/button', "type")
print(type_value) print(type_value)
el.assert_text_equals("", '//*[@id="root"]/div[1]/div/div/form/div[3]/button', "登录") 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" IE = "ie"
SAFARI = "safari" SAFARI = "safari"
EDGE = "edge" EDGE = "edge"
DEFAULT = "default"
class Locator(str, Enum): class Locator(str, Enum):

View File

@@ -43,6 +43,11 @@ EXCHANGER = Path(ROOT_PATH, "extract.toml")
# 自增ID # 自增ID
ID_PATH = Path(ROOT_PATH, "id.toml") 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 = { DEFAULT_CONF = {
"SCREENSHOT_DIR": SCREENSHOT_DIR, "SCREENSHOT_DIR": SCREENSHOT_DIR,
@@ -103,7 +108,6 @@ class Settings:
new_conf = DEFAULT_CONF | result new_conf = DEFAULT_CONF | result
for key, value in new_conf.items(): for key, value in new_conf.items():
self.__setattr__(key, value) self.__setattr__(key, value)
return self return self
@@ -134,4 +138,3 @@ configs = Settings()
if __name__ == '__main__': if __name__ == '__main__':
... ...
print(configs.items()) print(configs.items())

View File

@@ -9,17 +9,12 @@
@date: 2025/4/2 21:57 @date: 2025/4/2 21:57
@desc: @desc:
""" """
import json
import logging import logging
from itertools import chain
import inspect
import allure import allure
from pytest_xlsx.file import XlsxItem 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 commons.driver import KeyWordDriver
from utils.file_processors.file_handle import FileHandle from utils.file_processors.file_handle import FileHandle
from commons.templates import Template 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自动化测试框架" description = "Web自动化测试框架"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = []
]
[dependency-groups] [dependency-groups]
dev = [ 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}")