init: 初始化项目

- 创建了基本的项目结构
- 添加了 .gitignore 文件
- 配置了基本的开发环境
- 添加清华镜像源
- 设置了基础的文件夹和文件(如 commons, utils, POM, pytest.ini)

项目说明:
- [项目名称]:Web自动化测试
- [项目描述]:基于pytest,selenium的自动化测试工具
- [开发环境]:Python
This commit is contained in:
2025-04-14 23:05:39 +08:00
commit 92d06dd9cf
24 changed files with 1662 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: assert_functions
@date: 2025/4/13 21:06
@desc:
"""
import logging
from dataclasses import dataclass
from typing import Dict, Callable, TypeVar, Tuple
from typing import Literal
from typing import Optional
from typing import Union
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebElement
# from commons.driver import KeyWordDriver
# from selenium.webdriver.support import expected_conditions as EC
# __all__ = ["EC","custom_ec"]
logger = logging.getLogger(__name__)
T = TypeVar("T")
WebDriverOrWebElement = Union[WebDriver, WebElement]
custom_asserts = {
}
def register(name: str | None = None):
def decorator(func):
if name is not None:
custom_asserts[name] = func
custom_asserts[func.__name__] = func
return func
return decorator
@register()
def get_text_(driver,*args) -> T:
"""
无参预期条件函数
driver
- WebDriver的实例Ie、Firefox、Chrome或远程或 一个WebElement
- driver形参不可省略即使不使用
:return:
"""
_ = driver
logger.warning(f"{args=}")
list_ = [1, 2, 3, 4, 5, 6]
for item in list_:
logger.info(item)
if item == 5:
# logger.info(item)
return item
# @register()
# def get_value(locator: Tuple[str, str]) -> Callable[
# [KeyWordDriver], Union[Literal[False], WebElement]]:
# """
# 有参预期条件函数
# :param locator:
# :return:
# """
#
# def _predicate(driver: KeyWordDriver):
# try:
# return driver.find_element(*locator)
# except Exception as e:
# return False
#
# return _predicate

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: expected_conditional_function
@date: 2025/4/4 14:15
@desc:
"""
import logging
from dataclasses import dataclass
from typing import Dict, Callable, TypeVar, Tuple
from typing import Literal
from typing import Optional
from typing import Union
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebElement
from selenium.webdriver.support import expected_conditions as EC
__all__ = ["EC","custom_ec"]
logger = logging.getLogger(__name__)
T = TypeVar("T")
WebDriverOrWebElement = Union[WebDriver, WebElement]
"""
常用等待条件expected_conditions--来自EC模块
presence_of_element_located: 元素存在于DOM。
visibility_of_element_located: 元素可见。
element_to_be_clickable: 元素可点击。
title_contains: 页面标题包含特定文本。
text_to_be_present_in_element: 元素包含特定文本。
"""
# 自定义预期条件(Custom Expected Condition)
custom_ec = {
}
def register(name: str | None = None):
def decorator(func):
if name is not None:
custom_ec[name] = func
custom_ec[func.__name__] = func
return func
return decorator
@register()
def examples_no_parameters(driver: WebDriverOrWebElement)->T:
"""
无参预期条件函数
driver
- WebDriver的实例Ie、Firefox、Chrome或远程或 一个WebElement
- driver形参不可省略即使不使用
:return:
"""
_ = driver
list_ = [1, 2, 3, 4, 5, 6]
for item in list_:
logger.info(item)
if item == 5:
return item
@register()
def examples_have_parameters(locator: Tuple[str, str]) -> Callable[[WebDriverOrWebElement], Union[Literal[False], WebElement]]:
"""
有参预期条件函数(暂不支持)
:param locator:
:return:
"""
def _predicate(driver: WebDriverOrWebElement):
try:
return driver.find_element(*locator)
except Exception as e:
return False
return _predicate
def func_3(mark):
...
def func_4(mark):
...
if __name__ == "__main__":
print(custom_ec)

341
commons/driver.py Normal file
View File

@@ -0,0 +1,341 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: key_driver
@date: 2025/4/2 21:59
@desc:
"""
import logging
from time import sleep
import secrets # 原生库,用于生成安全的随机数
from typing import Optional, Callable, Union, Literal, Any, TypeVar
from urllib.parse import urljoin
from selenium.webdriver import Chrome, Edge, Firefox, Safari, Ie
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support.ui import Select
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebElement
from commons.assert_functions import custom_asserts
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
logger = logging.getLogger(__name__)
WebDriverOrWebElement = Union[WebDriver, WebElement]
D = TypeVar("D", bound=Union[WebDriver, WebElement])
T = TypeVar("T")
# 触发器 trigger
# 浏览器 Browser
# 执行器 actuator
# 筛选器 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
class KeyWordDriver:
# 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
# self.temp_value = None
def base_url(self, url: str, *args, **kwargs):
logger.info(f"前置URL: {url}")
self._url = url
# return url
def browser(self, browser_name: str | Browser = Browser.CHROME, *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() # 默认情况
def find_element(self, by: str = By.XPATH, value: Optional[str] = None, *args, **kwargs) -> WebElement:
by = by_converter(by)
return self.driver.find_element(by, value)
def delay(self, timeout: int | float):
sleep(timeout)
return self
def implicit_wait(self, timeout: float, *args, **kwargs) -> None:
"""
隐式等待
:param timeout: 超时时间
:param args:
:param kwargs:
:return:
"""
self.driver.implicitly_wait(timeout)
def explicit_wait(self, method: T, timeout: float = EXPLICIT_WAIT_TIMEOUT, *args, **kwargs):
"""
显示等待
:param method: 可调用对象名
:param timeout: 超时时间
:param args:
:param kwargs:
:return:
"""
try:
if isinstance(method, str):
method = custom_ec.get(method, (lambda _: False))
logger.info(f"预期条件: {method.__name__}")
return WebDriverWait(self.driver, timeout).until(method)
except TypeError as te:
logger.error(f"显示等待异常: {te}")
# self.driver.quit()
raise te
def page_load_timeout(self, timeout: float, *args, **kwargs) -> None:
self.driver.set_page_load_timeout(timeout)
def get(self, url, *args, **kwargs):
if self.driver is None:
self.browser(*args, **kwargs)
if not url.startswith("http"):
# 自动添加baseurl
url = urljoin(self._url, url)
logger.info(f"网址: {url}")
print(url)
self.driver.get(url)
def maximize_window(self):
self.driver.maximize_window()
def click(self, by, value, *args, **kwargs):
by = by_converter(by)
mark = (by, value)
method = EC.element_to_be_clickable(mark)
self.explicit_wait(method).click()
# self.find_element(by, value).click()
def clear(self, by, value, *args, **kwargs):
by = by_converter(by)
mark = (by, value)
method = EC.visibility_of_element_located(mark)
self.explicit_wait(method).clear()
# self.driver.find_element(by, value).clear()
def input(self, by, value, content: str | None = None, *args, **kwargs):
by = by_converter(by)
mark = (by, value)
method = EC.visibility_of_element_located(mark)
self.explicit_wait(method).send_keys(content)
# self.find_element(by, value).send_keys(content)
def get_text(self, by, value, *args, **kwargs):
"""
获取元素文本
:param by:
:param value:
:param args:
:param kwargs:
:return:
"""
by = by_converter(by)
mark = (by, value)
method = EC.visibility_of_element_located(mark)
text = self.explicit_wait(method).text
# print(text)
return text
def get_attribute(self, by, value, attributes: str, *args, **kwargs):
"""
获取元素属性值(classtypevalue,...)
:param by:
:param value:
:param attributes:
:param args:
:param kwargs:
:return:
"""
by = by_converter(by)
mark = (by, value)
method = EC.presence_of_element_located(mark)
text = self.explicit_wait(method).get_attribute(attributes)
# print(text)
return text
def enter_iframe(self, by, value, *args, **kwargs):
by = by_converter(by)
element = self.find_element(by, value)
self.driver.switch_to.frame(element)
def exit_iframe(self, *args, **kwargs):
self.driver.switch_to.default_content()
def get_cookie(self, name, *args, **kwargs) -> Optional[dict]:
return self.driver.get_cookie(name)
def add_cookie(self, cookie_dict, *args, **kwargs) -> None:
self.driver.add_cookie(cookie_dict)
def screenshot_png(self, by, value, name: str | None = None, *args, **kwargs) -> None:
by = by_converter(by)
# mark = (by, value)
# method = EC.visibility_of_element_located(mark)
if name is not None:
path = (configs.SCREENSHOT_DIR / f"{name}.png").as_posix()
else:
# 生成 8 个随机字节,然后转为 16 个字符的十六进制字符串
random_hex = secrets.token_hex(8) # n=8 表示生成 8 字节
path = (configs.SCREENSHOT_DIR / f"{random_hex}.png").as_posix()
logger.warning(f"截图存放路径: {path}")
# self.explicit_wait(method).screenshot(path)
self.find_element(by, value).screenshot(path)
def select(self, by, value, text, func_name, *args, **kwargs):
# 未完成
by = by_converter(by)
mark = (by, value)
method = EC.visibility_of_element_located(mark)
self.explicit_wait(method)
def run_javascript(self, by, value, code: str, *args, **kwargs) -> Any:
# 还需打磨
by = by_converter(by)
element = self.find_element(by, value)
return self.driver.execute_script(code, element)
# def assert_text_equals(self, by, value,method, text, msg=None):
# by = by_converter(by)
# # _text = self.get_text(by, value)
# _text = getattr(self,method)(by, value)
#
# assert _text == text, msg
# def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
# try:
# value = method(self.driver)
# if value:
# return value
# except AssertionError as e:
# logger.error(e)
#
# def assert_text_equals(self, method, text, msg=""):
# if isinstance(method, str):
# method = custom_asserts.get(method, (lambda _: False))
# logger.info(f"{method=}")
# # _text = self.until(method)
# _text = method(self.driver)
# logger.info(_text)
# assert _text == text, msg
def assert_text_equals(self, by, value, text):
by = by_converter(by)
_text = self.get_text(by, value)
assert _text == text, f"断言“{_text}”与“{text}”相等失败!!"
logger.info(f"断言: {_text} == {text}")
def assert_text_not_equals(self, by, value, text):
by = by_converter(by)
_text = self.get_text(by, value)
assert _text != text, f"断言“{_text}”与“{text}”不相等失败!!"
def assert_text_contains(self, by, value, text, msg=None):
by = by_converter(by)
_text = self.get_text(by, value)
assert _text in text, f"断言“{_text}”包含于“{text}”中失败!!"
def assert_text_not_contains(self, by, value, text, msg=None):
by = by_converter(by)
_text = self.get_text(by, value)
assert _text not in text, f"断言“{_text}”不包含于“{text}”中失败!!"
if __name__ == '__main__':
from commons.settings import configs
el = KeyWordDriver()
el.base_url(configs.base_url)
# el.browser("chrome")
el.browser(Browser.EDGE)
el.get("/users/login")
el.input("", '//*[@id="email"]', configs.username)
el.input("", '//*[@id="pass"]', configs.password)
el.get_text("", '//*[@id="root"]/div[1]/div/div/form/div[1]/label')
el.click("", '//*[@id="root"]/div[1]/div/div/form/div[3]/button')
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', "登录")

137
commons/funcs.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: chen wei
@Software: PyCharm
@contact: t6i888@163.com
@file: funcs.py
@date: 2024 2024/9/22 22:46
@desc:
"""
import base64
import logging
import time
import urllib.parse
import hashlib
# from commons.databases import db
#
from utils.file_processors.file_handle import FileHandle
from commons import settings
logger = logging.getLogger(__name__)
class Funcs:
FUNC_MAPPING = {
"int": int,
"float": float,
"bool": bool
} # 内置函数有的直接放入mapping内置函数没有的在funcs中定义自动放入mapping
@classmethod
def register(cls, name: str | None):
def decorator(func):
if name is None:
cls.FUNC_MAPPING[func.__name__] = func
cls.FUNC_MAPPING[name] = func
return func
return decorator
@Funcs.register("url_unquote")
def url_unquote(s: str) -> str:
return urllib.parse.unquote(s)
@Funcs.register("str")
def to_string(s) -> str:
# 将数据转换为str类型。
return f"'{s}'"
@Funcs.register("time_str")
def time_str() -> str:
return str(time.time())
@Funcs.register("add")
def add(a, b):
return str(int(a) + int(b))
# @Funcs.register("sql")
# def sql(s: str) -> str:
# res = db.execute_sql(s)
#
# return res[0][0]
@Funcs.register("new_id")
def new_id():
# 自增,永不重复
id_file = FileHandle(settings.ID_PATH)
id_file["id"] += 1
id_file.save()
return id_file["id"]
# @Funcs.register("last_id")
# def last_id() -> str:
# # 不自增,只返回结果
#
# id_file = FileHandle(settings.id_path)
# return id_file["id"]
@Funcs.register("md5")
def md5(content: str) -> str:
# 1原文转为字节
content = content.encode("utf-8")
result = hashlib.md5(content).hexdigest()
return result
@Funcs.register("base64_encode")
def base64_encode(content: str) -> str:
# 1原文转二进制
content = content.encode("utf-8")
# 2base64编码二进制
encode_value = base64.b64encode(content)
# 3转为字符串
encode_str = encode_value.decode("utf-8")
return encode_str
@Funcs.register("base64_decode")
def base64_decode(content: str) -> str:
# 1原文转二进制
content = content.encode("utf-8")
# 2base64解码二进制
decode_value = base64.b64decode(content)
# 3转为字符串
decode_str = decode_value.decode("utf-8")
return decode_str
@Funcs.register("rsa_encode")
def rsa_encode(content: str) -> str:
...
@Funcs.register("rsa_decode")
def rsa_decode(content: str) -> str:
...
if __name__ == '__main__':
# res = url_unquote("%E6%88%90%E5%8A%9F%E3%80%82")
# print(res)
# print(f"计数器:{new_id()}")
# print(f"当前数值:{last_id()}")
print(Funcs().FUNC_MAPPING)

41
commons/modules.py Normal file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: modules
@date: 2025/4/5 20:29
@desc:
"""
from enum import Enum
class Browser(str, Enum):
CHROME = "chrome"
FIREFOX = "firefox"
IE = "ie"
SAFARI = "safari"
EDGE = "edge"
class Locator(str, Enum):
ID = "id",
NAME = "name",
CLASS = "class",
TAG = "tag",
LINK_TEXT = "link_text",
PARTIAL_LINK_TEXT = "partial_link_text",
CSS = "css",
XPATH = "xpath"
if __name__ == '__main__':
print(Browser.CHROME)
print(type(Browser.CHROME))
print(Browser("chR ome ".lower().replace(' ', '')))
match "chrome":
case Browser.CHROME:
print("我能执行!!!")

137
commons/settings.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: CNWei
@Software: PyCharm
@contact: t6i888@163.com
@file: settings
@date: 2025/2/23 21:34
@desc:
"""
import logging
from pathlib import Path
from typing import Any
import tomlkit
ROOT_PATH = (Path(__file__)).resolve().parents[1] # 获取根路径(绝对路径)
EXPLICIT_WAIT_TIMEOUT = 10
DATA_ROOT = ROOT_PATH / "data"
if not DATA_ROOT.exists():
DATA_ROOT.mkdir(parents=True, exist_ok=True)
# 配置文件路
CONF_PATH = Path(DATA_ROOT, "settings.toml")
# 截图保存路径
SCREENSHOT_DIR = Path(ROOT_PATH, "screenshot")
if not SCREENSHOT_DIR.exists():
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
# 测试案例路径
CASES_DIR = Path(ROOT_PATH, "tests")
# 变量替换
EXCHANGER = Path(ROOT_PATH, "extract.toml")
# 自增ID
ID_PATH = Path(ROOT_PATH, "id.toml")
# 默认配置
DEFAULT_CONF = {
"SCREENSHOT_DIR": SCREENSHOT_DIR,
"CASES_DIR": CASES_DIR,
"EXCHANGER": EXCHANGER,
"ID_PATH": ID_PATH,
}
class Settings:
"""
配置管理类 (单例模式 Singleton Pattern)。
优先从项目根目录下的 'settings.toml' 文件加载配置。
如果 'settings.toml' 文件不存在、无法解析或缺少某个配置项,
则使用此类中定义的默认值。
通过创建类的唯一实例 `settings` 来全局访问配置。
"""
_instance = None
# --- 单例实现 ---
def __new__(cls, *args, **kwargs):
"""
实现单例模式。确保全局只有一个 Settings 实例。
"""
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
# 初始化只进行一次
cls._instance._initialized = False
return cls._instance
def __init__(self):
"""
初始化配置。仅在第一次创建实例时执行。
"""
# 防止重复初始化 (单例模式下可能会被多次调用 __init__)
if self._initialized:
return
self._initialized = True
logging.info("开始初始化配置...")
self._init_config()
self._load_config()
def _init_config(self):
"""初始化配置文件"""
if not CONF_PATH.exists():
CONF_PATH.parent.mkdir(parents=True, exist_ok=True)
CONF_PATH.touch()
# self._save_config(DEFAULT_CONF)
return self
def _load_config(self):
with open(CONF_PATH, 'r', encoding='utf-8') as f:
result = tomlkit.parse(f.read())
logging.debug(f"加载 settings.toml 文件 ===> {result}")
new_conf = DEFAULT_CONF | result
for key, value in new_conf.items():
self.__setattr__(key, value)
return self
def _save_config(self, data: dict[str, Any]):
"""保存配置到文件"""
for key, value in data.items():
if isinstance(value, Path):
data[key] = value.as_posix()
with open(CONF_PATH, 'w', encoding='utf-8') as f:
tomlkit.dump(data=data, fp=f, sort_keys=False)
return self
def __setattr__(self, key, value):
self.__dict__[key] = value
def items(self):
conf: dict = {}
for key, value in self.__dict__.items():
if not key.startswith('_'):
conf[key] = value
# return self.__dict__.items()
return conf
configs = Settings()
# --- 用于直接运行此文件进行测试 ---
if __name__ == '__main__':
...
print(configs.items())

63
commons/templates.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
# coding=utf-8
"""
@author: chen wei
@Software: PyCharm
@contact: t6i888@163.com
@file: templates.py
@date: 2024 2024/9/22 22:20
@desc:
"""
import copy
import logging
import re
import string
from commons.funcs import Funcs
logger = logging.getLogger(__name__)
class Template(string.Template):
"""
1支持函数调用
2参数也可以是变量
"""
call_pattern = re.compile(r"\${(?P<func_name>.*?)\((?P<func_args>.*?)\)}")
def render(self, mapping: dict) -> str:
s = self.safe_substitute(mapping) # 原有方法替换变量
s = self.safe_substitute_funcs(s, mapping)
return s
def safe_substitute_funcs(self, template, mapping) -> str:
"""
解析字符串中的函数名和参数,并将函数调用结果进行替换
:param template: 字符串
:param mapping: 上下文,提供要使用的函数和变量
:return: 替换后的结果
"""
mapping = copy.deepcopy(mapping)
logger.info(f"mapping更新前: {mapping}")
# mapping.update(self.FUNC_MAPPING) # 合并两个mapping
mapping.update(Funcs.FUNC_MAPPING) # 合并两个mapping
logger.info(f"mapping更新后: {mapping}")
def convert(mo):
func_name = mo.group("func_name")
func_args = mo.group("func_args").split(",")
func = mapping.get(func_name) # 读取指定函数
func_args_value = [mapping.get(arg, arg) for arg in func_args]
if func_args_value == [""]: # 处理没有参数的func
func_args_value = []
if not callable(func):
return mo.group() # 如果是不可调用的假函数,不进行替换
else:
return str(func(*func_args_value)) # 否则用函数结果进行替换
return self.call_pattern.sub(convert, template)