kdtest-playwright 2.0.4
KDTest Playwright Edition - 关键字驱动测试框架，专为 Element Plus/Vue3 优化
License: MIT
Requires Python: >=3.8
Keywords: testing, automation, playwright, keyword-driven, element-plus, vue3

# KDTest-Playwright

基于 Playwright 的关键字驱动测试框架，专为 Element Plus/Vue3 优化，兼容原版 kdtest 插件。

## 目录

- [安装](#安装)
- [快速开始](#快速开始)
- [配置文件](#配置文件)
- [测试用例格式](#测试用例格式)
- [CLI 命令](#cli-命令)
- [核心关键字](#核心关键字)
- [变量与内置函数](#变量与内置函数)
- [插件系统](#插件系统)
- [与原版 kdtest 的兼容性](#与原版-kdtest-的兼容性)
- [高级用法](#高级用法)

## 安装

```bash
# 从 PyPI 安装
pip install kdtest-pw

# 安装 Playwright 浏览器
playwright install chromium
```

## 快速开始

### 1. 创建配置文件 `parameters.json`

```json
{
    "testCaseFile": [
        {
            "caseFilePath": "testCases.xlsx",
            "caseItem": ["Sheet1"]
        }
    ],
    "browser": "chromium",
    "url": "http://localhost:8080",
    "headless": false,
    "timeout": 30000
}
```

### 2. 创建 Excel 测试用例

| 用例ID | 用例名称 | 描述 | 关键字 | 定位方式 | 定位表达式 | 操作值 |
|--------|----------|------|--------|----------|------------|--------|
| TC001 | 登录测试 | | | | | |
| | | 打开网站 | driver_get | | | http://localhost |
| | | 输入用户名 | input_text | xpath | //input[@placeholder='账号'] | admin |
| | | 输入密码 | input_text | css | input[type='password'] | 123456 |
| | | 点击登录 | click_btn | css | .el-button--primary | |
| | | 验证登录 | text_assert | css | .user-name | admin |

### 3. 运行测试

```bash
# 命令行运行
kdtest-pw -c parameters.json

# 或在 Python 中使用
from kdtest_pw.cli import KDTestRunner

runner = KDTestRunner("parameters.json")
runner.setup()
runner.run()
runner.teardown()
```

## 配置文件

### parameters.json 完整配置

```json
{
    "testCaseFile": [
        {
            "caseFilePath": "testCases.xlsx",
            "caseItem": ["Sheet1", "Sheet2"]
        }
    ],
    "browser": "chromium",
    "url": "http://localhost:8080",
    "headless": false,
    "timeout": 30000,
    "slowMo": 0,
    "viewport": {
        "width": 1920,
        "height": 1080
    },
    "video": false,
    "screenshot": "only-on-failure",
    "trace": false,
    "reportDir": "./reports"
}
```

### 配置项说明

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `testCaseFile` | Array | 必填 | 测试用例文件列表 |
| `browser` | String | "chromium" | 浏览器类型：chromium/firefox/webkit |
| `url` | String | - | 初始 URL |
| `headless` | Boolean | false | 无头模式 |
| `timeout` | Number | 30000 | 全局超时时间(ms) |
| `slowMo` | Number | 0 | 操作延迟(ms)，用于调试 |
| `viewport` | Object | {1920, 1080} | 视口大小 |
| `video` | Boolean | false | 是否录制视频 |
| `screenshot` | String | "only-on-failure" | 截图策略 |
| `trace` | Boolean | false | 是否启用 Trace |
| `reportDir` | String | "./reports" | 报告输出目录 |

## 测试用例格式

### Excel 列定义

| 列名 | 说明 | 示例 |
|------|------|------|
| 用例ID | 测试用例唯一标识 | TC001 |
| 用例名称 | 测试用例名称 | 登录测试 |
| 描述 | 步骤描述（可选） | 输入用户名 |
| 关键字 | 要执行的关键字 | input_text |
| 定位方式 | 元素定位方式 | xpath/css/id/name |
| 定位表达式 | 元素定位表达式 | //input[@id='username'] |
| 操作值 | 关键字参数 | admin |

### 定位方式

| 定位方式 | 说明 | 示例 |
|----------|------|------|
| `xpath` | XPath 表达式 | //div[@class='btn'] |
| `css` | CSS 选择器 | div.btn, #submit |
| `id` | 元素 ID | username |
| `name` | 元素 name 属性 | password |
| `text` | 文本内容 | 登录 |
| `role` | ARIA 角色 | button |

## CLI 命令

### 运行测试

```bash
# 基本运行
kdtest-pw -c parameters.json

# 指定浏览器
kdtest-pw -c parameters.json --browser firefox

# 无头模式
kdtest-pw -c parameters.json --headless

# 慢速模式（调试用）
kdtest-pw -c parameters.json --slow-mo 500
```

### 插件管理

```bash
# 查看已安装插件
kdtest-pw --unit_show

# 创建新插件
kdtest-pw --unit_new MyPlugin

# 安装插件
kdtest-pw --unit_install /path/to/plugin

# 卸载插件
kdtest-pw --unit_uninstall MyPlugin
```

### 其他命令

```bash
# 查看帮助
kdtest-pw --help

# 查看版本
kdtest-pw --version
```

## 核心关键字

### 浏览器操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `driver_get` | 打开 URL | URL 地址 |
| `driver_back` | 后退 | - |
| `driver_forward` | 前进 | - |
| `driver_refresh` | 刷新页面 | - |
| `driver_close` | 关闭页面 | - |
| `driver_quit` | 退出浏览器 | - |

### 输入操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `input_text` | 清除并输入文本 | 要输入的文本 |
| `input_append` | 追加输入文本 | 要追加的文本 |
| `input_clear` | 清除输入框 | - |
| `keyBoard_Events` | 键盘事件 | Keys.ENTER, Keys.TAB 等 |

### 点击操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `click_btn` | 点击元素 | - |
| `double_click` | 双击元素 | - |
| `right_click` | 右键点击 | - |
| `hover` | 鼠标悬停 | - |
| `force_click` | 强制点击（跳过可见性检查） | - |

### 等待操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `sleep` | 固定等待 | 秒数 |
| `wait_visible` | 等待元素可见 | 超时时间(ms) |
| `wait_hidden` | 等待元素隐藏 | 超时时间(ms) |
| `wait_enabled` | 等待元素可用 | 超时时间(ms) |

### 断言操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `text_assert` | 断言元素文本 | 预期文本 |
| `text_contains_assert` | 断言文本包含 | 预期包含的文本 |
| `title_assert` | 断言页面标题 | 预期标题 |
| `url_assert` | 断言页面 URL | 预期 URL |
| `element_visible_assert` | 断言元素可见 | true/false |
| `element_count_assert` | 断言元素数量 | 预期数量 |
| `attribute_assert` | 断言元素属性 | 属性名=预期值 |

### 数据操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `get_text` | 获取元素文本并存储 | 变量名 |
| `get_attribute` | 获取元素属性并存储 | 属性名,变量名 |
| `set_variable` | 设置变量 | 变量名=值 |
| `cache_data` | 缓存数据 | 键=值 |

### Element Plus 组件

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `el_select` | 下拉选择 | 选项文本 |
| `el_select_multiple` | 多选下拉 | 选项1,选项2 |
| `el_datepicker` | 日期选择 | 2024-01-01 |
| `el_daterange` | 日期范围 | 2024-01-01,2024-12-31 |
| `el_timepicker` | 时间选择 | 12:30:00 |
| `el_dialog_confirm` | 对话框确认 | - |
| `el_dialog_cancel` | 对话框取消 | - |
| `el_message_close` | 关闭消息提示 | - |
| `el_table_click_row` | 点击表格行 | 行索引(从0开始) |
| `el_table_click_cell` | 点击表格单元格 | 行索引,列索引 |
| `el_table_get_cell` | 获取单元格文本 | 行索引,列索引,变量名 |
| `el_tree_click_node` | 点击树节点 | 节点文本 |
| `el_tree_expand` | 展开树节点 | 节点文本 |
| `el_cascader` | 级联选择 | 一级/二级/三级 |
| `el_upload` | 文件上传 | 文件路径 |
| `el_switch` | 开关切换 | on/off |
| `el_checkbox` | 复选框 | check/uncheck |
| `el_radio` | 单选框 | 选项文本 |
| `el_slider` | 滑块 | 数值 |
| `el_form_input` | 表单项输入 | label=值 |
| `el_form_select` | 表单项选择 | label=选项 |
| `el_pagination_goto` | 分页跳转 | 页码 |
| `el_tab_click` | 切换标签页 | 标签名 |

### 菜单导航

**方式一：基于配置文件（适用于复杂系统）**

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `menu_load_data` | 加载菜单配置 | modularInformation.json 路径 |
| `menu_navigate` | 导航到菜单 | 一级菜单/二级菜单/三级菜单 |
| `menu_click_first` | 点击一级菜单 | 菜单名称 |
| `menu_click_second` | 点击二级菜单 | 一级/二级 |
| `menu_click_third` | 点击三级菜单 | 一级/二级/三级 |
| `menu_switch_iframe` | 切换到 iframe | 一级/二级 |
| `menu_exit_iframe` | 退出 iframe | - |

**方式二：基于文本直接定位（适用于 Element Plus 标准菜单）**

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `el_menu_navigate` | 导航到菜单（无需配置） | 一级菜单/二级菜单 |
| `el_menu_click` | 点击菜单项 | 菜单项文本 |
| `el_submenu_expand` | 展开子菜单 | 子菜单文本 |
| `el_submenu_collapse` | 收起子菜单 | 子菜单文本 |
| `el_tabs_click` | 点击标签页 | 标签页文本 |

**示例：**
```
# 方式一：使用配置文件
menu_load_data | | | ./menuConfig.json
menu_navigate | | | 系统管理/用户管理

# 方式二：直接文本定位（推荐用于 Element Plus）
el_menu_navigate | | | 系统管理/用户管理
el_tabs_click | | | 待处理
```

### Frame 操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `switch_to_frame` | 切换到 frame | frame 名称/索引/定位表达式 |
| `switch_to_parent` | 切换到父 frame | - |
| `switch_to_main` | 切换到主文档 | - |

### 窗口操作

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `switch_to_window` | 切换窗口 | 窗口标题或索引 |
| `new_tab` | 新建标签页 | URL(可选) |
| `close_tab` | 关闭当前标签页 | - |

### JavaScript 执行

| 关键字 | 说明 | 操作值 |
|--------|------|--------|
| `execute_js` | 执行 JavaScript | JS 代码 |
| `execute_js_on_element` | 在元素上执行 JS | JS 代码 |

## 变量与内置函数

### 变量引用

```
${varName}           # 从缓存获取变量
${self.param}        # 从自定义参数获取
${env.NAME}          # 从环境变量获取
${case.field}        # 从当前用例数据获取
```

### 内置函数

| 函数 | 说明 | 示例 |
|------|------|------|
| `@today()` | 今天日期 | 2024-01-15 |
| `@today(+7)` | 7天后 | 2024-01-22 |
| `@today(-30)` | 30天前 | 2023-12-16 |
| `@now()` | 当前时间 | 2024-01-15 14:30:00 |
| `@now(%H:%M)` | 格式化时间 | 14:30 |
| `@timestamp()` | 时间戳 | 1705301400 |
| `@random(1,100)` | 随机整数 | 42 |
| `@random_str(8)` | 随机字符串 | aB3kF9mN |
| `@random_phone()` | 随机手机号 | 13812345678 |
| `@random_email()` | 随机邮箱 | abc123@test.com |
| `@random_name()` | 随机姓名 | 张三 |
| `@uuid()` | UUID | 550e8400-e29b... |
| `@uuid_short()` | 短 UUID | 550e8400 |

### 使用示例

```
# 在操作值中使用变量
${username}

# 使用内置函数
@today()
@random(1000,9999)

# 组合使用
用户_@random(100,999)
test_@timestamp()@random_str(4)
```

## 插件系统

### 插件目录结构

kdtest-pw 支持两种插件格式：

#### 1. kdtest-pw 原生格式

```
my_plugin/
├── __init__.py
├── plugin.py          # 继承 PluginBase
└── elements.yaml      # 元素数据（可选）
```

#### 2. 原版 kdtest 格式（兼容）

```
MyPlugin/
├── __init__.py
├── my.ini             # [Information] NAME, DETAILE, STATE
├── MyPlugin.py        # 主模块，类名与 NAME 一致
├── elementData/
│   ├── elementData.yaml
│   └── modularInformation.json
└── utils/
    └── tool.py
```

### 创建插件

```bash
# 使用 CLI 创建
kdtest-pw --unit_new MyPlugin
```

生成的结构：

```
MyPlugin/
├── __init__.py
├── my.ini
├── MyPlugin.py
├── elementData/
│   ├── elementData.yaml
│   └── modularInformation.json
└── utils/
    └── tool.py
```

### my.ini 配置

```ini
[Information]
NAME = MyPlugin
DETAILE = "插件描述"
STATE = finish
```

- `NAME`: 插件名称（与主模块类名一致）
- `DETAILE`: 插件描述
- `STATE`: 状态（finish=启用, disabled=禁用）

### 插件主模块示例

```python
# MyPlugin.py
class MyPlugin:
    """插件主类"""

    def __init__(self):
        self.page = None  # Playwright Page 将被自动注入

    def my_keyword(self, targeting, element, content=None):
        """自定义关键字

        Args:
            targeting: 定位方式
            element: 定位表达式
            content: 操作值
        """
        if self.page:
            locator = self.page.locator(f'{targeting}={element}')
            locator.click()
            if content:
                locator.fill(content)

    def another_keyword(self, content):
        """另一个关键字"""
        print(f"执行: {content}")
        return content
```

### 元素数据格式

```yaml
# elementData/elementData.yaml
login:
  username:
    - xpath
    - //input[@id='username']
  password:
    - css
    - input[type='password']
  submit:
    - xpath
    - //button[@type='submit']

dashboard:
  menu:
    - css
    - .el-menu
  user_info:
    - id
    - userInfo
```

### 插件管理命令

```bash
# 查看已安装插件
kdtest-pw --unit_show

# 安装插件（从目录）
kdtest-pw --unit_install /path/to/MyPlugin

# 卸载插件
kdtest-pw --unit_uninstall MyPlugin

# 创建新插件模板
kdtest-pw --unit_new NewPlugin
```

## 与原版 kdtest 的兼容性

### 兼容的导入

原版 kdtest 插件中的以下导入会自动适配：

```python
from kdtest import GSTORE, INFO, reset_implicitlyWait
from kdtest import GSDSTORE, MODULEDATA, PRIVATEDATA, CASESDATA
```

### GSTORE 全局存储

```python
# 获取当前 page
page = GSTORE.get('page')

# 获取关键字对象
keyword = GSTORE.get('keyWord')

# 存储/获取数据
GSTORE['myData'] = value
data = GSTORE.get('myData')
```

### INFO 日志函数

```python
INFO("普通信息")
INFO("警告信息", "WARNING")
INFO("错误信息", "ERROR")
```

### reset_implicitlyWait 装饰器

```python
# 作为装饰器
@reset_implicitlyWait(1)
def my_method(self):
    pass

# 作为函数
reset_implicitlyWait(10)
```

### 主要差异

| 特性 | kdtest (Selenium) | kdtest-pw (Playwright) |
|------|-------------------|------------------------|
| 浏览器驱动 | 需要 WebDriver | 内置浏览器 |
| 等待机制 | 显式/隐式等待 | 自动等待 |
| driver 对象 | `self.driver` | `self.page` |
| 元素定位 | `find_element()` | `page.locator()` |
| Element Plus | 需要额外处理 | 原生支持 |
| 多标签页 | 需要切换句柄 | 原生支持 |
| iframe | `switch_to.frame()` | `frame_locator()` |
| 录制回放 | 无 | 支持 Trace |
| 视频录制 | 无 | 原生支持 |

### 迁移指南

1. **driver → page**
   ```python
   # Selenium
   self.driver.find_element(By.XPATH, '//button').click()

   # Playwright
   self.page.locator('xpath=//button').click()
   ```

2. **元素等待**
   ```python
   # Selenium
   WebDriverWait(driver, 10).until(EC.visibility_of_element_located(...))

   # Playwright (自动等待，无需显式等待)
   self.page.locator('...').click()
   ```

3. **iframe 切换**
   ```python
   # Selenium
   driver.switch_to.frame('frameName')

   # Playwright
   frame = self.page.frame_locator('#frameName')
   frame.locator('button').click()
   ```

## 高级用法

### 自定义报告

```python
from kdtest_pw.cli import KDTestRunner

runner = KDTestRunner("parameters.json")
runner.setup()
results = runner.run()

# 自定义报告处理
for case_result in results:
    print(f"用例: {case_result['name']}")
    print(f"状态: {case_result['status']}")
    print(f"耗时: {case_result['duration']}ms")
```

### 钩子函数

```python
from kdtest_pw.cli import KDTestRunner

class MyRunner(KDTestRunner):
    def on_case_start(self, case_info):
        """用例开始前"""
        print(f"开始执行: {case_info['name']}")

    def on_case_end(self, case_info, result):
        """用例结束后"""
        if result['status'] == 'failed':
            self.page.screenshot(path=f"error_{case_info['id']}.png")

    def on_step_error(self, step_info, error):
        """步骤出错时"""
        print(f"步骤失败: {step_info['keyword']} - {error}")
```

### 并行执行

```python
from kdtest_pw.cli import KDTestRunner
from concurrent.futures import ThreadPoolExecutor

def run_case_file(case_file):
    config = {
        "testCaseFile": [{"caseFilePath": case_file, "caseItem": ["Sheet1"]}],
        "browser": "chromium",
        "headless": True
    }
    runner = KDTestRunner(config)
    runner.setup()
    result = runner.run()
    runner.teardown()
    return result

# 并行执行多个用例文件
with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [
        executor.submit(run_case_file, f"cases_{i}.xlsx")
        for i in range(3)
    ]
    results = [f.result() for f in futures]
```

### Trace 调试

```json
{
    "trace": true,
    "traceDir": "./traces"
}
```

运行后使用 Playwright Trace Viewer 查看：

```bash
playwright show-trace traces/trace.zip
```

### 视频录制

```json
{
    "video": true,
    "videoDir": "./videos",
    "videoSize": {
        "width": 1280,
        "height": 720
    }
}
```

## 项目结构

```
kdtest_pw/
├── __init__.py
├── cli/
│   └── run.py              # CLI 入口和 KDTestRunner
├── compat/
│   └── kdtest_compat.py    # 原版 kdtest 兼容层
├── keywords/
│   ├── browser.py          # 浏览器操作关键字
│   ├── element.py          # 元素操作关键字
│   ├── assertion.py        # 断言关键字
│   └── element_plus.py     # Element Plus 关键字
├── plugins/
│   ├── plugin_base.py      # 插件基类
│   ├── plugin_loader.py    # 插件加载器
│   ├── legacy_adapter.py   # 原版插件适配器
│   ├── element_plus_plugin/# 内置 Element Plus 插件
│   └── OA_JavaWebUI/       # 用户插件示例
├── reference.py            # 全局引用（GSTORE, INFO 等）
└── utils/
    ├── excel_reader.py     # Excel 读取
    ├── variable_parser.py  # 变量解析
    └── report_generator.py # 报告生成
```

## License

MIT