Agent Skills › DanielSuo117/velocitai

DanielSuo117/velocitai

GitHub

为已有 PageObject 增量添加回归测试点。通过访问真实页面提取定位符,更新页面对象类、角色回归测试用例及文档,确保新增交互覆盖且不影响原有结构。

27 skills 178

Install All Skills

npx skills add DanielSuo117/velocitai --all -g -y
More Options

List skills in collection

npx skills add DanielSuo117/velocitai --list

Skills in Collection (27)

为已有 PageObject 增量添加回归测试点。通过访问真实页面提取定位符,更新页面对象类、角色回归测试用例及文档,确保新增交互覆盖且不影响原有结构。
新增测试点 添加回归点 add test point 增加覆盖 扩展页面方法
skills/add-regression-point/SKILL.md
npx skills add DanielSuo117/velocitai --skill add-regression-point -g -y
SKILL.md
Frontmatter
{
    "name": "add-regression-point",
    "description": "为已有 PageObject 增量添加回归测试点。触发:新增测试点、添加回归点、add test point、增加覆盖、扩展页面方法。"
}

Add Regression Point — 新增回归测试点

适用场景

已有 PageObject 新增单个或多个回归测试点。与 gen-page-test(新建整个页面)不同,本 skill 针对已存在的页面做增量扩展

输入参数

参数 必填 说明 示例
目标 PageObject 已有的页面类名 <ExistingPage>
页面 URL 真实可访问的页面 URL(已认证态) https://example.com/xxx
新增测试点 需要覆盖的交互元素 点击收藏、筛选类型

执行流程(4 步)

Step 1:访问真实页面抽取新元素定位符

使用 agent-browser 以已认证态访问目标 URL,为每个新增测试点按 locator-replacer 六级优先级选定定位符。浏览器工具选型遵循 agent-behavior P0.4:DOM 探索 / 定位符采集用 agent-browser,agent-browser 未安装时降级到 Playwright MCP。

Step 2:更新 PageObject

pages/<page_name>_page.py 中追加:

2a. 新增定位符常量(插入到已有常量之后、方法之前)

    # ↓ 新增定位符(来自真实页面,<日期>)
    <NEW_LOCATOR_1> = "<真实定位符>"    # P<0-5>: 说明
    <NEW_LOCATOR_2> = "<真实定位符>"    # P<0-5>: 说明

2b. 新增业务方法(插入到 is_page_loaded() 之前)

    def click_<new_method_1>(self):
        self.click(self.<NEW_LOCATOR_1>)

    def click_<new_method_2>(self):
        self.click(self.<NEW_LOCATOR_2>)

    def is_page_loaded(self) -> bool:    # 保持为最后一个方法
        return self.is_visible(self.PAGE_IDENTIFIER)

Step 3:在对应角色回归测试中追加调用

