【日常学习】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),就能完成锁定 / 解锁并确保操作有效,无需关心底层的元素定位和状态判断细节。

相关推荐
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码4 天前
嵌入式学习路线
学习
毛小茛4 天前
计算机系统概论——校验码
学习
babe小鑫4 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms4 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下4 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。4 天前
2026.2.25监控学习
学习
im_AMBER4 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J4 天前
从“Hello World“ 开始 C++
c语言·c++·学习