【日常学习】2025-9-2 学习控件Day1

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.关闭窗口()  # 关闭弹窗,避免影响后续操作

这是整个控件的 "核心功能区",完整描述了 "上传附件" 的流程:

  1. 借助SelectFileCtrl完成 "选文件→上传" 的底层操作(相当于 "让助手去做具体的体力活");
  2. 上传后检查弹窗,确认成功并关闭(相当于 "检查结果,清理现场");
  3. @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
  1. 支持多文件上传 :通过判断file是否为列表,实现 "单个文件" 和 "多个文件" 的兼容(比如一次上传多个发票图片)。
  2. 定位并操作原生上传控件
    • 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:这是定位页面上 "日期输入框" 的 "地图"。{}是占位符,后续会替换成12,分别定位 "开始日期输入框" 和 "结束日期输入框"(比如很多系统的日期范围组件是两个输入框并排)。

一、核心方法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 天)。

一、基础信息:控件定位和继承关系

  • 继承ElementBase:拥有输入控件的通用功能(如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()

三种输入格式的场景举例

  1. value = "本月"(字符串):

    直接点击页面上的 "本月" 选项,快速筛选本月的凭证。

  2. value = {"start":"2024-01-01", "end":"2024-01-31"}(字典):

    先点击 "自定义日期",弹出日期范围输入框,再调用LowCodeDateRangeCtrl输入具体的开始和结束日期。

  3. 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')
  • 继承ElementBase:拥有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),就能完成锁定 / 解锁并确保操作有效,无需关心底层的元素定位和状态判断细节。

相关推荐
永日456706 小时前
学习日记-SpringMVC-day48-9.2
学习
悠哉悠哉愿意6 小时前
【机器学习学习笔记】Matplotlib 基本操作
笔记·学习·机器学习
胡萝卜3.07 小时前
【LeetCode&数据结构】栈和队列的应用
数据结构·学习·算法··队列·栈和队列oj题
mengjiexu_cn7 小时前
强化学习PPO/DDPG算法学习记录
python·学习·算法
人生游戏牛马NPC1号8 小时前
学习 Android (十八) 学习 OpenCV (三)
android·opencv·学习
咸甜适中8 小时前
rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(二十三)控件中常用文本格式
笔记·学习·rust·egui
茯苓gao8 小时前
变频器实习DAY41 单元测试介绍
笔记·学习·单元测试
mysla11 小时前
嵌入式学习day40-硬件(1)
学习
三天不学习11 小时前
从零开始学习C#上位机开发学习进阶路线,窥探工业自动化和物联网应用
学习·c#·自动化