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 3b9772d..3219f70 100644 Binary files a/tests/test_login.xlsx and b/tests/test_login.xlsx differ 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}")