From 5a50eb82893523f954c04c448ae49cf9ce79a35d Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 25 Jan 2026 17:24:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=99=A8=E8=B0=83=E6=95=B4=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 finder.py 重构定位转换器 - 优化 .gitignore 文件 - 其他优化 --- .gitignore | 17 ++++ POM/{ => page}/login_page.py | 18 ++-- POM/test_login.py | 39 -------- POM/{ => tests}/conftest.py | 4 +- POM/tests/test_login.py | 40 ++++++++ commons/custom_expected_condition.py | 15 ++- commons/driver.py | 122 +++++++++++------------ commons/modules.py | 1 + commons/settings.py | 7 +- conftest.py | 7 +- excel_handle.py | 26 +++++ pyproject.toml | 3 +- settings.toml | 30 ++++++ tests/test_login.xlsx | Bin 10418 -> 10489 bytes utils/finder.py | 139 +++++++++++++++++++++++++++ 15 files changed, 342 insertions(+), 126 deletions(-) create mode 100644 .gitignore rename POM/{ => page}/login_page.py (80%) delete mode 100644 POM/test_login.py rename POM/{ => tests}/conftest.py (78%) create mode 100644 POM/tests/test_login.py create mode 100644 excel_handle.py create mode 100644 settings.toml create mode 100644 utils/finder.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff5baf0 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/POM/login_page.py b/POM/page/login_page.py similarity index 80% rename from POM/login_page.py rename to POM/page/login_page.py index 50baa62..ed2d917 100644 --- a/POM/login_page.py +++ b/POM/page/login_page.py @@ -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) diff --git a/POM/test_login.py b/POM/test_login.py deleted file mode 100644 index e5f8aac..0000000 --- a/POM/test_login.py +++ /dev/null @@ -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"), ("", "")]) -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") diff --git a/POM/conftest.py b/POM/tests/conftest.py similarity index 78% rename from POM/conftest.py rename to POM/tests/conftest.py index 14a77b6..1aa7461 100644 --- a/POM/conftest.py +++ b/POM/tests/conftest.py @@ -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() diff --git a/POM/tests/test_login.py b/POM/tests/test_login.py new file mode 100644 index 0000000..791f5f3 --- /dev/null +++ b/POM/tests/test_login.py @@ -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") diff --git a/commons/custom_expected_condition.py b/commons/custom_expected_condition.py index 139399e..672793d 100644 --- a/commons/custom_expected_condition.py +++ b/commons/custom_expected_condition.py @@ -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 \ No newline at end of file diff --git a/commons/driver.py b/commons/driver.py index faa0324..85f870a 100644 --- a/commons/driver.py +++ b/commons/driver.py @@ -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', "登录") - diff --git a/commons/modules.py b/commons/modules.py index 62b215d..f98f5c4 100644 --- a/commons/modules.py +++ b/commons/modules.py @@ -19,6 +19,7 @@ class Browser(str, Enum): IE = "ie" SAFARI = "safari" EDGE = "edge" + DEFAULT = "default" class Locator(str, Enum): diff --git a/commons/settings.py b/commons/settings.py index c27f4b3..96a4b1a 100644 --- a/commons/settings.py +++ b/commons/settings.py @@ -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()) - diff --git a/conftest.py b/conftest.py index e06b280..e722020 100644 --- a/conftest.py +++ b/conftest.py @@ -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 diff --git a/excel_handle.py b/excel_handle.py new file mode 100644 index 0000000..42e2fc7 --- /dev/null +++ b/excel_handle.py @@ -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 + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ea15532..13d2df4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,7 @@ version = "0.1.0" description = "Web自动化测试框架" readme = "README.md" requires-python = ">=3.10" -dependencies = [ -] +dependencies = [] [dependency-groups] dev = [ diff --git a/settings.toml b/settings.toml new file mode 100644 index 0000000..cb3d334 --- /dev/null +++ b/settings.toml @@ -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" # 也可以是绝对路径 \ No newline at end of file diff --git a/tests/test_login.xlsx b/tests/test_login.xlsx index 3b9772d95c6087eb999abe82a866e86fe8e1a998..3219f70e07645773d0352590194d6540cb69cef7 100644 GIT binary patch delta 5409 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 delta 5342 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 diff --git a/utils/finder.py b/utils/finder.py new file mode 100644 index 0000000..e3ba465 --- /dev/null +++ b/utils/finder.py @@ -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}")