1.AddAccessory(输入控件)
一、导入依赖:控件需要用到的 "工具"
Matlab
# 1. 重试装饰器:当上传失败时(比如网络波动),自动重试几次
from frameworkCore.common.decorator import retry
# 2. 输入控件基类:所有输入类控件(文本框、复选框、附件上传等)都继承它,能直接用基类里的通用功能(比如找元素、断言)
from frameworkCore.business.InputCtrlBase import InputCtrlBase
# 3. 定位工具:用来定义"如何找到页面上的控件"(比如通过XPATH路径)
from frameworkCore.driver.locator import Locator
# 4. 定位方式:提供By.XPATH、By.ID等定位元素的方式
from frameworkCore.driver.by import By
# 5. 控件注册工具:把当前控件"注册"到框架里,让其他地方能找到并调用它
from script.common.CtrlManager import register_comp
# 6. 通用弹窗控件:处理上传后弹出的提示窗口(比如"上传成功"提示)
from script.hsy.component.common.DialogComp import DialogComp
# 7. 确认弹窗控件:可能用于处理"覆盖已有文件"等确认场景(当前代码没用到,但预留了依赖)
from script.hsy.component.confirm.ConfirmDialog import ConfirmDialog
# 8. 文件选择控件:实际执行"打开文件选择窗口、选择文件并上传"的底层控件
from script.hsy.component.ctrl.SelectFileCtrl import SelectFileCtrl
作用:就像做饭前要准备好锅碗瓢盆,这些导入的工具是控件能正常工作的基础,每个工具都有明确的分工。
二、注册控件:告诉框架 "我是一个可用的控件"
Matlab
@register_comp # 这是一个装饰器,作用是把下面的AddAccessory类"注册"到框架的控件管理器中
class AddAccessory(InputCtrlBase): # 继承InputCtrlBase基类,获得输入控件的通用能力(比如找元素、基础交互)
"""添加附件ctrl""" # 类的文档注释,说明这个控件的功能
三、定义定位规则:告诉框架 "页面上哪个元素是我"
Matlab
# 定位规则1:默认的"添加附件"控件定位方式(通过XPATH找元素)
inputCtrlLoc = Locator(
By.XPATH, # 定位方式:用XPATH路径
'.//input[@type="checkbox"]/parent::span', # 具体路径:找类型为checkbox的input标签的父级span元素
wait=10 # 最多等10秒,直到元素出现
)
# 定位规则2:子控件的定位方式(可能用于页面上有多个附件选项的场景,比如"发票""合同"两个附件类型)
child_ctrl = Locator(
By.XPATH,
'.//span[@class="ui-checkbox-label" and .="{}"]//parent::div//input[@type="checkbox"]/parent::span', # {}是占位符,会替换成具体的附件名称(比如"发票")
wait=2 # 最多等2秒
)
作用 :框架运行时,需要知道 "在页面的哪个位置" 找到 "添加附件" 的按钮 / 复选框,这两个Locator
就是具体的 "地图"。
- 比如
inputCtrlLoc
是默认的 "添加附件" 按钮定位,child_ctrl
可能用于页面上有多个附件选项的场景(比如报销单里可以上传 "发票""行程单" 等不同附件,通过替换{}
里的文字定位到具体选项)。
四、初始化方法:创建控件实例时的准备工作
python
def __init__(self, ctrl=None) -> None:
super().__init__() # 调用父类InputCtrlBase的初始化方法,让父类的功能生效
self.ctrl = ctrl # 保存传入的控件实例(如果有的话),后续操作这个实例对应的页面元素
通俗理解 :当你用AddAccessory()
创建一个附件上传控件的实例时,这个方法会自动执行,先让父类做好准备工作,再把需要操作的页面元素(ctrl
)存起来,方便后续使用。
五、核心方法input()
:执行 "上传附件" 的具体步骤
Matlab
@retry() # 重试装饰器:如果这个方法执行失败(比如上传超时),会自动重试几次(次数在retry装饰器里定义)
def input(self, value): # value是要上传的文件路径(比如"C:/发票.jpg")
# 步骤1:创建文件选择控件的实例,负责实际选择文件并上传
selectFileCtrl = SelectFileCtrl.instance(self) # 通过instance方法创建实例,确保和当前控件关联
selectFileCtrl.selectFile(value) # 调用文件选择控件的selectFile方法,传入文件路径,执行上传
# 步骤2:处理上传后的提示弹窗
dialog_comp = DialogComp() # 创建通用弹窗控件的实例
if dialog_comp.is_show(): # 判断弹窗是否显示(比如上传成功后会弹出提示窗)
dialog_comp.文本校验('上传成功') # 校验弹窗里的文字是不是"上传成功"(确保上传真的成功了)
dialog_comp.关闭窗口() # 关闭弹窗,避免影响后续操作
这是整个控件的 "核心功能区",完整描述了 "上传附件" 的流程:
- 借助
SelectFileCtrl
完成 "选文件→上传" 的底层操作(相当于 "让助手去做具体的体力活"); - 上传后检查弹窗,确认成功并关闭(相当于 "检查结果,清理现场");
@retry()
确保遇到临时问题(如网络卡了)时,不会直接失败,而是重试几次,提高稳定性。
六、辅助方法:控件的 "附加功能"
Matlab
def text(self):
pass # 空实现:因为"添加附件"控件不需要"获取文本"的功能(不像输入框需要获取输入的内容),所以留空
@classmethod # 类方法:不需要创建实例,直接通过类调用
def is_this_ctrl(cls, ctrl_html):
"""根据控件html判断是否是此控件"""
# 如果页面元素的HTML代码里包含"oss-upload"或"fileUpload"(文件上传相关的关键词),就认为这是"添加附件"控件
if 'oss-upload' in ctrl_html or 'fileUpload' in ctrl_html:
return True
else:
return False
text()
:父类InputCtrlBase
可能要求所有输入控件都有text()
方法(为了统一接口),但附件上传控件用不上,所以空实现。is_this_ctrl()
:框架的 "控件识别器" 会调用这个方法。比如框架扫描页面时,拿到某个元素的 HTML 代码,用这个方法判断 "这是不是附件上传控件",如果是,就用AddAccessory
来操作它。
2.find_child
函数
是框架中用于查找 "子元素" 的核心方法,专门用来在 "当前控件的父元素" 内部查找更具体的子元素(比如在 "表单控件" 里找 "输入框子元素")。
一、先看装饰器和基本定义
Matlab
@check_case_stop() # 装饰器:作用是"如果测试用例被终止(比如手动暂停),就不再执行这个函数"
def find_child(self, locator, reload=False) -> SElementBase: # 返回值是一个元素对象(SElementBase类型)
@check_case_stop()
:这是测试框架常见的 "安全控制",比如你在运行用例时手动点了 "停止",这个装饰器会让函数立刻退出,避免继续执行无效操作。- 参数
locator
:要查找的子元素的定位规则(比如Locator(By.XPATH, './/input')
)。 - 参数
reload
:是否强制重新获取父元素(默认False
,表示用已有的父元素;True
表示重新找一次父元素)。
二、核心逻辑:在父元素内部找子元素
Matlab
try:
# 步骤1:获取当前控件的"父元素"(this_ele就是父元素对象)
this_ele = self.get_element(reload) # reload=False时,用缓存的父元素;True时重新查找
# 步骤2:在父元素内部,用传入的locator查找子元素
return this_ele.find_element(locator)
三、异常处理:解决元素查找失败的常见问题
Matlab
except StaleElementReferenceException as err:
# 异常1:"过时元素引用"------父元素已经失效(比如页面刷新后,原来的父元素被重新渲染了)
this_ele = self.get_element(True) # 强制重新获取父元素(reload=True)
return this_ele.find_element(locator) # 用新的父元素再找子元素
except TimeoutException as timeout_err:
# 异常2:超时------找了很久都没找到子元素
raise timeout_err # 把超时错误抛出去,让上层代码处理(比如记录失败日志)
3.SelectFileCtrl(执行控件)
是专门处理文件选择和上传 的核心控件,它是之前AddAccessory
控件的 "底层执行者"------AddAccessory
负责统筹流程,而SelectFileCtrl
负责实际的 "选文件、传文件" 操作。
一、基础信息:控件定位和继承关系
Matlab
# 导入动作控件基类:所有"执行操作"的控件(如按钮点击、文件上传)都继承它,提供基础的交互能力
from frameworkCore.business.ActionCtrlBase import ActionCtrlBase;
# 定位工具:定义如何找到页面上的上传区域
from frameworkCore.driver.locator import Locator
from frameworkCore.driver.by import By
# 重试装饰器:上传可能失败,自动重试
from frameworkCore.common.decorator import retry
# 运行上下文:可能用于获取当前测试的环境信息(如浏览器驱动)
from frameworkCore.business.runContext import RunContext
class SelectFileCtrl(ActionCtrlBase):
"""适用于上传input位于iframe中的情况"""
# 定位"上传区域容器"的规则:找包含这些class或属性的div(覆盖多种上传组件样式)
locator = Locator(By.XPATH,".//div[contains(@class,'datagrid-empty attachment-container') or contains(@class,'importDialogNormal') or contains(@class,'fileUpload') or contains(@class,'oss-upload') or contains(@class,'ant-upload ant-upload-drag') or contains(@class,'import-assets-dialog') or contains(@class,'import-upload-area') or contains(@data-super-id,'upload#oss上传')]")
# 定位"文件上传input框"的规则:找type为file、且符合特定id或accept属性的input标签
fileUploadLoc = Locator(By.XPATH,'.//input[@type="file" and contains(@id,"oss-file-auto") or contains(@accept,".png,.jpg,.jpeg,.pdf") or contains(@accept,".xlsx,.xls,.csv")]')
- 继承
ActionCtrlBase
:获得 "执行动作" 的基础能力(比如找元素、切换 iframe 等)。 locator
:定位页面上的 "上传区域容器"(比如 "拖放文件到此处" 的大框),覆盖了多种 class 和属性,适配不同样式的上传组件(比如普通上传、OSS 云上传、拖拽上传等)。fileUploadLoc
:定位真正负责接收文件的<input type="file">
标签(这是 HTML 中处理文件上传的原生控件,浏览器会自动关联文件选择窗口)。
二、核心方法selectFile()
:执行文件上传的具体步骤
Matlab
@retry() # 上传可能因网络或浏览器问题失败,重试几次提高成功率
def selectFile(self, file): # file是要上传的文件路径(支持单个文件路径或文件路径列表)
"""file:完整文件路径"""
# 步骤1:处理传入的文件参数(支持单个文件或多个文件)
try:
files = []
if (isinstance(file, list)): # 如果传入的是列表(多个文件)
files = file
else: # 如果传入的是单个文件路径,转为列表
files.append(file)
# 步骤2:循环上传每个文件
for data in files:
# 找到"文件上传input框"(在当前上传区域容器内找子元素)
uploadInputEle = self.find_child(self.fileUploadLoc)
# 向input框传入文件路径(相当于手动选择文件)
uploadInputEle.sendKeys(data)
# 步骤3:处理异常(上传失败时抛出错误,让上层控件处理)
except Exception as err:
raise err
- 支持多文件上传 :通过判断
file
是否为列表,实现 "单个文件" 和 "多个文件" 的兼容(比如一次上传多个发票图片)。 - 定位并操作原生上传控件 :
self.find_child(self.fileUploadLoc)
:在locator
定位的 "上传区域容器" 内,找到隐藏的<input type="file">
标签(这个标签通常看不到,是浏览器处理文件上传的底层元素)。uploadInputEle.sendKeys(data)
:这是关键!对原生文件上传控件使用sendKeys
方法传入文件路径,相当于在浏览器的 "文件选择窗口" 中手动选择了该文件,触发上传动作(比模拟点击 "浏览" 按钮更稳定)。
三、为什么这样设计?(结合 "iframe 中上传" 的场景)
注释里特别提到 "适用于上传 input 位于 iframe 中的情况",这是很多系统的常见设计 ------ 把上传组件放在独立的 iframe 里,避免样式冲突。这种情况下,直接找元素会失败,需要先 "进入 iframe" 才能操作里面的元素。
虽然代码里没直接写切换 iframe 的逻辑,但继承的ActionCtrlBase
基类中很可能包含了相关处理(比如自动检测并切换到上传控件所在的 iframe),这也是框架封装的价值:使用者不用关心 "怎么切换 iframe",只需要调用selectFile
方法传文件路径就行。
四、和AddAccessory
控件的关系
这两个控件是 "分工协作" 的关系:
AddAccessory(统筹) → 调用 → SelectFileCtrl(执行)
AddAccessory
:负责 "上传前的准备(如定位添加附件按钮)" 和 "上传后的校验(如检查成功弹窗)"。SelectFileCtrl
:专注于 "实际上传动作"(处理文件路径、找到上传控件、传入文件并触发上传)。
这种拆分符合 "单一职责原则"------ 每个控件只做自己擅长的事,既方便维护(比如要改上传逻辑,只需要改SelectFileCtrl
),也提高了复用性(其他需要上传文件的控件也能直接用SelectFileCtrl
)。
4.SelectFileFrame(执行控件)
和之前的SelectFileCtrl
功能类似,但更专注于 "iframe 切换" 的细节处理。
一、先明确:什么是 iframe?为什么要专门处理?
iframe 是网页中的 "嵌入式窗口",可以把一个独立页面嵌入到当前页面中(比如很多系统会把上传组件、弹窗内容放在 iframe 里,避免样式或脚本冲突)。
问题 :在自动化测试中,直接查找 iframe 内部的元素会失败,必须先 "切换到 iframe 内部" 才能操作里面的元素,操作完还要 "切回原页面"。
SelectFileFrame
的核心价值就是:自动完成 "进入 iframe→上传文件→切回原页面" 的全流程。
二、代码逐部分解析
1. 导入依赖和类定义
- 继承
ActionCtrlBase
:获得 "查找元素" 等基础能力,和SelectFileCtrl
属于同一类 "动作型控件"。 - 注释明确了适用场景:专门处理上传控件在 iframe 里的情况。
2. 定位规则:找 iframe 和内部的上传控件
Matlab
# 定位iframe的规则:找页面中所有iframe标签(简单直接,适用于页面只有一个上传相关iframe的场景)
locator = Locator(By.XPATH,".//iframe")
# 定位iframe内部的上传控件:找iframe里的所有input标签(通常上传控件是<input type="file">)
fileUploadLoc = Locator(By.XPATH,'//input')
locator
:目标是找到包含上传控件的那个 iframe(用.//iframe
定位,假设页面中该场景下只有一个关键 iframe)。fileUploadLoc
:在 iframe 内部找上传用的 input 标签(简化定位,因为 iframe 内部通常只有一个上传控件)。
3. 核心方法selectFile()
:带 iframe 切换的上传流程
Matlab
@retry() # 上传可能失败(如iframe切换出错、文件路径错误),自动重试
def selectFile(self, file): # file是要上传的文件完整路径
"""file:完整文件路径"""
# 步骤1:获取浏览器驱动(操作浏览器的入口,比如切换iframe、找元素都靠它)
driver = RunContext.getRunContext().driver
try:
# 步骤2:找到iframe元素(强制重新获取,避免iframe已刷新)
iframeEle = self.get_element(reload=True)
# 步骤3:切换到iframe内部(这是关键!不切换就找不到里面的上传控件)
driver.switchToFrame(iframeEle)
# 步骤4:在iframe内部找到上传input控件,传入文件路径
uploadInputEle = driver.find_element(self.fileUploadLoc)
uploadInputEle.sendKeys(file) # 传入文件路径,触发上传
# 步骤5:如果出错,抛出异常(让上层代码处理,比如记录日志)
except Exception as err:
raise err
# 步骤6:无论成功失败,最终都切回原页面(避免影响后续操作)
finally:
driver.switchToDefault()
三、这个控件的设计逻辑和适用场景
为什么需要SelectFileFrame
?
和之前的SelectFileCtrl
相比,它的核心差异是:明确处理了 iframe 切换的细节。
SelectFileCtrl
可能依赖基类自动处理 iframe(代码中没显式写切换逻辑);SelectFileFrame
则把 "切换 iframe→操作→切回" 的过程完全显式写出来,更适合 iframe 结构复杂、需要精确控制切换的场景。
5.LowCodeDateRangeCtrl(输入控件)
是一个处理 "日期范围选择" 的输入控件,专门用于在低代码平台(或类似系统)中设置 "开始日期" 和 "结束日期"(比如查询某段时间的订单、设置活动有效期等场景)。
Matlab
# 定义日期范围控件类,继承InputCtrlBase(获得输入控件的通用能力)
class LowCodeDateRangeCtrl(InputCtrlBase):
# 定义日期输入框的定位规则(核心定位器)
dateInputLoc = Locator(
By.XPATH, # 定位方式:使用XPATH路径
# XPATH路径:在日期弹窗(ant-popover-content)里找带特定class的输入框,{}是占位符
'(//div[@class="ant-popover-content"]//div[contains(@class,"ant-picker-input")]/input)[{}]',
desc='日期输入框' # 描述:方便调试时知道这个定位器是干嘛的
)
父元素是在LowCodeDateRangeCtrl这个类实例化的时候,调用基类的instance方法绑定了父类元素。
LowCodeDateRangeCtrl(InputCtrlBase)
:继承基类后,这个控件就自动拥有了 "查找元素""基础输入" 等功能,不用自己写。dateInputLoc
:这是定位页面上 "日期输入框" 的 "地图"。{}
是占位符,后续会替换成1
或2
,分别定位 "开始日期输入框" 和 "结束日期输入框"(比如很多系统的日期范围组件是两个输入框并排)。
一、核心方法input()
:设置日期范围的主逻辑
Matlab
def input(self, data): # data是传入的日期数据(可以是字符串或字典)
"""
date格式:
1、{"start":"2021-06-01","end":"2021-06-02"} # 字典格式,分别设置开始和结束日期
2、2021-06-01,表示开始和结束日期一样(单个日期)
"""
# 步骤1:初始化开始和结束日期变量
start = None
end = None
# 步骤2:根据传入的data格式,解析出start和end
if isinstance(data, str): # 如果data是字符串(比如"2021-06-01")
start = data # 开始日期=传入的字符串
end = data # 结束日期=传入的字符串(开始=结束,即单个日期)
else: # 如果data是字典(比如{"start":"...", "end":"..."})
start = data["start"] # 从字典取"start"对应的值
end = data["end"] # 从字典取"end"对应的值
# 步骤3:定位并输入开始日期
# 用format_key("1")把占位符{}替换成1,定位到"开始日期输入框"
startEle = self.find_child(self.dateInputLoc.format_key("1"))
# 调用_clearAndInput方法:清空输入框并填入start(开始日期)
self._clearAndInput(startEle, start)
time.sleep(1) # 等待1秒,避免输入太快导致页面没反应
# 步骤4:定位并输入结束日期
# 用format_key("2")把占位符{}替换成2,定位到"结束日期输入框"
endEle = self.find_child(self.dateInputLoc.format_key("2"))
# 调用_clearAndInput方法:清空输入框并填入end(结束日期)
self._clearAndInput(endEle, end)
# 步骤5:按Enter键确认(通常用于关闭日期选择弹窗或触发查询)
endEle.sendKeys(Keys.ENTER)
dateInputLoc是Locator实例,所以可以调用Locator的format_key方法
def format_key(self,format):
self.__format_key = format
return self
@property
def path(self):
if self.__format_key == None:
return self.__value
elif isinstance( self.__format_key,dict) :
return self.__value.format(**self.__format_key)
elif isinstance(self.__format_key, list):
return self.__value.format(*self.__format_key)
else:
return self.__value.format(self.__format_key)
这样做到了xpath的替换,把原先{}的地方换成传入的format_key值(先不细说Locator,后续再学)
二、辅助方法_clearAndInput()
:确保输入正确
Matlab
@retry() # 重试装饰器:如果这个方法执行失败(比如输入后校验不通过),会自动重试
def _clearAndInput(self, ctrl, value): # ctrl是输入框元素,value是要输入的日期
ctrl.mouse_click() # 点击输入框,激活它(有些输入框需要点击才会弹出光标)
ctrl.selectAll() # 全选输入框里的现有内容(比如默认的"今天")
ctrl.clear() # 清空输入框(避免旧内容残留)
ctrl.sendKeys(value) # 输入新的日期值(比如"2021-06-01")
# 校验输入是否成功:获取输入框的value属性(实际显示的值)
date = ctrl.getAttribute('value')
# 断言:如果实际值不等于输入值,就抛出错误(提示"校验日期输入是否正确")
assert date == value, '校验日期输入是否正确'
日期输入框元素就是最基本的ctrl元素,他的value值就是刚刚输入的日期。
- 方法名前的
_
:表示这是 "内部方法",只在控件内部使用,不建议外部直接调用(框架的规范)。 ctrl.mouse_click()
:有些日期输入框是 "只读" 的,必须点击后才能输入,这一步确保输入框处于可编辑状态。selectAll()
+clear()
:比如输入框里原本有 "2024-09-02",直接输入 "2021-06-01" 可能变成 "2021-06-012024-09-02"(拼接错误),全选清空后再输入才能保证正确。assert
断言:这是测试框架的 "校验点",确保输入的日期真的生效了(比如不会因为网络卡了导致输入失败)。如果失败,会直接报错,方便排查问题。
三、text()
方法:复用父类功能
Matlab
def text(self):
return super().text() # 调用父类InputCtrlBase的text()方法
作用 :text()
方法通常用于 "获取控件当前显示的文本"(比如获取输入框里的日期)。这里直接用父类的实现,因为日期输入框的 "获取文本" 逻辑和普通输入框一样,没必要重复写。super()
表示 "调用父类的方法"。
6.LowCodeVoucherDateCtrl(输入控件)
是一个处理 "凭证日期选择" 的高级控件 ,专门用于在低代码平台的凭证相关模块(如财务凭证、业务单据)中设置日期。它比之前的LowCodeDateRangeCtrl
功能更丰富,支持 "快捷日期选择"(如本月、上月)、"自定义日期范围" 和 "更多日期选项"(如昨天、近 7 天)。
一、基础信息:控件定位和继承关系
- 继承Element
Base
:拥有输入控件的通用功能(如find_child
查找子元素、is_element_exist
判断元素是否存在)。 - 三个定位器分别对应不同的日期选择入口:
customerDateLoc
:用于打开 "自定义日期范围" 弹窗;rangeDateLoc
:用于选择常用快捷日期(如 "本月");moreDateLoc
:用于选择 "更多" 菜单下的日期(如 "昨天")。
二、核心方法input()
:根据不同输入值选择日期
这个方法是控件的 "大脑",根据传入的value
类型(字符串、字典、列表),自动选择对应的日期设置方式:
Matlab
def input(self, value):
"""
:param value: 支持三种格式:
1. 字符串:如"上月"、"本月"(选择常用快捷日期);
2. 字典:如{"start":"2021-06-01","end":"2021-06-02"}(自定义日期范围);
3. 列表:如['更多','昨天'](从"更多"菜单选择具体日期)。
:return: 无返回值
"""
# 步骤1:判断是否是"自定义日期范围"(字典或合法日期字符串)
# DateUtil.is_valid_date(value):判断value是否是合法日期(如"2021-06-01")
bDate = DateUtil.is_valid_date(value)
if bDate or isinstance(value, dict):
# 点击"自定义日期"选项,打开日期范围输入弹窗
self.find_child(self.customerDateLoc).click()
# 复用LowCodeDateRangeCtrl控件,调用其input方法设置日期范围
dateRangeCtrl = LowCodeDateRangeCtrl.instance()
dateRangeCtrl.input(value)
# 步骤2:判断是否是"更多菜单中的日期"(列表格式,如['更多','昨天'])
elif isinstance(value, list):
# 先判断页面上是否有"本月"选项(兼容不同页面的菜单差异)
if self.is_element_exist(self.rangeDateLoc.format_key('本月')):
# 如果有"本月",先点击它(可能是为了展开菜单)
self.find_child(self.rangeDateLoc.format_key('本月')).click()
else:
# 如果没有"本月",就点击"今天"(作为备选展开菜单的方式)
self.find_child(self.rangeDateLoc.format_key('今天')).click()
# 点击列表中的第一个元素(通常是"更多"),展开更多日期选项
self.find_child(self.rangeDateLoc.format_key(value[0])).click()
# 点击列表中的第二个元素(具体日期,如"昨天")
self.find_global(self.moreDateLoc.format_key(value[1])).click()
# 步骤3:其他情况(字符串格式的常用日期,如"上月")
else:
# 直接点击对应的快捷日期选项(如"上月")
self.find_child(self.rangeDateLoc.format_key(value)).click()
三种输入格式的场景举例:
-
当
value = "本月"
(字符串):直接点击页面上的 "本月" 选项,快速筛选本月的凭证。
-
当
value = {"start":"2024-01-01", "end":"2024-01-31"}
(字典):先点击 "自定义日期",弹出日期范围输入框,再调用
LowCodeDateRangeCtrl
输入具体的开始和结束日期。 -
当
value = ["更多", "昨天"]
(列表):先点击 "更多" 展开子菜单,再选择 "昨天",筛选昨天的凭证。
三、辅助方法:控件识别和文本获取
Matlab
def text(self):
return "" # 此控件不需要返回文本,所以返回空字符串
@classmethod
def is_this_ctrl(cls, ctrl_html):
"""根据控件的HTML判断是否是当前控件"""
# 如果HTML中包含"evo-filter-convenient-date"(快捷日期选择),则认为是此控件
if 'evo-filter-convenient-date' in ctrl_html:
return True
else:
return False
def is_self_verify(self):
return True # 表示此控件支持自我校验(框架可能会用这个方法判断是否需要自动校验)
7.VoucherDateCtrl(输入控件)
是用于处理凭证日期选择 的功能控件,和你之前看到的LowCodeVoucherDateCtrl
类似,但适配了不同的页面样式和业务场景(比如传统代码实现的凭证模块,而非低代码平台)。
一、基础信息:定位规则与继承
- 与
LowCodeVoucherDateCtrl
相比,customerDateLoc
的定位规则更复杂(用|
分隔两种 XPATH),因为它需要适配不同页面中 "自定义日期" 按钮的两种样式(一种是带customButtonContainer
类的按钮,另一种是文本为 "自定义" 的选项)。 - 依赖的日期范围控件是
DateRangeCtrl
,而非低代码版本的LowCodeDateRangeCtrl
,说明它服务于传统开发的业务模块。
二、核心方法input()
:根据输入值选择日期
和之前的控件类似,input()
方法根据value
的类型(字符串、字典、列表)选择不同的日期设置逻辑,但增加了对特殊页面场景的兼容:
Matlab
def input(self, value):
"""
:param value: 支持三种格式:
1. 字符串:如"上月"、"本月"(常用快捷日期);
2. 字典:如{"start":"2021-06-01","end":"2021-06-02"}(自定义日期范围);
3. 列表:如['更多','昨天'](从"更多"菜单选择)。
:return: 无返回值
"""
# 步骤1:判断是否为"自定义日期范围"(合法日期字符串或字典)
bDate = DateUtil.is_valid_date(value) # 检查是否为合法日期(如"2021-06-01")
if bDate or isinstance(value, dict):
time.sleep(1) # 等待1秒,确保页面加载完成
# 点击"自定义日期"选项,打开日期输入弹窗
self.find_child(self.customerDateLoc).click()
# 复用DateRangeCtrl控件设置具体日期范围
dateRangeCtrl = DateRangeCtrl.instance()
dateRangeCtrl.input(value)
# 步骤2:判断是否为"更多菜单中的日期"(列表格式)
elif isinstance(value, list):
# 特殊场景处理:如果页面上没有"自定义日期"选项(说明是另一种日期控件样式)
if self.is_element_exist(self.customerDateLoc) is False:
# 改用BetweenInputCtrl控件处理(适配这种特殊样式)
ctrl = BetweenInputCtrl.instance(self.parent)
ctrl.key = self.key # 传递关键信息(如控件标识)
ctrl.input(value) # 用区间输入控件完成日期设置
return # 处理完成后直接返回,不执行后续逻辑
# 常规场景:展开日期菜单(优先点"本月",没有则点"今天")
if self.is_element_exist(self.rangeDateLoc.format_key('本月')):
self.find_child(self.rangeDateLoc.format_key('本月')).click()
else:
self.find_child(self.rangeDateLoc.format_key('今天')).click()
# 点击列表第一个元素(通常是"更多"),展开子菜单
self.find_child(self.rangeDateLoc.format_key(value[0])).click()
# 点击列表第二个元素(具体日期,如"昨天")
self.find_global(self.moreDateLoc.format_key(value[1])).click()
# 步骤3:其他情况(字符串格式的常用日期,如"上月")
else:
# 直接点击对应的快捷日期选项
self.find_child(self.rangeDateLoc.format_key(value)).click()
- 增加了对 "无自定义日期选项" 场景的兼容:当页面上没有
customerDateLoc
元素时,自动切换到BetweenInputCtrl
控件处理,避免在不同样式的页面上失效(这是框架 "兼容性" 设计的体现)。 - 增加了
time.sleep(1)
:在点击 "自定义日期" 前等待 1 秒,解决部分页面加载较慢导致的点击失效问题(临时解决方案,注释中TODO
提到需要优化为更稳定的校验重输机制)。
三、控件识别方法is_this_ctrl
Matlab
@classmethod
def is_this_ctrl(cls, ctrl_html):
"""根据控件的HTML判断是否是当前控件"""
# 两种识别条件(满足其一即可):
# 1. HTML包含"filter-convenient-date"且不包含"date-un-active"(活跃的日期控件)
# 2. HTML包含"filter-form_masterVoucherId_bizDate"(凭证主表业务日期的特征标识)
if ('filter-convenient-date' in ctrl_html and 'date-un-active' not in ctrl_html) or 'filter-form_masterVoucherId_bizDate' in ctrl_html:
return True
else:
return False
- 比低代码版本的识别规则更具体,增加了
filter-form_masterVoucherId_bizDate
这个特征(直接关联 "凭证主表的业务日期"),说明这个控件专门服务于 "凭证模块",识别更精准。 - 排除了包含
date-un-active
的元素(非活跃状态的控件),避免误识别不可操作的日期控件。
四、其他辅助方法
Matlab
def text(self):
return "" # 不需要返回文本,返回空字符串
def is_self_verify(self):
return True # 支持自我校验(框架可自动触发输入后的校验)
五、与LowCodeVoucherDateCtrl
的对比
差异点 | VoucherDateCtrl |
LowCodeVoucherDateCtrl |
---|---|---|
适用场景 | 传统代码开发的凭证模块 | 低代码平台开发的模块 |
日期范围控件依赖 | DateRangeCtrl (传统版本) |
LowCodeDateRangeCtrl (低代码版本) |
自定义日期定位 | 适配两种样式(customButtonContainer 或 "自定义" 文本) |
适配低代码平台的统一样式 |
特殊场景兼容 | 处理 "无自定义日期选项" 的情况 | 未处理此类场景(低代码平台样式更统一) |
控件识别规则 | 关联凭证主表标识(form_masterVoucherId ) |
依赖低代码平台的特征类(evo-filter ) |
8.DateRangeCtrl(输入类控件)
核心在于占位符的类型不同 :DateRangeCtrl
中使用了命名占位符{num}
,而低代码版本用的是匿名占位符{}
,所以传参方式必须对应调整。我们具体分析:
定位器的差异
Matlab
dateInputLoc = Locator(
By.XPATH,
# 注意这里的占位符是命名的:{num}
'//*[@class="rangePickerWrapContainer" or contains(@class,"nova-ui-DatePickerInput")]//span[@class="RangePickerInput"]/input[{num}] | (//div[contains(@class,"ant-picker-input")]/input)[{num}]',
desc='日期输入框'
)
而低代码版本LowCodeDateRangeCtrl
的定位器是:
Matlab
dateInputLoc = Locator(
By.XPATH,
# 这里的占位符是匿名的:{}
'(//div[@class="ant-popover-content"]//div[contains(@class,"ant-picker-input")]/input)[{}]',
desc='日期输入框'
)
关键区别:
- 低代码版本用的是匿名占位符
{}
(只需要按顺序传值); - 这个
DateRangeCtrl
用的是命名占位符{num}
(需要指定 "num" 这个名字对应的值)。
占位符类型 | 例子 | format_key 传参方式 |
替换逻辑 |
---|---|---|---|
匿名占位符 | (//input)[{}] |
直接传值(如"1" 、"2" ) |
按顺序替换{} |
命名占位符 | (//input)[{num}] |
传字典(如{"num": "1"} ) |
按键名num 替换{num} |
DateRangeCtrl
用{num}
命名占位符,是为了让定位器的逻辑更清晰(明确知道这个数字是 "输入框序号"),同时兼容更复杂的定位规则(比如一个定位器中需要多个不同的占位符,如{type}{num}
)。这也是框架在处理多样化定位场景时的灵活设计。
9.LockWareHouseCtrl(输入控件)
是一个处理 "仓库锁定 / 解锁" 操作的控件,专门用于在系统中切换仓库的锁定状态(比如防止误操作、控制仓库权限等场景)。
一、基础信息:定位规则与继承
Matlab
# 导入输入控件基类:提供元素查找等基础能力
from frameworkCore.business.InputCtrlBase import InputCtrlBase
# 导入控件注册工具:(当前注释了@register_comp,可能暂未注册到框架)
from script.common.CtrlManager import register_comp
# 导入定位工具:定义如何找到锁定按钮和状态图标
from frameworkCore.driver.locator import Locator
from frameworkCore.driver.by import By
# 导入键盘按键工具:(当前代码未使用,预留扩展)
from frameworkCore.driver.Keys import Keys
# 注释了注册装饰器,说明这个控件可能还在测试阶段或仅内部使用
#@register_comp
class LockWareHouseCtrl(InputCtrlBase):
# 定位"锁定/解锁仓库"按钮(包含特定class的button)
lockWareHouseLoc = Locator(By.XPATH,'.//button[contains(@class,"lock-warehouse")]')
# 定位状态图标(根据"锁定仓库"或"解锁仓库"文本找到其父按钮中的i标签,通常是图标)
statueLoc = Locator(By.XPATH, '//span[text()="锁定仓库" or text()="解锁仓库"]//parent::button//i')
- 继承Element
Base
:拥有find_child
(查找子元素)等通用方法。 - 两个核心定位器:
lockWareHouseLoc
:定位操作按钮(点击可切换锁定状态);statueLoc
:定位状态图标(用于判断当前仓库是锁定还是解锁状态)。
二、核心方法input()
:执行锁定 / 解锁操作
Matlab
def input(self, data):
# data是布尔值:True表示要"锁定仓库",False表示要"解锁仓库"
if not self.get_warehouse_status() and data == True:
# 情况1:当前仓库未锁(get_warehouse_status返回False),且需要锁定(data=True)
self.find_child(self.lockWareHouseLoc).click() # 点击锁定按钮
assert self.get_warehouse_status() == True # 校验是否成功锁定
elif self.get_warehouse_status() and data == False:
# 情况2:当前仓库已锁(get_warehouse_status返回True),且需要解锁(data=False)
self.find_child(self.lockWareHouseLoc).click() # 点击解锁按钮
assert self.get_warehouse_status() == False # 校验是否成功解锁
逻辑拆解 :
这个方法的核心是 "根据当前状态和目标状态,执行对应操作并校验结果",避免无效操作(比如仓库已锁时重复点击锁定按钮)。
- 举例 1:如果仓库当前是 "未锁" 状态(
get_warehouse_status()
返回False
),且传入data=True
(要锁定),则点击按钮并检查是否成功锁定。 - 举例 2:如果仓库当前是 "已锁" 状态(返回
True
),且传入data=False
(要解锁),则点击按钮并检查是否成功解锁。 - 其他情况(如状态与目标一致):不执行任何操作(比如已锁时传入
True
,无需处理)。
三、辅助方法get_warehouse_status()
:判断仓库当前状态
Matlab
def get_warehouse_status(self):
"""
返回True代表仓库已锁
返回False代表仓库未锁
"""
# 找到状态图标元素(通常是锁的图标,用i标签表示)
element = self.find_child(self.statueLoc)
# 获取图标的class属性(前端通常用class区分不同状态的图标,比如locked/unlocked)
status = element.getAttribute('class')
# 根据class中的关键词判断状态:
if 'unlocked' in status: # 包含unlocked(未锁定)关键词
return False
elif 'locked' in status: # 包含locked(已锁定)关键词
return True
设计思路 :
前端页面中,"锁定" 和 "解锁" 状态通常用不同的图标表示(比如一把锁的闭合 / 打开状态),而这些图标会通过class
属性区分(如class="icon locked"
或class="icon unlocked"
)。
这个方法通过读取图标元素的class
属性,判断当前状态,为input()
方法的操作逻辑提供依据。
四、控件的业务意义
在仓库管理系统中,"锁定仓库" 是一个重要操作,通常用于:
- 防止误操作:比如盘点仓库时,锁定后不允许出入库;
- 权限控制:只有管理员可以解锁仓库进行操作;
- 流程控制:某些业务流程(如月末结账)要求仓库必须处于锁定状态。
这个控件将 "判断状态→执行操作→校验结果" 的流程封装起来,测试用例只需调用input(True)
或input(False)
,就能完成锁定 / 解锁并确保操作有效,无需关心底层的元素定位和状态判断细节。