根据目标 PageObject 的角色:

  • 对应角色页面 → tests/teacher/test_<feature>.py(当前无主流程用例,新建时继承 <Role>BaseTest
  • 对应角色页面 → tests/<role>/test_<role>_flow.py
  • 对应角色 → tests/<role>/test_<role>_flow.py

在对应 @allure.story 方法中追加:

    # ↓ 新增回归点
    <page_instance>.click_<new_method_1>()
    <page_instance>.click_<new_method_2>()

⚠️ 若新增测试点的跳转目标脱离门户布局(例如离开侧边导航的子路由、沉浸式页面),必须套用 case-round-trip:PageObject 提供 click_back_to_<landing>,用例末尾显式返回起点页并断言;否则后续用例复位 fixture 会超时。

Step 4:同步更新 docs/

  • 在对应角色的 pages-catalog 子文件中追加新方法行:
  • 在对应角色的 regression-points 子文件中追加回归点行 + 关键定位符常量:

检查清单

通用检查(命名/导入/is_page_loaded/allure)→ coding-conventions.md "新增测试检查清单"

  • 新增定位符均来自真实页面 DOM,行尾标注 P0~P5 级别
  • 对应角色 test_<role>_flow.py 已追加新方法调用
  • 对应角色的 docs/pages-catalog-<role>.md 已同步
  • 对应角色的 docs/regression-points-<role>.md 已同步
  • 若跳转目标脱离门户布局,已套用 case-round-trip

注意事项

  • 不要修改已有方法,只做增量添加
  • 不需要更新 pages/__init__.py(页面类已存在)
  • 如果新增测试点需要页面导航(跳转到子页面),考虑新建独立 PageObject(使用 gen-page-test
  • 定位符直接基于真实页面抽取,不使用占位符
提供POM分层架构、PageObject骨架及context共享决策树。指导新增角色时的基类设计,判断用例是否共享上下文,并支持同Class内跨用例的page接力模式,确保测试结构清晰与状态隔离。
架构 分层 POM Page Object 基类设计 新增角色
skills/architecture/SKILL.md
npx skills add DanielSuo117/velocitai --skill architecture -g -y
SKILL.md
Frontmatter
{
    "name": "architecture",
    "description": "POM 分层与 context 共享决策树。触发:架构、分层、POM、Page Object、基类设计、新增角色。"
}

架构决策 — POM 分层与 context 共享模式

适用场景

  • 新增角色/端时决定是否引入新基类
  • 判断某组用例要不要共享 page / context
  • 给新成员解释 POM 分层

POM 分层通用骨架

┌─────────────────────────────────────────────┐
│                  tests/ 测试层                │
│  按角色拆分文件:test_<role>_flow.py          │
│  每个测试类继承 Base(或 Role-Base)           │
├─────────────────────────────────────────────┤
│                  pages/ 页面对象层             │
│  Base ← Login ← Landing ← Detail / ...        │
│  通用 Role 用前缀命名:Role<Role><Page>Page   │
├─────────────────────────────────────────────┤
│                  config/ 配置层                │
│  URL、Token、浏览器参数按环境切换              │
├─────────────────────────────────────────────┤
│                  conftest.py Fixture 层        │
│  browser → context → page → Role-specific base │
└─────────────────────────────────────────────┘

数据流向configconftest(读取配置创建 fixture) → pages(接收 page 对象) → tests(组合页面对象执行断言)


PageObject 骨架

# 页面名称:<中文名>
from pages.base_page import BasePage


class <PageName>(BasePage):
    # 定位符常量(类顶部)
    <LOCATOR_CONSTANT> = "<selector>"   # P<0-5>: 说明
    PAGE_IDENTIFIER = "<selector>"      # 页面加载锚点

    # 业务方法(前缀:click_ / fill_ / select_ / get_xxx_text / wait_for_ / is_xxx_visible)
    def click_<action>(self):
        self.click(self.<LOCATOR_CONSTANT>)

    # 页面加载验证(必须实现,放在最后)
    def is_page_loaded(self) -> bool:
        return self.is_visible(self.PAGE_IDENTIFIER)

context 共享模式决策树

一组用例是否满足以下 3 条?
   1. 同一域名(不跨端)
   2. 有统一前置(登录、角色切换…)
   3. 有统一起点页面(可复位)
       │
       ├─ 是(三条全满足)
       │    └→ class 级共享 context
       │       - 用 scope="class" 的 page fixture(实现见 browser-config skill)
       │       - 专用基类:class setup 完成前置;function-scope autouse 复位起点
       │       - 用例末尾必须往返闭合(见 case-round-trip skill)
       │
       └─ 否(任一不满足)
            └→ function 级独立 context
               - 每用例独立 browser.new_context() + new_page()
               - 每用例独立登录

注意事项:

  • ❌ 禁止 session 级跨 class 共享 context(跨 class 职责边界不清)
  • ❌ 禁止同一 class 内跨域(会污染共享 context)
  • 详细规则见 rules/playwright/browser-context.md "浏览器上下文"段

同 class 内跨用例 page 接力模式

场景:同一 class 的多个用例形成线性流程链,后一个用例的起点是前一个用例的终点,不必从头登录。

test_a(搜索课程)
    ↓ 页面停留在搜索结果
test_b(点击课程 → 新 tab 打开课程首页)
    ↓ 新 tab 保存为 class 属性
test_c(在课程首页继续操作)
    ↓ 直接使用 class 属性中的 page

实现要点

  1. class-scope fixture 完成一次性登录:覆盖父类 _login_setup 为 no-op,用 class_page + request.cls.page 共享
  2. 用例间 page 接力:当用例打开新 tab 时,将新 page 保存到 class 属性(type(self).<attr> = new_page),后续用例通过 self.<attr> 访问
  3. 新 tab 不关闭:接力链中的 page 保持打开,直到 class 结束 context 自动销毁
  4. 用例顺序即流程顺序:pytest 默认按文件中方法出现顺序执行

骨架

class_page fixture 的实现见 browser-config "Fixture 分层模板"。

class TestSomeFlow(BaseTest):

    @pytest.fixture(autouse=True)
    def _login_setup(self):
        yield  # no-op,覆盖父类

    @pytest.fixture(scope="class", autouse=True)
    def _shared_setup(self, class_page, token, request):
        login_page = LoginPage(class_page)
        login_page.login_with_token(BASE_URL, token)
        request.cls.page = class_page

    def test_step_1(self):
        ...  # self.page 已由 _shared_setup 注入,class 级共享

    def test_step_2(self):
        new_page = some_page.click_open_new_tab()
        type(self).detail_page = new_page  # 保存到 class 属性供后续用例

    def test_step_3(self):
        detail = SomeDetailPage(self.detail_page)  # 直接接力
        ...

适用条件

  • ✅ 同一 class 内、用例具有明确的先后依赖关系
  • ✅ 新 tab 仍在同域或已携带认证(无需二次登录)
  • ❌ 如果用例间无依赖关系,不要用接力模式(改用独立 function-scope page)

新增 Role 的落地步骤

  1. 决策:用上面的决策树判断新 Role 的用例是否可共享 context
  2. PageObject:以 Role<Role><Page>Page 命名、role_<page> 文件名前缀,继承 BasePage
  3. 基类(若采用 class 级共享):新建 tests/<role>/<role>_base_test.py::<Role>BaseTest 继承 BaseTest,实现登录 + 切换 + 起点断言
  4. 用例:tests/<role>/test_<role>_flow.py::Test<Role>Flow 继承新基类
  5. 文档:在 docs/architecture.md 表中追加一行
  6. 目录:pages/__init__.py 追加新页面导出

决策落地参考

本项目当前的落地情况(具体类名、fixture、起点页面)→ docs/architecture.md

规范浏览器启动配置,将viewport、超时、无痕模式分层注入。禁止硬编码,确保viewport适配屏幕且导航与元素超时分离,提升测试稳定性与可维护性。
viewport 浏览器配置 导航超时 headless slow_mo incognito
skills/browser-config/SKILL.md
npx skills add DanielSuo117/velocitai --skill browser-config -g -y
SKILL.md
Frontmatter
{
    "name": "browser-config",
    "description": "浏览器 viewport \/ headless \/ 超时分层配置。触发:viewport、浏览器配置、导航超时、headless、slow_mo、incognito。"
}

浏览器启动配置 — viewport / 无痕 / 超时分离

核心原则

所有浏览器配置集中声明在配置文件,通过 fixture 分层注入,禁止在用例或 PageObject 中硬编码。


一、pytest 浏览器完整配置架构

配置分层

配置文件(常量声明)
  │
  ├─ browser fixture(session 级)
  │    └─ headless / slow_mo / incognito
  │
  ├─ page fixture(function 级)
  │    └─ viewport / timeout / navigation_timeout
  │
  └─ class_page fixture(class 级)
       └─ viewport / timeout / navigation_timeout

配置文件:所有常量集中声明

# 浏览器配置
HEADLESS = False              # True: 无头模式(CI 环境); False: 有头模式(本地调试)
SLOW_MO = 500                 # 每步操作间隔(ms),便于肉眼观察;CI 设为 0
DEFAULT_TIMEOUT = 15000       # 元素操作超时(ms)
DEFAULT_NAVIGATION_TIMEOUT = 15000  # 页面导航超时(ms)
VIEWPORT_WIDTH = 1280         # 视口宽度
VIEWPORT_HEIGHT = 900         # 视口高度

Fixture 分层模板

@pytest.fixture(scope="session")
def browser(playwright_instance):
    browser = playwright_instance.chromium.launch(
        headless=HEADLESS, slow_mo=SLOW_MO, args=["--incognito"],
    )
    yield browser
    browser.close()

@pytest.fixture                   # function 级:每个用例独立 context + page
def page(browser):
    context = browser.new_context(viewport={"width": VIEWPORT_WIDTH, "height": VIEWPORT_HEIGHT})
    page = context.new_page()
    page.set_default_timeout(DEFAULT_TIMEOUT)
    page.set_default_navigation_timeout(DEFAULT_NAVIGATION_TIMEOUT)
    yield page
    page.close(); context.close()

@pytest.fixture(scope="class")    # class 级:同 class 内共享 context + page
def class_page(browser):
    # 同 page fixture,scope="class"
    ...

关键约束viewport 在 context 层、timeout 在 page 层、--incognito 在 browser 层——三者不得混用层级。


二、Viewport 尺寸选择

核心约束:viewport 高度必须适配物理屏幕

viewport 高度不能超过物理屏幕可用高度(屏幕分辨率 − 浏览器工具栏 − 系统任务栏),否则浏览器窗口底部超出屏幕,页面底部内容被遮挡、无法交互。

正确策略:保持宽度,适当增加高度(不超出屏幕)

响应式页面等比放大 viewport 无效(可见比不变);应保持宽度,适当增加高度(不超出屏幕)。

方案 Viewport 效果
默认 1280x720 基准,较小
推荐 1280x900 适配 MacBook 屏幕,窗口不超出屏幕
外接显示器 1280x1080 适配 Full HD 显示器
过大(错误) 1280x1440 超出多数屏幕,底部被遮挡
等比放大(错误) 2560x1440 响应式页面可见比无改善

三、无痕模式(Incognito)

pytest 测试浏览器启用(隔离环境),MCP 调试浏览器视需求(可能需要保留登录态)。通过 launch(args=["--incognito"]) 在 browser 层传入。


四、导航超时与元素超时分离

两者默认均为 15s。分离的意义:页面资源重时可单独调大导航超时而不影响元素操作的快速反馈。

page.set_default_timeout(DEFAULT_TIMEOUT)                    # 元素操作
page.set_default_navigation_timeout(DEFAULT_NAVIGATION_TIMEOUT)  # 页面跳转

检查清单

  • viewportnew_context() 时传入(context 层),不在 browser 层
  • set_default_timeoutset_default_navigation_timeout 均已设置(两者分离)
  • --incognito 在 pytest 测试浏览器上启用,MCP 调试浏览器视需求
  • 用例和 PageObject 中无硬编码的浏览器配置值
规范 Playwright 用例在共享 Page 下离开起点页后的闭环操作。要求用例显式通过 UI 返回并断言,禁止复位 Fixture 兜底。同时强调 is_page_loaded 需严格校验当前状态,防止测试顺序干扰。
用例离开起点页后需返回 case round trip 复位超时 scope 共享 page
skills/case-round-trip/SKILL.md
npx skills add DanielSuo117/velocitai --skill case-round-trip -g -y
SKILL.md
Frontmatter
{
    "name": "case-round-trip",
    "description": "共享 page 下用例离开起点后必须 UI 返回并断言。触发:往返闭合、case round trip、复位超时、scope 共享 page。"
}

Case Round Trip — 用例往返闭合

仅适用 Playwright + pytest。

核心规则

共享 page(scope > function 的 fixture)下,用例离开起点页后必须在末尾通过真实 UI 返回并断言;不要在复位 fixture 里做 goto 兜底。

何时适用

  • 多个用例共享一个 page / browser context(class/module/session scope)
  • 用例会进入"脱离门户布局"的页面(子路由、沉浸式页面、跨子域)
  • 复位 fixture 依赖门户布局才有的元素(侧边栏 / 顶部 nav)

做法

  1. 给每个会"脱离门户布局"的 PageObject 加 click_back_to_<landing>(),走真实 UI 返回按钮
  2. 用例主断言后显式调用返回方法,并再次断言起点页加载
  3. 复位 fixture 保持最简单形态(仅走门户导航),不做 goto 兜底
def test_enter_detail(self):
    self.home.click_first_item()
    detail = DetailPage(self.page)
    assert detail.is_page_loaded(), "详情页未加载"

    detail.click_back_to_home()
    assert self.home.is_page_loaded(), "返回起点页失败"

起点 is_page_loaded 的严格判据

复位 fixture 通常判断 if not start.is_page_loaded(): start.click_back_to_landing()。若 is_page_loaded() 仅校验"页面骨架可见"(title / 容器),切到其他 tab/子路由后骨架仍在 → 返回 True → 复位永不触发 → 下一用例起点错乱。

规则:起点 PageObject 的 is_page_loaded() 必须包含"当前停留在起点状态"判据(起点 tab 激活态 / 起点 URL / 起点独有内容),而不仅是"骨架存在"。

# ❌ 只校验骨架 — 切到其他 tab 后仍返回 True,复位失效
def is_page_loaded(self) -> bool:
    return self.is_visible(self.PAGE_TITLE) and self.is_visible(self.TAB_BAR)

# ✅ 加"当前在起点 tab"判据 — tab 切换后返回 False,触发复位
def is_page_loaded(self) -> bool:
    return (
        self.is_visible(self.PAGE_TITLE)
        and self.is_visible(self.TAB_BAR)
        and self.is_visible(self.LANDING_TAB_ACTIVE_MARK)   # 关键第 3 条
    )

LANDING_TAB_ACTIVE_MARK 的 selector 必须用 text= 等锁定到具体起点 tab(如 .tab.active >> text=<起点 tab 名>),否则 .active 会匹配任意激活 tab,同样失效(见 locator-strategy.md 状态类 selector 必须配 text 锁定)。

决策

跳转目标是否仍保留起点页的门户导航?
├─ 是 → 依赖复位 fixture 即可
└─ 否 → PageObject 必须提供返回方法 + 用例末尾显式返回 + 断言

❌ 反例

  • 进入沉浸式页面后只断言进入,不返回 → 后续用例复位 fixture 点不到门户导航
  • page.goto(<landing_url>) 绕过返回按钮 → 失去对返回按钮的回归覆盖
  • if not visible: goto(...) 塞进复位 fixture → 掩盖"用例未闭合"真问题

检查清单

  • 跳转目标脱离门户布局 → 本规则适用
  • PageObject 提供 click_back_to_<landing>()
  • 用例末尾调用返回方法并断言起点页加载
  • 测试顺序打乱仍全部通过

项目落地参考

项目中脱离门户布局的 PageObject(需实现 click_back_to_<landing>)见 docs/pages-catalog.mddocs/regression-points.md(带"⚠️ 跳转目标脱离门户布局"标注的页面)。

一键生成符合规范的PageObject类及配套回归测试。支持自动推导类名,基于真实DOM提取稳定定位符,更新模块索引与文档,并执行环境验证,确保页面对象与测试用例的完整性及可维护性。
新增页面 生成页面 gen page create page 添加页面对象
skills/gen-page-test/SKILL.md
npx skills add DanielSuo117/velocitai --skill gen-page-test -g -y
SKILL.md
Frontmatter
{
    "name": "gen-page-test",
    "description": "一键生成 PageObject + 配套测试。触发:新增页面、生成页面、gen page、create page、添加页面对象。"
}

Gen Page Test — 一键生成 PageObject + 测试

使用方式

用户输入:为 <中文页面名>(URL: https://...)生成页面对象和测试,测试点:点击A、填写B、查看C

输入参数

参数 必填 说明 示例
页面名称 中文业务名称 课程管理页
页面 URL 真实可访问的页面 URL(已认证态) https://example.com/xxx
角色 项目中已有的角色(如 <role> 对应角色
类名 否(自动推导) PascalCase <Role><Page>Page
回归测试点 需要覆盖的交互元素 点击新建、搜索、删除

类名推导规则

  • 非特定角色:中文名 → 英文翻译 → PascalCase + Page 后缀(如 <Feature>Page
  • 特定角色:<Role> + 英文翻译 → PascalCase + Page 后缀(如 <Role><Feature>Page
  • 角色文件名前缀:<role>_(如 <role>_<feature>_page.py

项目中已有的具体类名清单见 docs/pages-catalog.md


执行流程(6 步)

Step 1:访问真实页面获取 DOM

browser_navigate(必要时先 /entry?token=xxx)→ browser_snapshotbrowser_evaluate 提取元素。按 locator-replacer 六级优先级(P0 → P5)选出稳定定位符。

Step 2:生成 PageObject 文件

创建 pages/<snake_name>_page.py

# 页面名称:<中文名>
from pages.base_page import BasePage


class <ClassName>(BasePage):
    # 定位符 — 来自真实页面(<日期>,URL: <页面URL>)
    <LOCATOR_1> = "<真实定位符>"    # P<0-5>: 说明
    <LOCATOR_2> = "<真实定位符>"    # P<0-5>: 说明
    PAGE_IDENTIFIER = "<页面标识定位符>"    # P<0-5>: 说明

    def click_<method_1>(self):
        self.click(self.<LOCATOR_1>)

    def click_<method_2>(self):
        self.click(self.<LOCATOR_2>)

    def is_page_loaded(self) -> bool:
        return self.is_visible(self.PAGE_IDENTIFIER)

Step 3:更新 pages/__init__.py

from pages.<snake_name>_page import <ClassName>

# 在 __all__ 列表中追加 "<ClassName>"

Step 4:在对应角色测试文件追加 story

目标文件:对应角色 tests/<role>/test_<role>_flow.py(继承 <Role>BaseTest)中的 Test<Role>Flow 类。

对应角色示例:

@allure.story("<页面中文名>")
def test_<snake_name>(self):
    page_obj = <ClassName>(self.<role>_page)
    self.<role>_page.goto("<页面URL>")
    assert page_obj.is_page_loaded(), "<页面中文名>加载失败"
    page_obj.click_<method_1>()
    ...

对应角色使用 self.<role>_page,且若脱离门户布局须往返闭合(见 case-round-trip):

@allure.story("<页面中文名>")
def test_<snake_name>(self):
    self.<role>_home.click_<navigation>()
    page_obj = <ClassName>(self.<role>_page)
    assert page_obj.is_page_loaded(), "<页面中文名>加载失败"
    page_obj.click_<method_1>()
    page_obj.click_back_to_<landing>()
    assert self.<role>_home.is_page_loaded(), "返回起点失败"

Step 5:真实环境验证

pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_<snake_name>"

⚠️ 运行前向用户确认 --env(见 agent-behavior.md P0.2)。

失败时 → locator-replacer 重新抽取定位符,或 test-runner 分析失败类型。

Step 6:同步更新 docs/

  • docs/pages-catalog.md:追加新页面类 + 方法表
  • docs/regression-points.md:追加回归点表 + 关键定位符段

生成后检查清单

通用检查(命名/导入/is_page_loaded/allure)→ coding-conventions.md "新增测试检查清单"

  • 定位符均来自真实页面 DOM,行尾标注 P0~P5 级别
  • pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_<name>" 通过
  • 对应角色的 docs/pages-catalog.md 已追加
  • 对应角色的 docs/regression-points.md 已追加
  • 若跳转目标脱离门户布局,已套用 case-round-trip
将占位定位符替换为真实选择器。按六级优先级(Role、文本、表单属性、稳定类名、data-testid、相对XPath)逐级尝试,自动规避哈希类名与绝对路径,确保定位稳定抗重构。
替换定位符 locator selector 定位失败 元素找不到 DOM分析
skills/locator-replacer/SKILL.md
npx skills add DanielSuo117/velocitai --skill locator-replacer -g -y
SKILL.md
Frontmatter
{
    "name": "locator-replacer",
    "description": "替换占位定位符为真实选择器(六级优先级)。触发:替换定位符、locator、selector、定位失败、元素找不到、DOM 分析。"
}

Locator Replacer — 定位符替换 Skill

适用场景

  • 将页面对象中 text= 占位定位符替换为真实选择器
  • 前端构建后原有选择器失效,需要重新定位
  • 新增页面元素需要选择最佳定位策略

定位符选择策略(六级优先级)

按优先级从高到低,逐级尝试,选中即停。优先使用页面上已有的语义化信息,避免依赖需要前端额外配合的属性:

P0: 用户可见的语义化元素(Role + Text)

# 通过 ARIA role + 可见文本定位
LOGIN_BTN = "role=button[name='登录']"
NAV_LINK = "role=link[name='<功能名>']"
SEARCH_INPUT = "role=textbox[name='搜索']"
  • 基于无障碍语义,与用户看到的界面一致
  • 抗 CSS 重构:role 不依赖 class/id
  • 抗构建工具:不受 Webpack/Vite 哈希类名影响
  • 中文文案变更时需要同步更新

P1: 可见文本定位

# 精确匹配
FEATURE_BTN = "text=<功能名>"
# 包含匹配(文本可能嵌套在子元素中)
FEATURE_TAB = "text=<功能名>"
  • 简单直接,不依赖任何额外属性
  • 适合文案稳定、元素唯一的场景
  • 注意: text= 是当前项目的占位方案,如果分析后发现 text= 已经足够稳定且元素唯一,可以保留并标记 # P1: text(已验证唯一)

P2: 表单特有属性

# placeholder 定位
EMAIL_INPUT = "[placeholder='请输入邮箱']"
# label 关联定位
PASSWORD_INPUT = "css=input[name='password']"
# type 属性
SUBMIT_BTN = "css=button[type='submit']"
  • 仅适用于表单元素(input/select/textarea/button)
  • name 属性通常由后端约定,相对稳定
  • placeholder 跟随文案,稳定性中等

P3: 稳定的 CSS Class / ID

# 业务语义类名 — 稳定
COURSE_CARD = "css=.list-card"
LOGIN_FORM = "css=#login-form"
NAV_MENU = "css=.main-navigation"

# ⚠️ 以下是哈希类名 — 绝对禁止使用
# BAD: "css=.sc-bdVaJa.bVjGWg"         (styled-components 哈希)
# BAD: "css=.css-1a2b3c"                (CSS Modules 哈希)
# BAD: "css=[class*='_component_']"     (Vite CSS Modules)

哈希类名识别规则(必须跳过):

构建工具 哈希类名特征 示例
styled-components 随机字母组合 .sc-bdVaJa, .bVjGWg
CSS Modules 下划线 + 哈希 .header_abc123, ._component_1x2y3
Vite 短哈希后缀 .module_1a2b3c
Tailwind JIT 动态工具类 .[\31 /2], .[color:red]
Emotion 前缀 css- .css-1a2b3c

可用的类名特征:

  • 包含业务语义:.list-card, .login-form, .nav-menu
  • 遵循 BEM 命名:.header__title, .card--active
  • 带有明确前缀:.app-feature-list, .app-header

P4: data-testid 专用测试属性

# Playwright locator 写法
SUBMIT_BTN = "[data-testid='submit-button']"
  • 不受样式重构、文案修改、构建工具影响
  • 需要前端开发配合添加,无法独立完成
  • 适合 P0~P3 均无法稳定定位的复杂元素
  • 如果页面已有 data-testid,可以直接使用,但不必强求前端为所有元素添加

P5: 相对 XPath / CSS 结构定位(最后手段)

# ✅ 相对路径 — 从稳定祖先节点出发
SUBMIT_BTN = "xpath=//div[@class='login-form']//button[@type='submit']"
FIRST_COURSE = "css=.course-list > .course-item:first-child"

# ❌ 绝对路径 — 绝对禁止
# BAD: "xpath=/html/body/div[1]/div[2]/ul/li[3]/button"

XPath 编写规则:

  • 从离目标元素最近的稳定祖先节点开始
  • 祖先节点用业务语义属性锚定(class/id/data-*)
  • 路径层级不超过 3 层
  • 禁止使用绝对路径(从 /html/body 开始)
  • 禁止纯数字索引定位(div[3]),除非是列表且确实需要第 N 项

执行流程

Step 1: 分析目标页面 DOM

使用 agent-browser 打开目标页面,获取交互元素信息(工具选型遵循 agent-behavior P0.4,agent-browser 未安装时降级到 Playwright MCP):

操作步骤:
1. browser_navigate → 访问目标页面(已认证状态)
2. browser_snapshot → 获取页面无障碍树(accessibility tree)
3. browser_evaluate → 执行 JS 提取元素属性:
   - document.querySelectorAll('[data-testid]')  → 已有 testid
   - document.querySelectorAll('[role]')          → ARIA role
   - document.querySelectorAll('button, a, input') → 交互元素

Step 2: 为每个元素选择定位策略

对照页面对象中的每个占位定位符,按 P0→P5 逐级尝试,命中即停:

检查顺序 条件 使用
P0 有 role + name role=button[name='...']
P1 text= 唯一且稳定 text=...(标记"已验证唯一")
P2 表单属性(placeholder/name/type) [placeholder='...']
P3 稳定 CSS class(非哈希) css=.xxx
P4 有 data-testid [data-testid='...']
P5 以上均无 相对 XPath(≤3 层)

Step 3: 更新页面对象

class <PageName>(BasePage):
    # 定位符 — 已替换为真实选择器(<日期>)
    <PRIMARY_ACTION_BTN> = "role=button[name='<按钮文案>']"  # P0: ARIA role
    <SECONDARY_LINK> = "text=<链接文案>"                      # P1: text(已验证唯一)
    PAGE_IDENTIFIER = "role=heading[name='<页面标题>']"       # P0: ARIA role

更新规则:

  • 在定位符行尾注释标注选择级别(# P0: ARIA role / # P1: text / ... / # P4: testid / # P5: XPath
  • 如果 text= 经验证确实稳定且唯一,注释标记 # P1: text(已验证唯一)
  • 删除原来的 # 定位符 — 后续替换为实际选择器 注释
  • 更新为 # 定位符 — 已替换为真实选择器(日期)

Step 4: 验证

# 在对应角色的回归文件中跑指定用例(运行前向用户确认 --env,见 agent-behavior P0.2)
pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_<story_name>"

Step 5: 同步更新文档

更新 docs/regression-points.md 中对应 PageObject 的"关键定位符"段。


定位符质量检查清单

替换完成后,逐项确认:

  • 没有使用绝对 XPath(/html/body/...
  • 没有使用哈希类名(sc-xxx, css-xxx, _module_xxx
  • 没有使用纯数字索引(div[3],除非列表取第 N 项)
  • 每个定位符行尾标注了选择级别(# P0 ~ # P5
  • XPath 层级不超过 3 层
  • 主流程测试在真实环境通过

常见问题处理

同一文本出现多次导致 text= 不唯一

降级到 P3/P5,用父容器 + 文本组合:css=.list-card:first-child >> text=查看详情

元素无任何稳定属性

向前端团队提出添加 data-testid(P4)需求,附元素清单;临时用 P5 相对 XPath 过渡。

前端构建后选择器批量失效

检查是否使用了哈希类名(违反 P3)→ 优先升级到 P0/P1(不受构建影响)→ 文案变更则对照新文案更新。

定义 is_page_loaded() 四种验证模式(单元素、双元素AND、多元素+激活态、委托语义方法)及白屏检测,指导选择稳定断言策略以确保页面关键元素正确渲染。
页面断言 is_page_loaded page loaded 页面加载验证
skills/page-load-assertion/SKILL.md
npx skills add DanielSuo117/velocitai --skill page-load-assertion -g -y
SKILL.md
Frontmatter
{
    "name": "page-load-assertion",
    "description": "is_page_loaded() 四种验证模式与调用层级。触发:页面断言、is_page_loaded、page loaded、页面加载验证。"
}

Page Load Assertion — 页面加载断言方法论

核心思路

页面"正常"的判断不是检查 HTTP 状态码,而是验证用户能看到的关键元素确实渲染出来了。通过每个 PageObject 的 is_page_loaded() 方法统一实现。


is_page_loaded() 的四种验证模式

按严格程度从低到高:

模式 A:单元素验证(最简单)

检查该页面独有的标志性容器是否可见。适用于结构简单、一个元素就能区分的页面:

PAGE_CONTAINER = "css=.feature-container"    # P3

def is_page_loaded(self) -> bool:
    return self.is_visible(self.PAGE_CONTAINER)

模式 B:双元素 AND 组合(推荐默认)

容器 + 内容 双重验证,确保"页面骨架到位 + 数据渲染完成":

PAGE_CONTAINER = "css=.feature-container"    # P3
PAGE_TITLE = "text=欢迎使用"                  # P1

def is_page_loaded(self) -> bool:
    return self.is_visible(self.PAGE_CONTAINER) and self.is_visible(self.PAGE_TITLE)

模式 C:多元素 + 激活态验证(最严格)

在结构和内容基础上,还验证当前 UI 状态(如某个 tab 处于 active)。适用于承担"起点状态判断"语义的页面:

PAGE_TITLE = "css=span.page-title"                                    # P3
TAB_BAR = "css=ul.tab-bar"                                            # P3
DEFAULT_TAB_ACTIVE = "css=ul.tab-bar li.tab.active >> text=默认Tab"    # P3+P1

def is_page_loaded(self) -> bool:
    return (
        self.is_visible(self.PAGE_TITLE)
        and self.is_visible(self.TAB_BAR)
        and self.is_visible(self.DEFAULT_TAB_ACTIVE)
    )

注意:状态类 selector(.active / aria-selected 等)必须配 text 锁定,否则切换 tab 后 .active 跟随新激活对象漂移。

模式 D:委托给语义方法

把验证逻辑提取为有业务含义的方法名,便于 setup fixture 中单独调用:

def is_page_loaded(self) -> bool:
    return self.is_login_success()

def is_login_success(self) -> bool:
    return self.is_visible(self.SUCCESS_FLAG) and self.is_visible(self.NAV_LIST)

模式选择决策树

新页面需要 is_page_loaded()
│
├─ 页面结构简单,一个独有容器即可区分?
│  └─ 是 → 模式 A(单元素)
│
├─ 需要确认内容也渲染到位?
│  └─ 是 → 模式 B(容器 + 内容,推荐默认)
│
├─ 该页面是复位 fixture 的起点,需要判断 UI 状态?
│  └─ 是 → 模式 C(多元素 + 激活态)
│
├─ 验证逻辑在 setup 中也需要单独调用?
│  └─ 是 → 模式 D(委托给语义方法)
│
└─ 容器可能存在但内容未渲染(SPA 子 Tab 切换 / 异步加载)?
   └─ 是 → 模式 E(JS evaluate 检查子元素数量)

验证元素的选择原则

原则 说明
选该页面独有的元素 不选通用 header/footer,选只有这个页面才有的容器或文本
优先选结构性容器 .feature-container.detail-wrapper,页面骨架不易变
文本元素锁定业务语义 text=欢迎使用,确认内容渲染到位
状态类 selector 配 text .active 会随操作漂移,必须加 >> text=具体文本 锁定
不选动态数据 具体数字、用户名等因数据变化导致断言不稳定

断言目标的三级稳定性

核心原则:优先选模板级文本(页面框架固定标题),禁止选数据级内容(动态计数、日期)。多视图场景中每个断言目标须同时满足「模板级 + 视图独占」。

完整规则与反例/正例 → assertion-patterns.md "断言目标稳定性"章节。

模式 E:白屏检测 + 空数据 vs 有数据 三态判断

SPA 应用中,容器 div 可能存在于 DOM 且 is_visible 返回 true,但内部组件未挂载(白屏)。此时模式 A/B 的 is_visible(容器) 会误判为正常。

页面内容区有三种状态,必须分别处理

页面内容区状态
│
├─ 白屏(渲染失败)
│  内容区子元素数 = 0,组件未挂载
│  → 判定:❌ 测试失败,报"内容区域未渲染,疑似白屏"
│
├─ 空数据(无业务数据但渲染正常)
│  内容区子元素数 > 0,但无业务数据项
│  通常有空状态提示(图片 + "暂无数据"/"还没有收藏题哦"等文案)
│  → 判定:✅ 渲染正常,页面确实没有业务数据
│
└─ 有数据(正常渲染)
   内容区子元素数 > 0,且有业务数据项
   → 判定:✅ 渲染正常

实现模板

# 定位符
CONTENT_CONTAINER = "css=<内容主容器>"                 # 页面骨架容器
EMPTY_STATE = "css=<空状态提示元素>"                    # 空状态组件(图片+文案)
DATA_ITEM = "css=<业务数据项>"                          # 单个数据条目

def get_content_render_state(self) -> str:
    """返回内容区渲染状态:'blank' / 'empty' / 'loaded'。"""
    return self.page.evaluate("""() => {
        const container = document.querySelector('<内容主容器选择器>');
        if (!container) return 'blank';
        const contentArea = container.querySelector('<内容区域选择器>');
        if (!contentArea || contentArea.children.length === 0) return 'blank';
        return 'loaded';
    }""")

def is_content_rendered(self) -> bool:
    """白屏检测:容器存在但内容区无子元素 → False。
    空数据和有数据都算渲染成功 → True。"""
    return self.get_content_render_state() != 'blank'

def has_data_items(self) -> bool:
    """是否有业务数据(排除空状态)。"""
    return self.get_element_count(self.DATA_ITEM) > 0

用例中的三态断言

# 第一层:渲染检测(白屏 vs 已渲染)
is_rendered = page_obj.is_content_rendered()
if not is_rendered:
    assert False, "内容区域未渲染,疑似白屏"

# 第二层(可选):空数据 vs 有数据
# 根据业务预期决定是否需要进一步区分
if page_obj.has_data_items():
    # 有数据 → 可以进一步验证数据内容
    pass
else:
    # 空数据 → 记录到报告,不视为失败
    allure.attach("该Tab当前无业务数据(空状态)", ...)

⚠️ 陷阱

错误做法 问题 正确做法
innerText.length > N 判断白屏 空状态提示文案可能很短,被误判为白屏 children.length > 0
空数据等同于失败 无收藏/无错题是合理的业务状态 白屏才失败,空数据只记录
只检查容器 is_visible 容器 div 可能存在但子组件未挂载 JS evaluate 检查子元素

适用场景

  • 同一容器内切换多个子 Tab,部分 Tab 可能无业务数据
  • 异步加载组件可能因接口超时未渲染
  • 页面骨架先渲染、内容延迟加载的 SPA 路由

调用层级

  1. class setup:登录后 assert home.is_page_loaded(), "首页加载失败"
  2. function resetif not self.home.is_page_loaded(): click_nav_home(); assert ...
  3. 用例体:每次跳转后 assert target.is_page_loaded(), "<页面>加载失败"

规则约束

  1. 每个 PageObject 必须实现 is_page_loaded()——返回 bool,放在类的最后一个方法
  2. is_page_loaded() 内部只用 is_visible() 组合,不抛异常
测试失败时免登跳转至目标页面原地排查。解析错误类型,通过决策树诊断定位符、超时或断言问题,调用对应Skill修复并验证,避免重跑全流程,提升纠错效率。
排查 纠错 debug 定位符失败 超时 元素找不到 断言失败
skills/quick-debug/SKILL.md
npx skills add DanielSuo117/velocitai --skill quick-debug -g -y
SKILL.md
Frontmatter
{
    "name": "quick-debug",
    "description": "测试失败时免登跳转原地排查修复。触发:排查、纠错、debug、定位符失败、超时、元素找不到、断言失败。"
}

Quick Debug — 快速纠错验证

核心思路

跳过从登录到问题节点的完整流程,直接免登到目标页面原地排查。

利用项目已有的 token URL 免登机制,配合浏览器工具直接在问题页面进行 DOM 探索、定位符验证、等待策略调整,修复后原地验证,全程不重跑完整测试流程。


主干流程(5 步)

Step 1: 解析失败信息
    ↓
Step 2: 构造免登跳转 URL
    ↓
Step 3: 免登跳转到目标页面
    ↓
Step 4: 诊断问题(决策树)
    ↓
Step 5: 修复 + 原地验证

Step 1: 解析失败信息

从堆栈 / 用户描述提取:失败用例名、错误类型(TimeoutError / AssertionError)、失败定位符、失败时所在 URL。信息不足时读取测试代码反推。

Step 2: 构造免登跳转 URL

从配置文件读取对应环境的 token。

⚠️ 免登前必须向用户确认 --env(见 agent-behavior P0.2)。

两种情况:

情况 策略
目标页面有明确 URL 路径 先免登(/entry?token=<JWT>),再导航到目标路径
目标页面需要交互才能到达(弹窗、新 tab 等) 免登到最近的可直接访问的父级页面,执行最少量的导航操作到达目标

Step 3: 免登跳转到目标页面

browser_navigate → <base_url>/entry?token=<JWT> → 导航到目标路径 → 验证 is_page_loaded()。免登失败(跳回登录页)→ 提示用户刷新 token。

Step 4: 诊断问题

见下一节「诊断决策树」。

Step 5: 修复 + 原地验证

根据诊断结果调用对应 skill:

诊断结果 调用 skill
定位符问题 locator-replacer
等待策略问题 wait-strategy
页面加载断言问题 page-load-assertion
页面大改版 报告用户,建议 gen-page-test 重新生成
断言预期值变更 报告差异,由用户决策是否更新预期值

原地验证: 修复后在当前浏览器页面直接验证(用 Playwright MCP 重新执行失败操作),确认无误后建议用户重跑完整测试。


诊断决策树(Step 4 展开)

先用 agent-browser 快速扫描当前页面 DOM(未安装时降级到 Playwright MCP),然后根据错误类型走对应分支:

错误类型判定
    ├── A. 定位符问题(元素找不到 / 匹配多个)
    ├── B. 超时问题(元素存在但加载慢 / 网络延迟)
    ├── C. 断言问题(元素存在但内容/状态不符预期)
    └── D. 页面结构变化(页面整体改版 / 路由变更)

分支 A: 定位符问题

判定依据: locator.count() == 0strict mode violation(匹配多个)

排查步骤:

  1. 用 agent-browser 抓取问题区域的 DOM 快照
  2. 对比 PageObject 定位符与实际 DOM,判定原因:类名变更 / 文本变更 / 动态渲染未完成

修复路径 → 调用 locator-replacer skill

分支 B: 超时问题

判定依据: TimeoutError 且元素在更长等待后可出现,或 networkidle 未达成

排查步骤:

  1. 检查当前页面 URL 是否正确(排除导航偏差),目标元素是否存在但不可见(hidden / 被遮挡)
  2. 判定原因:隐式等待不够 / SPA 渲染延迟 / 网络接口慢

修复路径 → 调用 wait-strategy skill 或调整超时配置

分支 C: 断言问题

判定依据: AssertionError,元素存在且可见,但值/状态不符预期

排查步骤:

  1. 获取元素实际文本/属性值,与测试代码预期值对比
  2. 判定原因:业务数据变化 / 文案更新 / 环境差异

修复路径 → 报告差异并建议用户决策(通常是业务变更而非代码 bug)

分支 D: 页面结构变化

判定依据: is_page_loaded() 失败 或 当前 URL 与预期不一致

排查步骤:

  1. 用 agent-browser 抓取全页面快照,与 PageObject 定位符批量比对
  2. 判定原因:路由变更 / 页面改版 / 权限变化导致重定向

修复路径 → 小范围改动调用 locator-replacer;大范围改版报告用户,建议 gen-page-test

回退机制

如果归类后排查未果,回退到上一级重新分类。最多回退一次,仍无法定位则整理已收集信息报告用户。


浏览器工具选择

DOM 探索/定位符采集 → agent-browser(优先);精确验证/断言 → Playwright MCP;agent-browser 未安装 → 全程 Playwright MCP(P0.4 降级)。


检查清单

每次 quick-debug 结束前,必须确认:

  • 已明确失败原因并归类(A/B/C/D 哪个分支)
  • 修复动作已执行(或已报告用户需人工决策)
  • 原地验证通过(修复后的定位符/等待在当前页面生效)
  • 建议用户重跑完整测试确认回归
  • 保存操作验证失败时,参考 save-verify-strategy(Toast 时序 / 重定向检测 / 数据对比)
规范表单保存后的验证策略,解决macOS富文本全选快捷键问题。提供Toast、重定向及数据对比三级验证方案,强调时序陷阱规避与Tab管理最佳实践,确保自动化测试稳定性。
触发保存验证 处理toast提示 检测页面重定向 使用Meta+A快捷键 判断保存是否成功
skills/save-verify-strategy/SKILL.md
npx skills add DanielSuo117/velocitai --skill save-verify-strategy -g -y
SKILL.md
Frontmatter
{
    "name": "save-verify-strategy",
    "description": "表单保存后验证(Toast \/ 重定向 \/ 数据对比 \/ 富文本清空)。触发:保存验证、toast、重定向检测、Meta+A、保存成功判断。"
}

表单保存验证策略


一、macOS 富文本编辑器全选必须用 Meta+A

macOS Chromium 中 Control+A 是 Emacs 快捷键(光标移到行首),不是"全选"。在 CKEditor / TinyMCE / Quill 等富文本编辑器中,Control+A 无法选中全部内容,导致旧内容残留。

❌ 反例:

def fill_rich_text(self, text: str):
    self.click(self.EDITOR)
    self.page.keyboard.press("Control+A")   # macOS 上只移动光标到行首
    self.page.keyboard.press("Backspace")   # 只删一个字符,旧内容残留
    self.page.keyboard.type(text)           # 新内容追加在旧内容后面

✅ 正例:

def fill_rich_text(self, text: str):
    self.click(self.EDITOR)
    self.page.keyboard.press("Meta+A")      # macOS Command+A = 全选
    self.page.keyboard.press("Backspace")   # 清空全部内容
    self.page.keyboard.type(text)           # 写入纯新内容

排查信号:填入的数据末尾多了旧内容残片(如 "2026-05-06 10:30:00123" 尾部多了 123)→ 全选快捷键无效。


二、保存后验证策略:三级选择

保存后行为 验证方式 可靠性 适用场景
显示 Toast is_visible(toast_locator) — 必须在 networkidle 后立即检测 低(2-3s 消失) 有明确 Toast 的场景
页面重定向 检测编辑页特征元素消失 wait_for(state="hidden") 中(持久状态) 保存后自动跳转的场景
无 UI 反馈 写入标记数据 → 重新打开 → 读取对比 高(数据级验证) 无 Toast、不确定是否重定向

原则:三级可组合使用。优先用持久状态变更,Toast 仅作辅助。

2.1 Toast 时序陷阱

click_save() 方法中的 wait_for_timeout() 会消耗 Toast 存活时间。必须在 networkidle 后立即检测 Toast,不能先等待再检测。

❌ 反例:

def click_save(self):
    self.click(self.SAVE_BTN)
    self.page.wait_for_load_state("networkidle")
    self.page.wait_for_timeout(3000)   # Toast 在这 3s 内出现又消失了

# 调用方
assert page.is_visible(toast)          # Toast 已消失,永远 False

✅ 正例:

def click_save(self):
    self.click(self.SAVE_BTN)
    self.page.wait_for_load_state("networkidle")
    # 不加额外等待 — 让调用方立即检测 Toast

# 调用方
is_ok = page.is_visible(toast, timeout=10000)   # 立即开始等 Toast
page.wait_for_timeout(3000)                      # 检测完再等页面稳定

2.2 重定向检测

保存后页面可能重定向离开编辑页。通过检测编辑页特征元素消失来确认保存成功:

def is_save_success(self) -> bool:
    """编辑页 header 消失 = 保存成功并已离开编辑页"""
    try:
        self.page.locator(self.EDIT_PAGE_HEADER).first.wait_for(
            state="hidden", timeout=15000
        )
        return True
    except Exception:
        return False

2.3 重定向目的地不可假设

保存后的重定向目的地可能因导航上下文不同而异(直接 URL 访问 vs SPA 内跳转 vs 新 tab 中操作)。不要断言重定向到某个特定页面。

❌ 反例:

# 假设保存后一定重定向到首页
redirected_home = HomePage(page)
assert redirected_home.is_page_loaded()   # 实际可能跳到其他页面

✅ 正例:

# 只验证离开了编辑页,不假设去了哪里
assert edit_page.is_save_success()        # 编辑页 header 消失 = 保存成功

三、多 tab 保存后:关闭旧 tab,从原始 tab 重入

保存后如果需要重新访问同一页面验证数据,不要尝试在重定向后的页面内导航(重定向目的地不确定,元素可能不存在)。关闭旧 tab,从原始 tab(状态稳定)重新打开新 tab 进入。

❌ 反例:

# 保存后尝试在重定向页面内导航回去
redirected_page = SomePage(new_page)
assert redirected_page.is_page_loaded()    # 重定向目的地不确定 → 失败
redirected_page.click(breadcrumb)          # 元素可能不存在

✅ 正例:

# 保存后关闭旧 tab
finally:
    new_page.close()

# 原始 tab 状态稳定,从这里重新进入
assert original_home.is_page_loaded()
new_page_2 = original_home.open_target_in_new_tab(name)
try:
    # 在新 tab 中导航到编辑页 → 验证数据
finally:
    new_page_2.close()

四、数据对比验证模式(最终验证)

当没有 Toast 或 UI 反馈时,用「写入标记 → 保存 → 重新打开 → 读取对比」作为最终验证闭环。

# ── 写入阶段 ──
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
edit_page.fill_description(timestamp)
write_test_data("标记键名", timestamp)          # 持久化到配置文件

# ... 保存操作 ...

# ── 验证阶段(重新打开编辑页)──
saved = read_test_data().get("标记键名")
actual = edit_page.get_description_text()
allure.attach(saved, name="期望时间戳", ...)     # 无论成败都记录到报告
allure.attach(actual, name="实际时间戳", ...)
assert actual == saved, (
    f"数据不一致:期望='{saved}',实际='{actual}'"
)

关键要点

  1. 标记数据必须写入配置文件(跨用例共享,非内存变量),便于后续用例复用
  2. 无论断言成败,都用 allure.attach() 将期望值和实际值写入报告
  3. 断言失败信息必须包含两个值的对比,方便排查
用于运行 pytest 测试并结构化分析失败原因。支持单故事调试与全量回归,强制确认环境参数。针对定位符失效、超时、断言及认证失败提供分类诊断与处理建议,并生成标准化结果摘要。
运行测试 跑测试 run test pytest 失败分析 allure
skills/test-runner/SKILL.md
npx skills add DanielSuo117/velocitai --skill test-runner -g -y
SKILL.md
Frontmatter
{
    "name": "test-runner",
    "description": "运行 pytest + 结构化分析失败原因。触发:运行测试、跑测试、run test、pytest、失败分析、allure。"
}

Test Runner — 运行测试与结果分析

⚠️ 运行前必须向用户确认 --env(见 agent-behavior.md P0.2)。 即使 config/settings.py::DEFAULT_ENV="prod"、即使下方示例写 --env=<pre|prod>(示例 ≠ 默认值),仍必须问用户。

运行前准备

# 确认虚拟环境已激活
source .venv/bin/activate

# 确认依赖已安装
pip list | grep -E "playwright|pytest|allure"

运行模式

所有测试均基于真实环境(URL + token)运行。

模式 1: 单个 story(调试 / 定位符验证)

场景: 替换定位符后验证、调试单个页面回归点

# 对应角色
pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_xxx"
# 对应角色
pytest tests/<role>/test_<role>_home.py --env=<pre|prod> -v -k "test_<case_name>"

模式 2: 按角色 / 全量回归

场景: 发版前验证、完整回归

# <角色A>全量
pytest tests/<roleA>/ --env=<pre|prod>
# <角色B>全量
pytest tests/<roleB>/ --env=<pre|prod>
# 全量
pytest tests/ --env=<pre|prod>

查看 Allure 报告

# 启动报告服务器查看(运行完测试后直接可用)
allure serve reports/allure-results

# 生成静态报告(可部署)
allure generate reports/allure-results -o reports/allure-report --clean

结果分析

pytest 输出解读

tests/test_xxx.py::TestXxx::test_method PASSED    → 通过
tests/test_xxx.py::TestXxx::test_method FAILED    → 失败(需分析)
tests/test_xxx.py::TestXxx::test_method ERROR     → fixture/setup 异常
tests/test_xxx.py::TestXxx::test_method SKIPPED   → 跳过

失败分类与处理

运行失败后,按以下分类诊断:

类型 A: 定位符失效

特征:

TimeoutError: Timeout 10000ms exceeded.
  waiting for locator("text=<功能名>")

处理: 触发 locator-replacer skill,使用 agent-browser 访问真实页面重新抽取定位符(工具选型遵循 agent-behavior P0.4)。

类型 B: 页面加载超时

特征:

TimeoutError: page.wait_for_load_state: Timeout 30000ms exceeded.
  waiting for "networkidle"

处理:

  1. 检查网络连通性(hosts 配置、VPN)
  2. 检查 token 是否过期
  3. 增大 DEFAULT_TIMEOUT(临时措施)

类型 C: 断言失败

特征:

AssertionError: <页面中文名>加载失败
assert False

处理:

  1. 页面是否正确导航到目标 URL
  2. PAGE_IDENTIFIER 定位符是否仍然有效
  3. 页面内容是否因版本更新发生变化

类型 D: 认证失败

特征:

AssertionError: Token 登录失败

处理:

  1. 检查 config/settings.py 中的 token 是否有效
  2. 检查 --env 参数是否选对
  3. 检查本地 hosts 是否指向正确的服务器 IP

💡 快速排查建议

当失败需要深入排查时(定位符失效、页面结构变化等),建议进入 quick-debug 模式:通过 token 免登直接跳转到问题页面,避免从登录重走完整流程。


结果摘要模板

运行完成后,按以下格式输出摘要:

## 测试运行摘要

**运行模式:** 单个 story / 主流程全量
**环境:** pre / prod
**时间:** 2026-04-15 15:30

### 结果

| 状态 | 数量 |
|------|------|
| PASSED | X |
| FAILED | X |
| ERROR | X |
| SKIPPED | X |

### 失败详情(如有)

| 测试用例 | 失败类型 | 原因 | 建议操作 |
|----------|---------|------|---------|
| test_xxx | A: 定位符失效 | text=xxx 不唯一 | 使用 locator-replacer |
| test_yyy | D: 认证失败 | token 过期 | 更新 settings.py |
为已有PageObject增量添加回归测试点。通过真实页面抽取定位符,更新页面对象及测试用例,并同步文档。支持脱离布局时的往返处理,确保覆盖新增交互元素且不修改现有代码。
新增测试点 添加回归点 add test point 增加覆盖 扩展页面方法
zh/skills/add-regression-point/SKILL.md
npx skills add DanielSuo117/velocitai --skill add-regression-point -g -y
SKILL.md
Frontmatter
{
    "name": "add-regression-point",
    "description": "为已有 PageObject 增量添加回归测试点。触发:新增测试点、添加回归点、add test point、增加覆盖、扩展页面方法。"
}

Add Regression Point — 新增回归测试点

适用场景

已有 PageObject 新增单个或多个回归测试点。与 gen-page-test(新建整个页面)不同,本 skill 针对已存在的页面做增量扩展

输入参数

参数 必填 说明 示例
目标 PageObject 已有的页面类名 <ExistingPage>
页面 URL 真实可访问的页面 URL(已认证态) https://example.com/xxx
新增测试点 需要覆盖的交互元素 点击收藏、筛选类型

执行流程(4 步)

Step 1:访问真实页面抽取新元素定位符

使用 agent-browser 以已认证态访问目标 URL,为每个新增测试点按 locator-replacer 六级优先级选定定位符。浏览器工具选型遵循 agent-behavior P0.4:DOM 探索 / 定位符采集用 agent-browser,agent-browser 未安装时降级到 Playwright MCP。

Step 2:更新 PageObject

pages/<page_name>_page.py 中追加:

2a. 新增定位符常量(插入到已有常量之后、方法之前)

    # ↓ 新增定位符(来自真实页面,<日期>)
    <NEW_LOCATOR_1> = "<真实定位符>"    # P<0-5>: 说明
    <NEW_LOCATOR_2> = "<真实定位符>"    # P<0-5>: 说明

2b. 新增业务方法(插入到 is_page_loaded() 之前)

    def click_<new_method_1>(self):
        self.click(self.<NEW_LOCATOR_1>)

    def click_<new_method_2>(self):
        self.click(self.<NEW_LOCATOR_2>)

    def is_page_loaded(self) -> bool:    # 保持为最后一个方法
        return self.is_visible(self.PAGE_IDENTIFIER)

Step 3:在对应角色回归测试中追加调用

根据目标 PageObject 的角色:

  • 对应角色页面 → tests/teacher/test_<feature>.py(当前无主流程用例,新建时继承 <Role>BaseTest
  • 对应角色页面 → tests/<role>/test_<role>_flow.py
  • 对应角色 → tests/<role>/test_<role>_flow.py

在对应 @allure.story 方法中追加:

    # ↓ 新增回归点
    <page_instance>.click_<new_method_1>()
    <page_instance>.click_<new_method_2>()

⚠️ 若新增测试点的跳转目标脱离门户布局(例如离开侧边导航的子路由、沉浸式页面),必须套用 case-round-trip:PageObject 提供 click_back_to_<landing>,用例末尾显式返回起点页并断言;否则后续用例复位 fixture 会超时。

Step 4:同步更新 docs/

  • 在对应角色的 pages-catalog 子文件中追加新方法行:
  • 在对应角色的 regression-points 子文件中追加回归点行 + 关键定位符常量:

检查清单

通用检查(命名/导入/is_page_loaded/allure)→ coding-conventions.md "新增测试检查清单"

  • 新增定位符均来自真实页面 DOM,行尾标注 P0~P5 级别
  • 对应角色 test_<role>_flow.py 已追加新方法调用
  • 对应角色的 docs/pages-catalog-<role>.md 已同步
  • 对应角色的 docs/regression-points-<role>.md 已同步
  • 若跳转目标脱离门户布局,已套用 case-round-trip

注意事项

  • 不要修改已有方法,只做增量添加
  • 不需要更新 pages/__init__.py(页面类已存在)
  • 如果新增测试点需要页面导航(跳转到子页面),考虑新建独立 PageObject(使用 gen-page-test
  • 定位符直接基于真实页面抽取,不使用占位符
提供POM分层架构决策与Context共享模式指南。涵盖测试、页面对象及配置层结构,定义PageObject骨架,并通过决策树指导基于域名、前置条件及起点的Class级或Function级Context共享策略,支持用例接力执行。
架构设计 POM分层 Page Object模式 基类设计 新增角色/端 Context共享决策
zh/skills/architecture/SKILL.md
npx skills add DanielSuo117/velocitai --skill architecture -g -y
SKILL.md
Frontmatter
{
    "name": "architecture",
    "description": "POM 分层与 context 共享决策树。触发:架构、分层、POM、Page Object、基类设计、新增角色。"
}

架构决策 — POM 分层与 context 共享模式

适用场景

  • 新增角色/端时决定是否引入新基类
  • 判断某组用例要不要共享 page / context
  • 给新成员解释 POM 分层

POM 分层通用骨架

┌─────────────────────────────────────────────┐
│                  tests/ 测试层                │
│  按角色拆分文件:test_<role>_flow.py          │
│  每个测试类继承 Base(或 Role-Base)           │
├─────────────────────────────────────────────┤
│                  pages/ 页面对象层             │
│  Base ← Login ← Landing ← Detail / ...        │
│  通用 Role 用前缀命名:Role<Role><Page>Page   │
├─────────────────────────────────────────────┤
│                  config/ 配置层                │
│  URL、Token、浏览器参数按环境切换              │
├─────────────────────────────────────────────┤
│                  conftest.py Fixture 层        │
│  browser → context → page → Role-specific base │
└─────────────────────────────────────────────┘

数据流向configconftest(读取配置创建 fixture) → pages(接收 page 对象) → tests(组合页面对象执行断言)


PageObject 骨架

# 页面名称:<中文名>
from pages.base_page import BasePage


class <PageName>(BasePage):
    # 定位符常量(类顶部)
    <LOCATOR_CONSTANT> = "<selector>"   # P<0-5>: 说明
    PAGE_IDENTIFIER = "<selector>"      # 页面加载锚点

    # 业务方法(前缀:click_ / fill_ / select_ / get_xxx_text / wait_for_ / is_xxx_visible)
    def click_<action>(self):
        self.click(self.<LOCATOR_CONSTANT>)

    # 页面加载验证(必须实现,放在最后)
    def is_page_loaded(self) -> bool:
        return self.is_visible(self.PAGE_IDENTIFIER)

context 共享模式决策树

一组用例是否满足以下 3 条?
   1. 同一域名(不跨端)
   2. 有统一前置(登录、角色切换…)
   3. 有统一起点页面(可复位)
       │
       ├─ 是(三条全满足)
       │    └→ class 级共享 context
       │       - 用 scope="class" 的 page fixture(实现见 browser-config skill)
       │       - 专用基类:class setup 完成前置;function-scope autouse 复位起点
       │       - 用例末尾必须往返闭合(见 case-round-trip skill)
       │
       └─ 否(任一不满足)
            └→ function 级独立 context
               - 每用例独立 browser.new_context() + new_page()
               - 每用例独立登录

注意事项:

  • ❌ 禁止 session 级跨 class 共享 context(跨 class 职责边界不清)
  • ❌ 禁止同一 class 内跨域(会污染共享 context)
  • 详细规则见 rules/playwright/browser-context.md "浏览器上下文"段

同 class 内跨用例 page 接力模式

场景:同一 class 的多个用例形成线性流程链,后一个用例的起点是前一个用例的终点,不必从头登录。

test_a(搜索课程)
    ↓ 页面停留在搜索结果
test_b(点击课程 → 新 tab 打开课程首页)
    ↓ 新 tab 保存为 class 属性
test_c(在课程首页继续操作)
    ↓ 直接使用 class 属性中的 page

实现要点

  1. class-scope fixture 完成一次性登录:覆盖父类 _login_setup 为 no-op,用 class_page + request.cls.page 共享
  2. 用例间 page 接力:当用例打开新 tab 时,将新 page 保存到 class 属性(type(self).<attr> = new_page),后续用例通过 self.<attr> 访问
  3. 新 tab 不关闭:接力链中的 page 保持打开,直到 class 结束 context 自动销毁
  4. 用例顺序即流程顺序:pytest 默认按文件中方法出现顺序执行

骨架

class_page fixture 的实现见 browser-config "Fixture 分层模板"。

class TestSomeFlow(BaseTest):

    @pytest.fixture(autouse=True)
    def _login_setup(self):
        yield  # no-op,覆盖父类

    @pytest.fixture(scope="class", autouse=True)
    def _shared_setup(self, class_page, token, request):
        login_page = LoginPage(class_page)
        login_page.login_with_token(BASE_URL, token)
        request.cls.page = class_page

    def test_step_1(self):
        ...  # self.page 已由 _shared_setup 注入,class 级共享

    def test_step_2(self):
        new_page = some_page.click_open_new_tab()
        type(self).detail_page = new_page  # 保存到 class 属性供后续用例

    def test_step_3(self):
        detail = SomeDetailPage(self.detail_page)  # 直接接力
        ...

适用条件

  • ✅ 同一 class 内、用例具有明确的先后依赖关系
  • ✅ 新 tab 仍在同域或已携带认证(无需二次登录)
  • ❌ 如果用例间无依赖关系,不要用接力模式(改用独立 function-scope page)

新增 Role 的落地步骤

  1. 决策:用上面的决策树判断新 Role 的用例是否可共享 context
  2. PageObject:以 Role<Role><Page>Page 命名、role_<page> 文件名前缀,继承 BasePage
  3. 基类(若采用 class 级共享):新建 tests/<role>/<role>_base_test.py::<Role>BaseTest 继承 BaseTest,实现登录 + 切换 + 起点断言
  4. 用例:tests/<role>/test_<role>_flow.py::Test<Role>Flow 继承新基类
  5. 文档:在 docs/architecture.md 表中追加一行
  6. 目录:pages/__init__.py 追加新页面导出

决策落地参考

本项目当前的落地情况(具体类名、fixture、起点页面)→ docs/architecture.md

规范浏览器启动配置,集中管理viewport、headless及超时参数。通过fixture分层注入,禁止硬编码。强调viewport适配屏幕、无痕模式隔离及导航与元素超时分离,确保测试稳定性与环境一致性。
viewport 浏览器配置 导航超时 headless slow_mo incognito
zh/skills/browser-config/SKILL.md
npx skills add DanielSuo117/velocitai --skill browser-config -g -y
SKILL.md
Frontmatter
{
    "name": "browser-config",
    "description": "浏览器 viewport \/ headless \/ 超时分层配置。触发:viewport、浏览器配置、导航超时、headless、slow_mo、incognito。"
}

浏览器启动配置 — viewport / 无痕 / 超时分离

核心原则

所有浏览器配置集中声明在配置文件,通过 fixture 分层注入,禁止在用例或 PageObject 中硬编码。


一、pytest 浏览器完整配置架构

配置分层

配置文件(常量声明)
  │
  ├─ browser fixture(session 级)
  │    └─ headless / slow_mo / incognito
  │
  ├─ page fixture(function 级)
  │    └─ viewport / timeout / navigation_timeout
  │
  └─ class_page fixture(class 级)
       └─ viewport / timeout / navigation_timeout

配置文件:所有常量集中声明

# 浏览器配置
HEADLESS = False              # True: 无头模式(CI 环境); False: 有头模式(本地调试)
SLOW_MO = 500                 # 每步操作间隔(ms),便于肉眼观察;CI 设为 0
DEFAULT_TIMEOUT = 15000       # 元素操作超时(ms)
DEFAULT_NAVIGATION_TIMEOUT = 15000  # 页面导航超时(ms)
VIEWPORT_WIDTH = 1280         # 视口宽度
VIEWPORT_HEIGHT = 900         # 视口高度

Fixture 分层模板

@pytest.fixture(scope="session")
def browser(playwright_instance):
    browser = playwright_instance.chromium.launch(
        headless=HEADLESS, slow_mo=SLOW_MO, args=["--incognito"],
    )
    yield browser
    browser.close()

@pytest.fixture                   # function 级:每个用例独立 context + page
def page(browser):
    context = browser.new_context(viewport={"width": VIEWPORT_WIDTH, "height": VIEWPORT_HEIGHT})
    page = context.new_page()
    page.set_default_timeout(DEFAULT_TIMEOUT)
    page.set_default_navigation_timeout(DEFAULT_NAVIGATION_TIMEOUT)
    yield page
    page.close(); context.close()

@pytest.fixture(scope="class")    # class 级:同 class 内共享 context + page
def class_page(browser):
    # 同 page fixture,scope="class"
    ...

关键约束viewport 在 context 层、timeout 在 page 层、--incognito 在 browser 层——三者不得混用层级。


二、Viewport 尺寸选择

核心约束:viewport 高度必须适配物理屏幕

viewport 高度不能超过物理屏幕可用高度(屏幕分辨率 − 浏览器工具栏 − 系统任务栏),否则浏览器窗口底部超出屏幕,页面底部内容被遮挡、无法交互。

正确策略:保持宽度,适当增加高度(不超出屏幕)

响应式页面等比放大 viewport 无效(可见比不变);应保持宽度,适当增加高度(不超出屏幕)。

方案 Viewport 效果
默认 1280x720 基准,较小
推荐 1280x900 适配 MacBook 屏幕,窗口不超出屏幕
外接显示器 1280x1080 适配 Full HD 显示器
过大(错误) 1280x1440 超出多数屏幕,底部被遮挡
等比放大(错误) 2560x1440 响应式页面可见比无改善

三、无痕模式(Incognito)

pytest 测试浏览器启用(隔离环境),MCP 调试浏览器视需求(可能需要保留登录态)。通过 launch(args=["--incognito"]) 在 browser 层传入。


四、导航超时与元素超时分离

两者默认均为 15s。分离的意义:页面资源重时可单独调大导航超时而不影响元素操作的快速反馈。

page.set_default_timeout(DEFAULT_TIMEOUT)                    # 元素操作
page.set_default_navigation_timeout(DEFAULT_NAVIGATION_TIMEOUT)  # 页面跳转

检查清单

  • viewportnew_context() 时传入(context 层),不在 browser 层
  • set_default_timeoutset_default_navigation_timeout 均已设置(两者分离)
  • --incognito 在 pytest 测试浏览器上启用,MCP 调试浏览器视需求
  • 用例和 PageObject 中无硬编码的浏览器配置值
规范 Playwright+pytest 中共享 Page 用例的往返闭合:脱离门户布局时需显式 UI 返回并断言,禁止复位 Fixture 兜底;is_page_loaded 需锁定起点状态。
用例离开起点页后需返回 case round trip 复位超时 scope 共享 page
zh/skills/case-round-trip/SKILL.md
npx skills add DanielSuo117/velocitai --skill case-round-trip -g -y
SKILL.md
Frontmatter
{
    "name": "case-round-trip",
    "description": "共享 page 下用例离开起点后必须 UI 返回并断言。触发:往返闭合、case round trip、复位超时、scope 共享 page。"
}

Case Round Trip — 用例往返闭合

仅适用 Playwright + pytest。

核心规则

共享 page(scope > function 的 fixture)下,用例离开起点页后必须在末尾通过真实 UI 返回并断言;不要在复位 fixture 里做 goto 兜底。

何时适用

  • 多个用例共享一个 page / browser context(class/module/session scope)
  • 用例会进入"脱离门户布局"的页面(子路由、沉浸式页面、跨子域)
  • 复位 fixture 依赖门户布局才有的元素(侧边栏 / 顶部 nav)

做法

  1. 给每个会"脱离门户布局"的 PageObject 加 click_back_to_<landing>(),走真实 UI 返回按钮
  2. 用例主断言后显式调用返回方法,并再次断言起点页加载
  3. 复位 fixture 保持最简单形态(仅走门户导航),不做 goto 兜底
def test_enter_detail(self):
    self.home.click_first_item()
    detail = DetailPage(self.page)
    assert detail.is_page_loaded(), "详情页未加载"

    detail.click_back_to_home()
    assert self.home.is_page_loaded(), "返回起点页失败"

起点 is_page_loaded 的严格判据

复位 fixture 通常判断 if not start.is_page_loaded(): start.click_back_to_landing()。若 is_page_loaded() 仅校验"页面骨架可见"(title / 容器),切到其他 tab/子路由后骨架仍在 → 返回 True → 复位永不触发 → 下一用例起点错乱。

规则:起点 PageObject 的 is_page_loaded() 必须包含"当前停留在起点状态"判据(起点 tab 激活态 / 起点 URL / 起点独有内容),而不仅是"骨架存在"。

# ❌ 只校验骨架 — 切到其他 tab 后仍返回 True,复位失效
def is_page_loaded(self) -> bool:
    return self.is_visible(self.PAGE_TITLE) and self.is_visible(self.TAB_BAR)

# ✅ 加"当前在起点 tab"判据 — tab 切换后返回 False,触发复位
def is_page_loaded(self) -> bool:
    return (
        self.is_visible(self.PAGE_TITLE)
        and self.is_visible(self.TAB_BAR)
        and self.is_visible(self.LANDING_TAB_ACTIVE_MARK)   # 关键第 3 条
    )

LANDING_TAB_ACTIVE_MARK 的 selector 必须用 text= 等锁定到具体起点 tab(如 .tab.active >> text=<起点 tab 名>),否则 .active 会匹配任意激活 tab,同样失效(见 locator-strategy.md 状态类 selector 必须配 text 锁定)。

决策

跳转目标是否仍保留起点页的门户导航?
├─ 是 → 依赖复位 fixture 即可
└─ 否 → PageObject 必须提供返回方法 + 用例末尾显式返回 + 断言

❌ 反例

  • 进入沉浸式页面后只断言进入,不返回 → 后续用例复位 fixture 点不到门户导航
  • page.goto(<landing_url>) 绕过返回按钮 → 失去对返回按钮的回归覆盖
  • if not visible: goto(...) 塞进复位 fixture → 掩盖"用例未闭合"真问题

检查清单

  • 跳转目标脱离门户布局 → 本规则适用
  • PageObject 提供 click_back_to_<landing>()
  • 用例末尾调用返回方法并断言起点页加载
  • 测试顺序打乱仍全部通过

项目落地参考

项目中脱离门户布局的 PageObject(需实现 click_back_to_<landing>)见 docs/pages-catalog.mddocs/regression-points.md(带"⚠️ 跳转目标脱离门户布局"标注的页面)。

一键生成PageObject类及配套测试用例。通过访问真实页面提取DOM定位符,创建页面对象文件,更新模块索引与回归文档,并在指定角色测试文件中添加验证逻辑,最后执行环境验证确保稳定性。
新增页面 生成页面 gen page create page 添加页面对象
zh/skills/gen-page-test/SKILL.md
npx skills add DanielSuo117/velocitai --skill gen-page-test -g -y
SKILL.md
Frontmatter
{
    "name": "gen-page-test",
    "description": "一键生成 PageObject + 配套测试。触发:新增页面、生成页面、gen page、create page、添加页面对象。"
}

Gen Page Test — 一键生成 PageObject + 测试

使用方式

用户输入:为 <中文页面名>(URL: https://...)生成页面对象和测试,测试点:点击A、填写B、查看C

输入参数

参数 必填 说明 示例
页面名称 中文业务名称 课程管理页
页面 URL 真实可访问的页面 URL(已认证态) https://example.com/xxx
角色 项目中已有的角色(如 <role> 对应角色
类名 否(自动推导) PascalCase <Role><Page>Page
回归测试点 需要覆盖的交互元素 点击新建、搜索、删除

类名推导规则

  • 非特定角色:中文名 → 英文翻译 → PascalCase + Page 后缀(如 <Feature>Page
  • 特定角色:<Role> + 英文翻译 → PascalCase + Page 后缀(如 <Role><Feature>Page
  • 角色文件名前缀:<role>_(如 <role>_<feature>_page.py

项目中已有的具体类名清单见 docs/pages-catalog.md


执行流程(6 步)

Step 1:访问真实页面获取 DOM

browser_navigate(必要时先 /entry?token=xxx)→ browser_snapshotbrowser_evaluate 提取元素。按 locator-replacer 六级优先级(P0 → P5)选出稳定定位符。

Step 2:生成 PageObject 文件

创建 pages/<snake_name>_page.py

# 页面名称:<中文名>
from pages.base_page import BasePage


class <ClassName>(BasePage):
    # 定位符 — 来自真实页面(<日期>,URL: <页面URL>)
    <LOCATOR_1> = "<真实定位符>"    # P<0-5>: 说明
    <LOCATOR_2> = "<真实定位符>"    # P<0-5>: 说明
    PAGE_IDENTIFIER = "<页面标识定位符>"    # P<0-5>: 说明

    def click_<method_1>(self):
        self.click(self.<LOCATOR_1>)

    def click_<method_2>(self):
        self.click(self.<LOCATOR_2>)

    def is_page_loaded(self) -> bool:
        return self.is_visible(self.PAGE_IDENTIFIER)

Step 3:更新 pages/__init__.py

from pages.<snake_name>_page import <ClassName>

# 在 __all__ 列表中追加 "<ClassName>"

Step 4:在对应角色测试文件追加 story

目标文件:对应角色 tests/<role>/test_<role>_flow.py(继承 <Role>BaseTest)中的 Test<Role>Flow 类。

对应角色示例:

@allure.story("<页面中文名>")
def test_<snake_name>(self):
    page_obj = <ClassName>(self.<role>_page)
    self.<role>_page.goto("<页面URL>")
    assert page_obj.is_page_loaded(), "<页面中文名>加载失败"
    page_obj.click_<method_1>()
    ...

对应角色使用 self.<role>_page,且若脱离门户布局须往返闭合(见 case-round-trip):

@allure.story("<页面中文名>")
def test_<snake_name>(self):
    self.<role>_home.click_<navigation>()
    page_obj = <ClassName>(self.<role>_page)
    assert page_obj.is_page_loaded(), "<页面中文名>加载失败"
    page_obj.click_<method_1>()
    page_obj.click_back_to_<landing>()
    assert self.<role>_home.is_page_loaded(), "返回起点失败"

Step 5:真实环境验证

pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_<snake_name>"

⚠️ 运行前向用户确认 --env(见 agent-behavior.md P0.2)。

失败时 → locator-replacer 重新抽取定位符,或 test-runner 分析失败类型。

Step 6:同步更新 docs/

  • docs/pages-catalog.md:追加新页面类 + 方法表
  • docs/regression-points.md:追加回归点表 + 关键定位符段

生成后检查清单

通用检查(命名/导入/is_page_loaded/allure)→ coding-conventions.md "新增测试检查清单"

  • 定位符均来自真实页面 DOM,行尾标注 P0~P5 级别
  • pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_<name>" 通过
  • 对应角色的 docs/pages-catalog.md 已追加
  • 对应角色的 docs/regression-points.md 已追加
  • 若跳转目标脱离门户布局,已套用 case-round-trip
将占位定位符替换为真实选择器。按六级优先级(语义、文本、表单属性、稳定类名、data-testid、相对路径)逐级尝试,自动识别并规避哈希类名,确保定位稳定抗重构。
替换定位符 locator selector 定位失败 元素找不到 DOM 分析
zh/skills/locator-replacer/SKILL.md
npx skills add DanielSuo117/velocitai --skill locator-replacer -g -y
SKILL.md
Frontmatter
{
    "name": "locator-replacer",
    "description": "替换占位定位符为真实选择器(六级优先级)。触发:替换定位符、locator、selector、定位失败、元素找不到、DOM 分析。"
}

Locator Replacer — 定位符替换 Skill

适用场景

  • 将页面对象中 text= 占位定位符替换为真实选择器
  • 前端构建后原有选择器失效,需要重新定位
  • 新增页面元素需要选择最佳定位策略

定位符选择策略(六级优先级)

按优先级从高到低,逐级尝试,选中即停。优先使用页面上已有的语义化信息,避免依赖需要前端额外配合的属性:

P0: 用户可见的语义化元素(Role + Text)

# 通过 ARIA role + 可见文本定位
LOGIN_BTN = "role=button[name='登录']"
NAV_LINK = "role=link[name='<功能名>']"
SEARCH_INPUT = "role=textbox[name='搜索']"
  • 基于无障碍语义,与用户看到的界面一致
  • 抗 CSS 重构:role 不依赖 class/id
  • 抗构建工具:不受 Webpack/Vite 哈希类名影响
  • 中文文案变更时需要同步更新

P1: 可见文本定位

# 精确匹配
FEATURE_BTN = "text=<功能名>"
# 包含匹配(文本可能嵌套在子元素中)
FEATURE_TAB = "text=<功能名>"
  • 简单直接,不依赖任何额外属性
  • 适合文案稳定、元素唯一的场景
  • 注意: text= 是当前项目的占位方案,如果分析后发现 text= 已经足够稳定且元素唯一,可以保留并标记 # P1: text(已验证唯一)

P2: 表单特有属性

# placeholder 定位
EMAIL_INPUT = "[placeholder='请输入邮箱']"
# label 关联定位
PASSWORD_INPUT = "css=input[name='password']"
# type 属性
SUBMIT_BTN = "css=button[type='submit']"
  • 仅适用于表单元素(input/select/textarea/button)
  • name 属性通常由后端约定,相对稳定
  • placeholder 跟随文案,稳定性中等

P3: 稳定的 CSS Class / ID

# 业务语义类名 — 稳定
COURSE_CARD = "css=.list-card"
LOGIN_FORM = "css=#login-form"
NAV_MENU = "css=.main-navigation"

# ⚠️ 以下是哈希类名 — 绝对禁止使用
# BAD: "css=.sc-bdVaJa.bVjGWg"         (styled-components 哈希)
# BAD: "css=.css-1a2b3c"                (CSS Modules 哈希)
# BAD: "css=[class*='_component_']"     (Vite CSS Modules)

哈希类名识别规则(必须跳过):

构建工具 哈希类名特征 示例
styled-components 随机字母组合 .sc-bdVaJa, .bVjGWg
CSS Modules 下划线 + 哈希 .header_abc123, ._component_1x2y3
Vite 短哈希后缀 .module_1a2b3c
Tailwind JIT 动态工具类 .[\31 /2], .[color:red]
Emotion 前缀 css- .css-1a2b3c

可用的类名特征:

  • 包含业务语义:.list-card, .login-form, .nav-menu
  • 遵循 BEM 命名:.header__title, .card--active
  • 带有明确前缀:.app-feature-list, .app-header

P4: data-testid 专用测试属性

# Playwright locator 写法
SUBMIT_BTN = "[data-testid='submit-button']"
  • 不受样式重构、文案修改、构建工具影响
  • 需要前端开发配合添加,无法独立完成
  • 适合 P0~P3 均无法稳定定位的复杂元素
  • 如果页面已有 data-testid,可以直接使用,但不必强求前端为所有元素添加

P5: 相对 XPath / CSS 结构定位(最后手段)

# ✅ 相对路径 — 从稳定祖先节点出发
SUBMIT_BTN = "xpath=//div[@class='login-form']//button[@type='submit']"
FIRST_COURSE = "css=.course-list > .course-item:first-child"

# ❌ 绝对路径 — 绝对禁止
# BAD: "xpath=/html/body/div[1]/div[2]/ul/li[3]/button"

XPath 编写规则:

  • 从离目标元素最近的稳定祖先节点开始
  • 祖先节点用业务语义属性锚定(class/id/data-*)
  • 路径层级不超过 3 层
  • 禁止使用绝对路径(从 /html/body 开始)
  • 禁止纯数字索引定位(div[3]),除非是列表且确实需要第 N 项

执行流程

Step 1: 分析目标页面 DOM

使用 agent-browser 打开目标页面,获取交互元素信息(工具选型遵循 agent-behavior P0.4,agent-browser 未安装时降级到 Playwright MCP):

操作步骤:
1. browser_navigate → 访问目标页面(已认证状态)
2. browser_snapshot → 获取页面无障碍树(accessibility tree)
3. browser_evaluate → 执行 JS 提取元素属性:
   - document.querySelectorAll('[data-testid]')  → 已有 testid
   - document.querySelectorAll('[role]')          → ARIA role
   - document.querySelectorAll('button, a, input') → 交互元素

Step 2: 为每个元素选择定位策略

对照页面对象中的每个占位定位符,按 P0→P5 逐级尝试,命中即停:

检查顺序 条件 使用
P0 有 role + name role=button[name='...']
P1 text= 唯一且稳定 text=...(标记"已验证唯一")
P2 表单属性(placeholder/name/type) [placeholder='...']
P3 稳定 CSS class(非哈希) css=.xxx
P4 有 data-testid [data-testid='...']
P5 以上均无 相对 XPath(≤3 层)

Step 3: 更新页面对象

class <PageName>(BasePage):
    # 定位符 — 已替换为真实选择器(<日期>)
    <PRIMARY_ACTION_BTN> = "role=button[name='<按钮文案>']"  # P0: ARIA role
    <SECONDARY_LINK> = "text=<链接文案>"                      # P1: text(已验证唯一)
    PAGE_IDENTIFIER = "role=heading[name='<页面标题>']"       # P0: ARIA role

更新规则:

  • 在定位符行尾注释标注选择级别(# P0: ARIA role / # P1: text / ... / # P4: testid / # P5: XPath
  • 如果 text= 经验证确实稳定且唯一,注释标记 # P1: text(已验证唯一)
  • 删除原来的 # 定位符 — 后续替换为实际选择器 注释
  • 更新为 # 定位符 — 已替换为真实选择器(日期)

Step 4: 验证

# 在对应角色的回归文件中跑指定用例(运行前向用户确认 --env,见 agent-behavior P0.2)
pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_<story_name>"

Step 5: 同步更新文档

更新 docs/regression-points.md 中对应 PageObject 的"关键定位符"段。


定位符质量检查清单

替换完成后,逐项确认:

  • 没有使用绝对 XPath(/html/body/...
  • 没有使用哈希类名(sc-xxx, css-xxx, _module_xxx
  • 没有使用纯数字索引(div[3],除非列表取第 N 项)
  • 每个定位符行尾标注了选择级别(# P0 ~ # P5
  • XPath 层级不超过 3 层
  • 主流程测试在真实环境通过

常见问题处理

同一文本出现多次导致 text= 不唯一

降级到 P3/P5,用父容器 + 文本组合:css=.list-card:first-child >> text=查看详情

元素无任何稳定属性

向前端团队提出添加 data-testid(P4)需求,附元素清单;临时用 P5 相对 XPath 过渡。

前端构建后选择器批量失效

检查是否使用了哈希类名(违反 P3)→ 优先升级到 P0/P1(不受构建影响)→ 文案变更则对照新文案更新。

定义页面加载断言方法is_page_loaded(),提供A/B/C/D/E五种验证模式及决策树。通过验证关键元素渲染而非HTTP状态,确保页面正常加载,涵盖单元素、组合验证、激活态及白屏检测等场景。
页面断言 is_page_loaded page loaded 页面加载验证
zh/skills/page-load-assertion/SKILL.md
npx skills add DanielSuo117/velocitai --skill page-load-assertion -g -y
SKILL.md
Frontmatter
{
    "name": "page-load-assertion",
    "description": "is_page_loaded() 四种验证模式与调用层级。触发:页面断言、is_page_loaded、page loaded、页面加载验证。"
}

Page Load Assertion — 页面加载断言方法论

核心思路

页面"正常"的判断不是检查 HTTP 状态码,而是验证用户能看到的关键元素确实渲染出来了。通过每个 PageObject 的 is_page_loaded() 方法统一实现。


is_page_loaded() 的四种验证模式

按严格程度从低到高:

模式 A:单元素验证(最简单)

检查该页面独有的标志性容器是否可见。适用于结构简单、一个元素就能区分的页面:

PAGE_CONTAINER = "css=.feature-container"    # P3

def is_page_loaded(self) -> bool:
    return self.is_visible(self.PAGE_CONTAINER)

模式 B:双元素 AND 组合(推荐默认)

容器 + 内容 双重验证,确保"页面骨架到位 + 数据渲染完成":

PAGE_CONTAINER = "css=.feature-container"    # P3
PAGE_TITLE = "text=欢迎使用"                  # P1

def is_page_loaded(self) -> bool:
    return self.is_visible(self.PAGE_CONTAINER) and self.is_visible(self.PAGE_TITLE)

模式 C:多元素 + 激活态验证(最严格)

在结构和内容基础上,还验证当前 UI 状态(如某个 tab 处于 active)。适用于承担"起点状态判断"语义的页面:

PAGE_TITLE = "css=span.page-title"                                    # P3
TAB_BAR = "css=ul.tab-bar"                                            # P3
DEFAULT_TAB_ACTIVE = "css=ul.tab-bar li.tab.active >> text=默认Tab"    # P3+P1

def is_page_loaded(self) -> bool:
    return (
        self.is_visible(self.PAGE_TITLE)
        and self.is_visible(self.TAB_BAR)
        and self.is_visible(self.DEFAULT_TAB_ACTIVE)
    )

注意:状态类 selector(.active / aria-selected 等)必须配 text 锁定,否则切换 tab 后 .active 跟随新激活对象漂移。

模式 D:委托给语义方法

把验证逻辑提取为有业务含义的方法名,便于 setup fixture 中单独调用:

def is_page_loaded(self) -> bool:
    return self.is_login_success()

def is_login_success(self) -> bool:
    return self.is_visible(self.SUCCESS_FLAG) and self.is_visible(self.NAV_LIST)

模式选择决策树

新页面需要 is_page_loaded()
│
├─ 页面结构简单,一个独有容器即可区分?
│  └─ 是 → 模式 A(单元素)
│
├─ 需要确认内容也渲染到位?
│  └─ 是 → 模式 B(容器 + 内容,推荐默认)
│
├─ 该页面是复位 fixture 的起点,需要判断 UI 状态?
│  └─ 是 → 模式 C(多元素 + 激活态)
│
├─ 验证逻辑在 setup 中也需要单独调用?
│  └─ 是 → 模式 D(委托给语义方法)
│
└─ 容器可能存在但内容未渲染(SPA 子 Tab 切换 / 异步加载)?
   └─ 是 → 模式 E(JS evaluate 检查子元素数量)

验证元素的选择原则

原则 说明
选该页面独有的元素 不选通用 header/footer,选只有这个页面才有的容器或文本
优先选结构性容器 .feature-container.detail-wrapper,页面骨架不易变
文本元素锁定业务语义 text=欢迎使用,确认内容渲染到位
状态类 selector 配 text .active 会随操作漂移,必须加 >> text=具体文本 锁定
不选动态数据 具体数字、用户名等因数据变化导致断言不稳定

断言目标的三级稳定性

核心原则:优先选模板级文本(页面框架固定标题),禁止选数据级内容(动态计数、日期)。多视图场景中每个断言目标须同时满足「模板级 + 视图独占」。

完整规则与反例/正例 → assertion-patterns.md "断言目标稳定性"章节。

模式 E:白屏检测 + 空数据 vs 有数据 三态判断

SPA 应用中,容器 div 可能存在于 DOM 且 is_visible 返回 true,但内部组件未挂载(白屏)。此时模式 A/B 的 is_visible(容器) 会误判为正常。

页面内容区有三种状态,必须分别处理

页面内容区状态
│
├─ 白屏(渲染失败)
│  内容区子元素数 = 0,组件未挂载
│  → 判定:❌ 测试失败,报"内容区域未渲染,疑似白屏"
│
├─ 空数据(无业务数据但渲染正常)
│  内容区子元素数 > 0,但无业务数据项
│  通常有空状态提示(图片 + "暂无数据"/"还没有收藏题哦"等文案)
│  → 判定:✅ 渲染正常,页面确实没有业务数据
│
└─ 有数据(正常渲染)
   内容区子元素数 > 0,且有业务数据项
   → 判定:✅ 渲染正常

实现模板

# 定位符
CONTENT_CONTAINER = "css=<内容主容器>"                 # 页面骨架容器
EMPTY_STATE = "css=<空状态提示元素>"                    # 空状态组件(图片+文案)
DATA_ITEM = "css=<业务数据项>"                          # 单个数据条目

def get_content_render_state(self) -> str:
    """返回内容区渲染状态:'blank' / 'empty' / 'loaded'。"""
    return self.page.evaluate("""() => {
        const container = document.querySelector('<内容主容器选择器>');
        if (!container) return 'blank';
        const contentArea = container.querySelector('<内容区域选择器>');
        if (!contentArea || contentArea.children.length === 0) return 'blank';
        return 'loaded';
    }""")

def is_content_rendered(self) -> bool:
    """白屏检测:容器存在但内容区无子元素 → False。
    空数据和有数据都算渲染成功 → True。"""
    return self.get_content_render_state() != 'blank'

def has_data_items(self) -> bool:
    """是否有业务数据(排除空状态)。"""
    return self.get_element_count(self.DATA_ITEM) > 0

用例中的三态断言

# 第一层:渲染检测(白屏 vs 已渲染)
is_rendered = page_obj.is_content_rendered()
if not is_rendered:
    assert False, "内容区域未渲染,疑似白屏"

# 第二层(可选):空数据 vs 有数据
# 根据业务预期决定是否需要进一步区分
if page_obj.has_data_items():
    # 有数据 → 可以进一步验证数据内容
    pass
else:
    # 空数据 → 记录到报告,不视为失败
    allure.attach("该Tab当前无业务数据(空状态)", ...)

⚠️ 陷阱

错误做法 问题 正确做法
innerText.length > N 判断白屏 空状态提示文案可能很短,被误判为白屏 children.length > 0
空数据等同于失败 无收藏/无错题是合理的业务状态 白屏才失败,空数据只记录
只检查容器 is_visible 容器 div 可能存在但子组件未挂载 JS evaluate 检查子元素

适用场景

  • 同一容器内切换多个子 Tab,部分 Tab 可能无业务数据
  • 异步加载组件可能因接口超时未渲染
  • 页面骨架先渲染、内容延迟加载的 SPA 路由

调用层级

  1. class setup:登录后 assert home.is_page_loaded(), "首页加载失败"
  2. function resetif not self.home.is_page_loaded(): click_nav_home(); assert ...
  3. 用例体:每次跳转后 assert target.is_page_loaded(), "<页面>加载失败"

规则约束

  1. 每个 PageObject 必须实现 is_page_loaded()——返回 bool,放在类的最后一个方法
  2. is_page_loaded() 内部只用 is_visible() 组合,不抛异常
测试失败时免登直达目标页面原地排查修复。解析失败信息,构造URL跳转,通过决策树诊断定位符、超时或断言问题,调用对应Skill修复并验证,跳过完整流程以加速纠错。
排查 纠错 debug 定位符失败 超时 元素找不到 断言失败
zh/skills/quick-debug/SKILL.md
npx skills add DanielSuo117/velocitai --skill quick-debug -g -y
SKILL.md
Frontmatter
{
    "name": "quick-debug",
    "description": "测试失败时免登跳转原地排查修复。触发:排查、纠错、debug、定位符失败、超时、元素找不到、断言失败。"
}

Quick Debug — 快速纠错验证

核心思路

跳过从登录到问题节点的完整流程,直接免登到目标页面原地排查。

利用项目已有的 token URL 免登机制,配合浏览器工具直接在问题页面进行 DOM 探索、定位符验证、等待策略调整,修复后原地验证,全程不重跑完整测试流程。


主干流程(5 步)

Step 1: 解析失败信息
    ↓
Step 2: 构造免登跳转 URL
    ↓
Step 3: 免登跳转到目标页面
    ↓
Step 4: 诊断问题(决策树)
    ↓
Step 5: 修复 + 原地验证

Step 1: 解析失败信息

从堆栈 / 用户描述提取:失败用例名、错误类型(TimeoutError / AssertionError)、失败定位符、失败时所在 URL。信息不足时读取测试代码反推。

Step 2: 构造免登跳转 URL

从配置文件读取对应环境的 token。

⚠️ 免登前必须向用户确认 --env(见 agent-behavior P0.2)。

两种情况:

情况 策略
目标页面有明确 URL 路径 先免登(/entry?token=<JWT>),再导航到目标路径
目标页面需要交互才能到达(弹窗、新 tab 等) 免登到最近的可直接访问的父级页面,执行最少量的导航操作到达目标

Step 3: 免登跳转到目标页面

browser_navigate → <base_url>/entry?token=<JWT> → 导航到目标路径 → 验证 is_page_loaded()。免登失败(跳回登录页)→ 提示用户刷新 token。

Step 4: 诊断问题

见下一节「诊断决策树」。

Step 5: 修复 + 原地验证

根据诊断结果调用对应 skill:

诊断结果 调用 skill
定位符问题 locator-replacer
等待策略问题 wait-strategy
页面加载断言问题 page-load-assertion
页面大改版 报告用户,建议 gen-page-test 重新生成
断言预期值变更 报告差异,由用户决策是否更新预期值

原地验证: 修复后在当前浏览器页面直接验证(用 Playwright MCP 重新执行失败操作),确认无误后建议用户重跑完整测试。


诊断决策树(Step 4 展开)

先用 agent-browser 快速扫描当前页面 DOM(未安装时降级到 Playwright MCP),然后根据错误类型走对应分支:

错误类型判定
    ├── A. 定位符问题(元素找不到 / 匹配多个)
    ├── B. 超时问题(元素存在但加载慢 / 网络延迟)
    ├── C. 断言问题(元素存在但内容/状态不符预期)
    └── D. 页面结构变化(页面整体改版 / 路由变更)

分支 A: 定位符问题

判定依据: locator.count() == 0strict mode violation(匹配多个)

排查步骤:

  1. 用 agent-browser 抓取问题区域的 DOM 快照
  2. 对比 PageObject 定位符与实际 DOM,判定原因:类名变更 / 文本变更 / 动态渲染未完成

修复路径 → 调用 locator-replacer skill

分支 B: 超时问题

判定依据: TimeoutError 且元素在更长等待后可出现,或 networkidle 未达成

排查步骤:

  1. 检查当前页面 URL 是否正确(排除导航偏差),目标元素是否存在但不可见(hidden / 被遮挡)
  2. 判定原因:隐式等待不够 / SPA 渲染延迟 / 网络接口慢

修复路径 → 调用 wait-strategy skill 或调整超时配置

分支 C: 断言问题

判定依据: AssertionError,元素存在且可见,但值/状态不符预期

排查步骤:

  1. 获取元素实际文本/属性值,与测试代码预期值对比
  2. 判定原因:业务数据变化 / 文案更新 / 环境差异

修复路径 → 报告差异并建议用户决策(通常是业务变更而非代码 bug)

分支 D: 页面结构变化

判定依据: is_page_loaded() 失败 或 当前 URL 与预期不一致

排查步骤:

  1. 用 agent-browser 抓取全页面快照,与 PageObject 定位符批量比对
  2. 判定原因:路由变更 / 页面改版 / 权限变化导致重定向

修复路径 → 小范围改动调用 locator-replacer;大范围改版报告用户,建议 gen-page-test

回退机制

如果归类后排查未果,回退到上一级重新分类。最多回退一次,仍无法定位则整理已收集信息报告用户。


浏览器工具选择

DOM 探索/定位符采集 → agent-browser(优先);精确验证/断言 → Playwright MCP;agent-browser 未安装 → 全程 Playwright MCP(P0.4 降级)。


检查清单

每次 quick-debug 结束前,必须确认:

  • 已明确失败原因并归类(A/B/C/D 哪个分支)
  • 修复动作已执行(或已报告用户需人工决策)
  • 原地验证通过(修复后的定位符/等待在当前页面生效)
  • 建议用户重跑完整测试确认回归
  • 保存操作验证失败时,参考 save-verify-strategy(Toast 时序 / 重定向检测 / 数据对比)
提供表单保存后的验证策略,涵盖Toast、重定向及数据对比三种方式。重点解决macOS富文本全选快捷键(Meta+A)错误导致的残留问题,并强调Toast检测的时序陷阱及避免假设重定向目标的最佳实践。
表单保存操作 富文本内容填写与清空 保存成功状态验证 macOS环境下的键盘快捷键处理
zh/skills/save-verify-strategy/SKILL.md
npx skills add DanielSuo117/velocitai --skill save-verify-strategy -g -y
SKILL.md
Frontmatter
{
    "name": "save-verify-strategy",
    "description": "表单保存后验证(Toast \/ 重定向 \/ 数据对比 \/ 富文本清空)。触发:保存验证、toast、重定向检测、Meta+A、保存成功判断。"
}

表单保存验证策略


一、macOS 富文本编辑器全选必须用 Meta+A

macOS Chromium 中 Control+A 是 Emacs 快捷键(光标移到行首),不是"全选"。在 CKEditor / TinyMCE / Quill 等富文本编辑器中,Control+A 无法选中全部内容,导致旧内容残留。

❌ 反例:

def fill_rich_text(self, text: str):
    self.click(self.EDITOR)
    self.page.keyboard.press("Control+A")   # macOS 上只移动光标到行首
    self.page.keyboard.press("Backspace")   # 只删一个字符,旧内容残留
    self.page.keyboard.type(text)           # 新内容追加在旧内容后面

✅ 正例:

def fill_rich_text(self, text: str):
    self.click(self.EDITOR)
    self.page.keyboard.press("Meta+A")      # macOS Command+A = 全选
    self.page.keyboard.press("Backspace")   # 清空全部内容
    self.page.keyboard.type(text)           # 写入纯新内容

排查信号:填入的数据末尾多了旧内容残片(如 "2026-05-06 10:30:00123" 尾部多了 123)→ 全选快捷键无效。


二、保存后验证策略:三级选择

保存后行为 验证方式 可靠性 适用场景
显示 Toast is_visible(toast_locator) — 必须在 networkidle 后立即检测 低(2-3s 消失) 有明确 Toast 的场景
页面重定向 检测编辑页特征元素消失 wait_for(state="hidden") 中(持久状态) 保存后自动跳转的场景
无 UI 反馈 写入标记数据 → 重新打开 → 读取对比 高(数据级验证) 无 Toast、不确定是否重定向

原则:三级可组合使用。优先用持久状态变更,Toast 仅作辅助。

2.1 Toast 时序陷阱

click_save() 方法中的 wait_for_timeout() 会消耗 Toast 存活时间。必须在 networkidle 后立即检测 Toast,不能先等待再检测。

❌ 反例:

def click_save(self):
    self.click(self.SAVE_BTN)
    self.page.wait_for_load_state("networkidle")
    self.page.wait_for_timeout(3000)   # Toast 在这 3s 内出现又消失了

# 调用方
assert page.is_visible(toast)          # Toast 已消失,永远 False

✅ 正例:

def click_save(self):
    self.click(self.SAVE_BTN)
    self.page.wait_for_load_state("networkidle")
    # 不加额外等待 — 让调用方立即检测 Toast

# 调用方
is_ok = page.is_visible(toast, timeout=10000)   # 立即开始等 Toast
page.wait_for_timeout(3000)                      # 检测完再等页面稳定

2.2 重定向检测

保存后页面可能重定向离开编辑页。通过检测编辑页特征元素消失来确认保存成功:

def is_save_success(self) -> bool:
    """编辑页 header 消失 = 保存成功并已离开编辑页"""
    try:
        self.page.locator(self.EDIT_PAGE_HEADER).first.wait_for(
            state="hidden", timeout=15000
        )
        return True
    except Exception:
        return False

2.3 重定向目的地不可假设

保存后的重定向目的地可能因导航上下文不同而异(直接 URL 访问 vs SPA 内跳转 vs 新 tab 中操作)。不要断言重定向到某个特定页面。

❌ 反例:

# 假设保存后一定重定向到首页
redirected_home = HomePage(page)
assert redirected_home.is_page_loaded()   # 实际可能跳到其他页面

✅ 正例:

# 只验证离开了编辑页,不假设去了哪里
assert edit_page.is_save_success()        # 编辑页 header 消失 = 保存成功

三、多 tab 保存后:关闭旧 tab,从原始 tab 重入

保存后如果需要重新访问同一页面验证数据,不要尝试在重定向后的页面内导航(重定向目的地不确定,元素可能不存在)。关闭旧 tab,从原始 tab(状态稳定)重新打开新 tab 进入。

❌ 反例:

# 保存后尝试在重定向页面内导航回去
redirected_page = SomePage(new_page)
assert redirected_page.is_page_loaded()    # 重定向目的地不确定 → 失败
redirected_page.click(breadcrumb)          # 元素可能不存在

✅ 正例:

# 保存后关闭旧 tab
finally:
    new_page.close()

# 原始 tab 状态稳定,从这里重新进入
assert original_home.is_page_loaded()
new_page_2 = original_home.open_target_in_new_tab(name)
try:
    # 在新 tab 中导航到编辑页 → 验证数据
finally:
    new_page_2.close()

四、数据对比验证模式(最终验证)

当没有 Toast 或 UI 反馈时,用「写入标记 → 保存 → 重新打开 → 读取对比」作为最终验证闭环。

# ── 写入阶段 ──
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
edit_page.fill_description(timestamp)
write_test_data("标记键名", timestamp)          # 持久化到配置文件

# ... 保存操作 ...

# ── 验证阶段(重新打开编辑页)──
saved = read_test_data().get("标记键名")
actual = edit_page.get_description_text()
allure.attach(saved, name="期望时间戳", ...)     # 无论成败都记录到报告
allure.attach(actual, name="实际时间戳", ...)
assert actual == saved, (
    f"数据不一致:期望='{saved}',实际='{actual}'"
)

关键要点

  1. 标记数据必须写入配置文件(跨用例共享,非内存变量),便于后续用例复用
  2. 无论断言成败,都用 allure.attach() 将期望值和实际值写入报告
  3. 断言失败信息必须包含两个值的对比,方便排查
用于运行pytest测试并分析结果。支持单故事调试和全量回归,强制确认环境参数。根据失败特征(定位符、超时、断言、认证)分类诊断,提供Allure报告查看及结构化摘要输出。
运行测试 跑测试 run test pytest 失败分析 allure
zh/skills/test-runner/SKILL.md
npx skills add DanielSuo117/velocitai --skill test-runner -g -y
SKILL.md
Frontmatter
{
    "name": "test-runner",
    "description": "运行 pytest + 结构化分析失败原因。触发:运行测试、跑测试、run test、pytest、失败分析、allure。"
}

Test Runner — 运行测试与结果分析

⚠️ 运行前必须向用户确认 --env(见 agent-behavior.md P0.2)。 即使 config/settings.py::DEFAULT_ENV="prod"、即使下方示例写 --env=<pre|prod>(示例 ≠ 默认值),仍必须问用户。

运行前准备

# 确认虚拟环境已激活
source .venv/bin/activate

# 确认依赖已安装
pip list | grep -E "playwright|pytest|allure"

运行模式

所有测试均基于真实环境(URL + token)运行。

模式 1: 单个 story(调试 / 定位符验证)

场景: 替换定位符后验证、调试单个页面回归点

# 对应角色
pytest tests/<role>/test_<role>_flow.py --env=<pre|prod> -v -k "test_xxx"
# 对应角色
pytest tests/<role>/test_<role>_home.py --env=<pre|prod> -v -k "test_<case_name>"

模式 2: 按角色 / 全量回归

场景: 发版前验证、完整回归

# <角色A>全量
pytest tests/<roleA>/ --env=<pre|prod>
# <角色B>全量
pytest tests/<roleB>/ --env=<pre|prod>
# 全量
pytest tests/ --env=<pre|prod>

查看 Allure 报告

# 启动报告服务器查看(运行完测试后直接可用)
allure serve reports/allure-results

# 生成静态报告(可部署)
allure generate reports/allure-results -o reports/allure-report --clean

结果分析

pytest 输出解读

tests/test_xxx.py::TestXxx::test_method PASSED    → 通过
tests/test_xxx.py::TestXxx::test_method FAILED    → 失败(需分析)
tests/test_xxx.py::TestXxx::test_method ERROR     → fixture/setup 异常
tests/test_xxx.py::TestXxx::test_method SKIPPED   → 跳过

失败分类与处理

运行失败后,按以下分类诊断:

类型 A: 定位符失效

特征:

TimeoutError: Timeout 10000ms exceeded.
  waiting for locator("text=<功能名>")

处理: 触发 locator-replacer skill,使用 agent-browser 访问真实页面重新抽取定位符(工具选型遵循 agent-behavior P0.4)。

类型 B: 页面加载超时

特征:

TimeoutError: page.wait_for_load_state: Timeout 30000ms exceeded.
  waiting for "networkidle"

处理:

  1. 检查网络连通性(hosts 配置、VPN)
  2. 检查 token 是否过期
  3. 增大 DEFAULT_TIMEOUT(临时措施)

类型 C: 断言失败

特征:

AssertionError: <页面中文名>加载失败
assert False

处理:

  1. 页面是否正确导航到目标 URL
  2. PAGE_IDENTIFIER 定位符是否仍然有效
  3. 页面内容是否因版本更新发生变化

类型 D: 认证失败

特征:

AssertionError: Token 登录失败

处理:

  1. 检查 config/settings.py 中的 token 是否有效
  2. 检查 --env 参数是否选对
  3. 检查本地 hosts 是否指向正确的服务器 IP

💡 快速排查建议

当失败需要深入排查时(定位符失效、页面结构变化等),建议进入 quick-debug 模式:通过 token 免登直接跳转到问题页面,避免从登录重走完整流程。


结果摘要模板

运行完成后,按以下格式输出摘要:

## 测试运行摘要

**运行模式:** 单个 story / 主流程全量
**环境:** pre / prod
**时间:** 2026-04-15 15:30

### 结果

| 状态 | 数量 |
|------|------|
| PASSED | X |
| FAILED | X |
| ERROR | X |
| SKIPPED | X |

### 失败详情(如有)

| 测试用例 | 失败类型 | 原因 | 建议操作 |
|----------|---------|------|---------|
| test_xxx | A: 定位符失效 | text=xxx 不唯一 | 使用 locator-replacer |
| test_yyy | D: 认证失败 | token 过期 | 更新 settings.py |
UI回归测试子技能的路由入口,协调页面生成、定位器替换、功能添加及故障修复等复合场景。通过串联gen-page-test、test-runner等专用工具,实现从代码审查到自动化验证的完整测试工作流。
需要执行UI回归测试流程 需组合多个测试工具完成复杂任务
en/skills/SKILL.md
npx skills add DanielSuo117/velocitai --skill ui-automation-harness -g -y
SKILL.md
Frontmatter
{
    "name": "ui-automation-harness",
    "description": "UI regression test sub-skill router entry point."
}

UI Regression Testing — Main Skill

Composite Scenarios

  • Add new page and run it → gen-page-testtest-runner
  • Replace locators and verify → locator-replacertest-runner
  • Add features to an existing page → add-regression-pointtest-runner
  • Navigation test cases that leave the portal layout → add-regression-point + case-round-triptest-runner
  • Design load assertion for a new page → page-load-assertiongen-page-test
  • Review changes → code-review-graphtest-runner
  • Locate a bug → code-review-graph (debug workflow)
  • Safe refactoring → code-review-graph (refactor workflow)
  • Fix failing tests → test-runner (detect failure) → quick-debug (login-free triage + auto-fix) → test-runner (regression verification)
  • Verify after form save → save-verify-strategy (choose verification method) → test-runner (run verification)

Reference Index

Coding conventions, project facts, and behavior rules are all guided by CLAUDE.md and not repeated here.

Key references:

基于AST代码知识图谱的工具集,用于变更审查、代码探索、问题调试及安全重构。通过图谱工具优先于文本搜索,提供影响分析、调用链追踪及测试覆盖检查,旨在提升代码理解与修改效率。
代码审查 影响分析 爆炸半径评估 代码重构 Bug调试 代码库结构探索
skills/code-review-graph/SKILL.md
npx skills add DanielSuo117/velocitai --skill code-review-graph -g -y
SKILL.md
Frontmatter
{
    "name": "code-review-graph",
    "description": "AST 知识图谱:变更审查、探索、调试、重构。触发:知识图谱、代码审查、影响分析、爆炸半径、重构。"
}

code-review-graph — 知识图谱工具集

本项目通过 code-review-graph MCP 接入了基于 AST 的代码知识图谱。探索代码时优先使用图谱工具,再降级到 Grep/Glob/Read。

Token 效率规则(全局)

  • 始终先调用 get_minimal_context(task="<你的任务>"),再使用其他图谱工具。
  • 所有调用使用 detail_level="minimal",仅在不够用时升级到 "standard"。
  • 目标:5 次工具调用、800 个输出 token 内完成任务。

工作流一:变更审查

利用知识图谱进行风险感知的代码审查。

步骤

  1. 运行 detect_changes 获取带风险评分的变更分析。
  2. 运行 get_affected_flows 查找受影响的执行路径。
  3. 对高风险函数,运行 query_graph pattern="tests_for" 检查测试覆盖。
  4. 运行 get_impact_radius 理解影响范围。
  5. 对未覆盖测试的变更,建议具体的测试用例。

输出格式

按风险等级(高/中/低)分组,包含:变更内容、测试覆盖状态、改进建议、合并建议。


工作流二:代码库探索

利用图谱快速理解代码库结构。

步骤

  1. 运行 list_graph_stats 查看整体指标。
  2. 运行 get_architecture_overview 了解高层模块结构。
  3. 使用 list_communities 查找主要模块,用 get_community 获取详情。
  4. 使用 semantic_search_nodes 按名称或关键词查找函数/类。
  5. 使用 query_graphcallers_ofcallees_ofimports_of 追踪关系。
  6. 使用 list_flowsget_flow 理解执行路径。

提示

  • 先宏观(统计、架构),再缩小到具体区域。
  • children_of 查看文件内所有函数和类;find_large_functions 定位复杂代码。

工作流三:问题调试

利用图谱系统化追踪和调试问题。

步骤

  1. 使用 semantic_search_nodes 查找与问题相关的代码。
  2. 使用 query_graphcallers_ofcallees_of 追踪调用链。
  3. 使用 get_flow 查看可疑区域的完整执行路径。
  4. 运行 detect_changes 检查近期变更是否引发了问题。
  5. 对可疑文件使用 get_impact_radius 查看受影响范围。

提示

  • 同时检查调用者和被调用者,理解完整上下文。
  • 查看受影响的执行流,找到触发 bug 的入口点。
  • 近期变更是新问题最常见的来源。

工作流四:安全重构

利用图谱自信地规划和执行重构。

步骤

  1. 使用 refactor_tool mode="suggest" 获取重构建议。
  2. 使用 refactor_tool mode="dead_code" 查找死代码。
  3. 重命名时用 refactor_tool mode="rename" 预览所有受影响位置。
  4. 使用 apply_refactor_tool 配合 refactor_id 应用重命名。
  5. 变更后运行 detect_changes 验证影响。

安全检查

  • 应用前始终先预览(rename 模式给出编辑清单)。
  • 大规模重构前先检查 get_impact_radius
  • 使用 get_affected_flows 确保关键路径未被破坏。
  • find_large_functions 识别需要拆分的大函数。
UI回归测试子技能路由入口,根据场景自动组合gen-page-test、test-runner等工具执行新增页面、定位符替换、Bug修复及代码审查等自动化测试任务。
需要执行UI回归测试 发现测试失败需自动纠错 进行代码重构或安全审查
skills/SKILL.md
npx skills add DanielSuo117/velocitai --skill ui-automation-harness -g -y
SKILL.md
Frontmatter
{
    "name": "ui-automation-harness",
    "description": "UI 回归测试子 skill 路由入口。"
}

UI 回归测试 — 主 Skill

组合场景

  • 新增页面并跑通 → gen-page-testtest-runner
  • 替换定位符并验证 → locator-replacertest-runner
  • 给已有页面加功能 → add-regression-pointtest-runner
  • 脱离门户布局的跳转用例 → add-regression-point + case-round-triptest-runner
  • 设计新页面的加载验证 → page-load-assertiongen-page-test
  • 审查变更 → code-review-graphtest-runner
  • 定位 bug → code-review-graph(调试工作流)
  • 安全重构 → code-review-graph(重构工作流)
  • 测试失败纠错 → test-runner(发现失败)→ quick-debug(免登排查 + 自动修复)→ test-runner(回归验证)
  • 表单保存后验证 → save-verify-strategy(选择验证方式)→ test-runner(运行验证)

引用索引

编码规范、项目事实、行为规则等均由 CLAUDE.md 统一指引,此处不重复。

关键引用:

规范Playwright自动化测试中的等待策略。默认使用全局隐式等待覆盖99%场景,显式等待仅用于大文件上传、异步回调等慢路径。禁止硬等待和滥用显式超时,确保测试稳定高效。
等待策略 timeout wait 隐式等待 显式等待
skills/wait-strategy/SKILL.md
npx skills add DanielSuo117/velocitai --skill wait-strategy -g -y
SKILL.md
Frontmatter
{
    "name": "wait-strategy",
    "description": "隐式等待为主、显式等待仅慢路径。触发:等待策略、timeout、wait、隐式等待、显式等待。"
}

等待策略 — 隐式等待为主,显式等待为辅

核心原则

隐式等待覆盖 99% 场景,显式等待仅用于已知的慢路径。


一、隐式等待(默认,全局生效)

什么是隐式等待

通过 page.set_default_timeout(ms) 在 page 创建后立即设置。此后该 page 上所有 Playwright 操作(clickfilllocator.wait_foris_visible 等)自动等待元素就绪,超时前持续重试,无需逐个调用指定 timeout。

配置位置

隐式等待的超时时长统一声明在配置文件中(如 DEFAULT_TIMEOUT 常量),在 fixture 层注入到每个 page 实例:

# 配置文件
DEFAULT_TIMEOUT = 15000   # 15 秒,兼顾网络波动

# fixture 层(function 级 / class 级均需设置)
page = context.new_page()
page.set_default_timeout(DEFAULT_TIMEOUT)

推荐时长

环境 推荐值 说明
内网 / CI 10000 (10s) 网络稳定,无需长等
外网 / 生产 15000 (15s) 兼顾偶发网络波动
弱网测试 20000~30000 专项弱网场景

二、显式等待(仅用于特殊场景)

什么时候用

场景 原因 做法
大文件上传 / 导入 服务器处理耗时远超常规页面操作 self.is_visible(LOCATOR, timeout=60000)
长轮询 / 异步任务回调 后端处理完才刷新前端状态 page.locator(...).wait_for(state="visible", timeout=30000)
第三方服务跳转 OAuth / 支付等外部页面响应不可控 page.wait_for_url("**/callback**", timeout=30000)
首次冷启动 服务刚部署、首次请求慢 对首个导航操作单独加大 timeout

使用方式

调用处传入显式 timeout,覆盖隐式默认值:

# BasePage 封装方法支持 timeout 参数
def is_visible(self, locator, timeout=DEFAULT_TIMEOUT) -> bool:
    ...

# 用例中:仅对已知慢操作显式指定
def test_upload_large_file(self):
    self.page_obj.click_upload()
    assert self.page_obj.is_visible(
        self.page_obj.UPLOAD_SUCCESS_INDICATOR,
        timeout=60000   # 显式等待 60 秒,仅此一处
    ), "大文件上传超时"

禁止事项

  • 禁止 time.sleep() 硬等待 — Playwright 的自动等待机制已覆盖,硬等待浪费时间且不可靠
  • 禁止给每个操作都加显式 timeout — 这等于放弃了隐式等待的统一管理优势
  • 禁止在 PageObject 方法内部硬编码超长 timeout — 超时值应由调用方(用例层)按场景决定

三、wait_for_load_state 与隐式等待的关系

page.wait_for_load_state("networkidle")导航级等待,与元素级隐式等待互补,不冲突:

等待类型 作用层级 触发时机 set_default_timeout 控制
wait_for_load_state 导航 / 网络 页面跳转、AJAX 批量请求后 set_default_navigation_timeout 控制
隐式等待 元素 每次元素操作前

推荐模式:

def click_navigate_to_target(self):
    self.click(self.TARGET_LINK)
    self.page.wait_for_load_state("networkidle")  # 等网络请求完成
    # 之后的 is_visible / click 等操作由隐式等待兜底

检查清单

  • 全局 DEFAULT_TIMEOUT 已在配置文件统一声明
  • 所有 page fixture(function 级 / class 级)均调用了 page.set_default_timeout()
  • PageObject 业务方法中无 time.sleep()
  • 显式 timeout 仅出现在已知慢路径的调用处,而非 PageObject 内部
  • PageObject 的 is_visible() 等方法保留 timeout 参数供调用方覆盖
基于AST的知识图谱工具集,用于代码审查、库探索、调试和重构。优先使用图谱工具以优化Token效率,支持变更分析、影响半径计算及安全重构,提供结构化工作流指引。
知识图谱查询 代码审查 影响分析 爆炸半径评估 代码重构
zh/skills/code-review-graph/SKILL.md
npx skills add DanielSuo117/velocitai --skill code-review-graph -g -y
SKILL.md
Frontmatter
{
    "name": "code-review-graph",
    "description": "AST 知识图谱:变更审查、探索、调试、重构。触发:知识图谱、代码审查、影响分析、爆炸半径、重构。"
}

code-review-graph — 知识图谱工具集

本项目通过 code-review-graph MCP 接入了基于 AST 的代码知识图谱。探索代码时优先使用图谱工具,再降级到 Grep/Glob/Read。

Token 效率规则(全局)

  • 始终先调用 get_minimal_context(task="<你的任务>"),再使用其他图谱工具。
  • 所有调用使用 detail_level="minimal",仅在不够用时升级到 "standard"。
  • 目标:5 次工具调用、800 个输出 token 内完成任务。

工作流一:变更审查

利用知识图谱进行风险感知的代码审查。

步骤

  1. 运行 detect_changes 获取带风险评分的变更分析。
  2. 运行 get_affected_flows 查找受影响的执行路径。
  3. 对高风险函数,运行 query_graph pattern="tests_for" 检查测试覆盖。
  4. 运行 get_impact_radius 理解影响范围。
  5. 对未覆盖测试的变更,建议具体的测试用例。

输出格式

按风险等级(高/中/低)分组,包含:变更内容、测试覆盖状态、改进建议、合并建议。


工作流二:代码库探索

利用图谱快速理解代码库结构。

步骤

  1. 运行 list_graph_stats 查看整体指标。
  2. 运行 get_architecture_overview 了解高层模块结构。
  3. 使用 list_communities 查找主要模块,用 get_community 获取详情。
  4. 使用 semantic_search_nodes 按名称或关键词查找函数/类。
  5. 使用 query_graphcallers_ofcallees_ofimports_of 追踪关系。
  6. 使用 list_flowsget_flow 理解执行路径。

提示

  • 先宏观(统计、架构),再缩小到具体区域。
  • children_of 查看文件内所有函数和类;find_large_functions 定位复杂代码。

工作流三:问题调试

利用图谱系统化追踪和调试问题。

步骤

  1. 使用 semantic_search_nodes 查找与问题相关的代码。
  2. 使用 query_graphcallers_ofcallees_of 追踪调用链。
  3. 使用 get_flow 查看可疑区域的完整执行路径。
  4. 运行 detect_changes 检查近期变更是否引发了问题。
  5. 对可疑文件使用 get_impact_radius 查看受影响范围。

提示

  • 同时检查调用者和被调用者,理解完整上下文。
  • 查看受影响的执行流,找到触发 bug 的入口点。
  • 近期变更是新问题最常见的来源。

工作流四:安全重构

利用图谱自信地规划和执行重构。

步骤

  1. 使用 refactor_tool mode="suggest" 获取重构建议。
  2. 使用 refactor_tool mode="dead_code" 查找死代码。
  3. 重命名时用 refactor_tool mode="rename" 预览所有受影响位置。
  4. 使用 apply_refactor_tool 配合 refactor_id 应用重命名。
  5. 变更后运行 detect_changes 验证影响。

安全检查

  • 应用前始终先预览(rename 模式给出编辑清单)。
  • 大规模重构前先检查 get_impact_radius
  • 使用 get_affected_flows 确保关键路径未被破坏。
  • find_large_functions 识别需要拆分的大函数。
UI回归测试子技能路由入口,协调gen-page-test、test-runner等组件。支持新增页面、替换定位符、功能添加、跳转用例及加载验证等场景,集成代码审查、Bug定位、安全重构与失败纠错流程,遵循CLAUDE.md规范。
需要执行UI回归测试流程 运行或调试UI自动化测试脚本 进行UI代码审查或重构 排查和修复UI测试失败问题
zh/skills/SKILL.md
npx skills add DanielSuo117/velocitai --skill ui-automation-harness -g -y
SKILL.md
Frontmatter
{
    "name": "ui-automation-harness",
    "description": "UI 回归测试子 skill 路由入口。"
}

UI 回归测试 — 主 Skill

组合场景

  • 新增页面并跑通 → gen-page-testtest-runner
  • 替换定位符并验证 → locator-replacertest-runner
  • 给已有页面加功能 → add-regression-pointtest-runner
  • 脱离门户布局的跳转用例 → add-regression-point + case-round-triptest-runner
  • 设计新页面的加载验证 → page-load-assertiongen-page-test
  • 审查变更 → code-review-graphtest-runner
  • 定位 bug → code-review-graph(调试工作流)
  • 安全重构 → code-review-graph(重构工作流)
  • 测试失败纠错 → test-runner(发现失败)→ quick-debug(免登排查 + 自动修复)→ test-runner(回归验证)
  • 表单保存后验证 → save-verify-strategy(选择验证方式)→ test-runner(运行验证)

引用索引

编码规范、项目事实、行为规则等均由 CLAUDE.md 统一指引,此处不重复。

关键引用:

定义Playwright自动化测试的等待策略:默认使用隐式等待覆盖99%场景,显式等待仅用于大文件上传、异步任务等已知慢路径。禁止硬等待,确保超时统一管理与代码可靠性。
等待策略 timeout wait 隐式等待 显式等待
zh/skills/wait-strategy/SKILL.md
npx skills add DanielSuo117/velocitai --skill wait-strategy -g -y
SKILL.md
Frontmatter
{
    "name": "wait-strategy",
    "description": "隐式等待为主、显式等待仅慢路径。触发:等待策略、timeout、wait、隐式等待、显式等待。"
}

等待策略 — 隐式等待为主,显式等待为辅

核心原则

隐式等待覆盖 99% 场景,显式等待仅用于已知的慢路径。


一、隐式等待(默认,全局生效)

什么是隐式等待

通过 page.set_default_timeout(ms) 在 page 创建后立即设置。此后该 page 上所有 Playwright 操作(clickfilllocator.wait_foris_visible 等)自动等待元素就绪,超时前持续重试,无需逐个调用指定 timeout。

配置位置

隐式等待的超时时长统一声明在配置文件中(如 DEFAULT_TIMEOUT 常量),在 fixture 层注入到每个 page 实例:

# 配置文件
DEFAULT_TIMEOUT = 15000   # 15 秒,兼顾网络波动

# fixture 层(function 级 / class 级均需设置)
page = context.new_page()
page.set_default_timeout(DEFAULT_TIMEOUT)

推荐时长

环境 推荐值 说明
内网 / CI 10000 (10s) 网络稳定,无需长等
外网 / 生产 15000 (15s) 兼顾偶发网络波动
弱网测试 20000~30000 专项弱网场景

二、显式等待(仅用于特殊场景)

什么时候用

场景 原因 做法
大文件上传 / 导入 服务器处理耗时远超常规页面操作 self.is_visible(LOCATOR, timeout=60000)
长轮询 / 异步任务回调 后端处理完才刷新前端状态 page.locator(...).wait_for(state="visible", timeout=30000)
第三方服务跳转 OAuth / 支付等外部页面响应不可控 page.wait_for_url("**/callback**", timeout=30000)
首次冷启动 服务刚部署、首次请求慢 对首个导航操作单独加大 timeout

使用方式

调用处传入显式 timeout,覆盖隐式默认值:

# BasePage 封装方法支持 timeout 参数
def is_visible(self, locator, timeout=DEFAULT_TIMEOUT) -> bool:
    ...

# 用例中:仅对已知慢操作显式指定
def test_upload_large_file(self):
    self.page_obj.click_upload()
    assert self.page_obj.is_visible(
        self.page_obj.UPLOAD_SUCCESS_INDICATOR,
        timeout=60000   # 显式等待 60 秒,仅此一处
    ), "大文件上传超时"

禁止事项

  • 禁止 time.sleep() 硬等待 — Playwright 的自动等待机制已覆盖,硬等待浪费时间且不可靠
  • 禁止给每个操作都加显式 timeout — 这等于放弃了隐式等待的统一管理优势
  • 禁止在 PageObject 方法内部硬编码超长 timeout — 超时值应由调用方(用例层)按场景决定

三、wait_for_load_state 与隐式等待的关系

page.wait_for_load_state("networkidle")导航级等待,与元素级隐式等待互补,不冲突:

等待类型 作用层级 触发时机 set_default_timeout 控制
wait_for_load_state 导航 / 网络 页面跳转、AJAX 批量请求后 set_default_navigation_timeout 控制
隐式等待 元素 每次元素操作前

推荐模式:

def click_navigate_to_target(self):
    self.click(self.TARGET_LINK)
    self.page.wait_for_load_state("networkidle")  # 等网络请求完成
    # 之后的 is_visible / click 等操作由隐式等待兜底

检查清单

  • 全局 DEFAULT_TIMEOUT 已在配置文件统一声明
  • 所有 page fixture(function 级 / class 级)均调用了 page.set_default_timeout()
  • PageObject 业务方法中无 time.sleep()
  • 显式 timeout 仅出现在已知慢路径的调用处,而非 PageObject 内部
  • PageObject 的 is_visible() 等方法保留 timeout 参数供调用方覆盖

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-09 05:11
浙ICP备14020137号-1 $Гость$