fix(business): e2e 链路验证与两处修复

- wait_toast 超时返回最后见到的非空提示(错误 toast 一闪而过时诊断信息全丢)
- 业务/发货明细行单价类字段放行 4 位小数(与后端 DECIMAL(14,4) 对齐)
- 清理步骤适配删除保护(被终态业务/引用拦截时留档而非失败)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-13 17:06:46 +08:00
1 parent fba8cfc215
commit 8c4acb82fe
5 files changed
+67 -41

No files matched your search

+20 -16
View File
@@ -370,24 +370,18 @@ def step_person_edit_regions(page: Page) -> None:
def step_person_delete_protection(page: Page) -> None:
"""删除被引用的经理被 400 拦截并引导「改为离职」,改为离职成功。"""
"""删除被引用的经理被 400 拦截并引导「改为离职」。
此处**不真改离职**——后续归属调整步骤要以在职经理为目标;
「改为离职」的动作移到清理步骤执行(删除保护路径已在本步断言)。
"""
row = find_row(page, "姓名", MANAGER_NAME)
row.locator("button:has-text('删除')").click()
page.wait_for_timeout(400)
# 确认框文案本身已含「此时应改为离职」引导;确认后走后端校验
confirm_message_box(page, "删除")
expect_toast(page, "请改为离职", "人员删除保护")
row = find_row(page, "姓名", MANAGER_NAME)
row.locator("button:has-text('设为离职')").click()
page.wait_for_timeout(400)
confirm_message_box(page, "设为离职")
expect_toast(page, "已设为离职", "人员离职")
row = find_row(page, "姓名", MANAGER_NAME)
if "离职" not in row.inner_text():
fail("人员离职", "列表状态未更新为离职")
print("✓ 5. 删除保护 删除被引人员被 400 拦截,改为离职成功")
print("✓ 5. 删除保护 删除被引人员被 400 拦截,引导改为离职")
def step_products(page: Page) -> None:
@@ -460,8 +454,8 @@ def step_ownership_change(page: Page) -> None:
def step_cleanup(page: Page) -> None:
"""清理测试数据:按 客户 → 产品 → 业务员 → 经理 依赖序删除。
经理在删除保护步骤已改为离职;仍被引用拦截时打印留档说明(属预期
保护行为,不做假绿)。
经理此刻仍在职(删除保护步骤刻意未改);删除被拦时先「设为离职」
再删——届时客户/业务员均已删除,经理应无引用、可正常删除。
"""
open_menu(page, "客户管理", "基础数据")
row = find_row(page, "关键词", CUSTOMER_COMPANY)
@@ -486,8 +480,18 @@ def step_cleanup(page: Page) -> None:
text = wait_toast(page, "已删除")
if "人员已删除" in text:
pass
elif "请改为离职" in text or "存在团队成员" in text:
print(" 数据清理 经理仍被引用(属预期保护),留档说明:保留离职态经理")
elif "请改为离职" in text or "存在团队成员" in text or "业务归属" in text:
# 引导路径照做:设为离职后再删(引用已随客户/业务员删除解除)
row = find_row(page, "姓名", MANAGER_NAME)
row.locator("button:has-text('设为离职')").click()
page.wait_for_timeout(400)
confirm_message_box(page, "设为离职")
expect_toast(page, "已设为离职", "清理·经理离职")
row = find_row(page, "姓名", MANAGER_NAME)
row.locator("button:has-text('删除')").click()
page.wait_for_timeout(400)
confirm_message_box(page, "删除")
expect_toast(page, "人员已删除", "清理·经理删除")
else:
fail("数据清理", f"清理经理出现未预期提示:{text or '(无)'}")
print("✓ 12. 数据清理 测试数据按依赖序清理,被保护项按留档说明处理")
+25 -14
View File
@@ -466,8 +466,10 @@ def step_cleanup(page: Page) -> None:
"""清理:按 回款 → 发货 → 业务 → 客户 → 产品 → 人员 依赖序删除。
删除保护拦截属预期行为:被拦截时打印留档说明,不做假绿也不误伤主流程。
特别地,步骤 12 已把业务流转为「已完成」——终态业务禁删回款/发货是
设计保护(单据金额参与核算口径),此处删除被拦即留档,不视为失败。
"""
# 回款(未核算可删;删除后业务回款合计自动回退)
# 回款(业务此刻已是终态:删除必然被拦,留档说明)
base.open_menu(page, "回款管理", "业务数据")
base.select_filter_option(page, "业务", business_no)
base.click_query(page)
@@ -475,7 +477,10 @@ def step_cleanup(page: Page) -> None:
base.table_rows(page).first.locator("button:has-text('删除')").click()
page.wait_for_timeout(400)
base.confirm_message_box(page, "删除")
expect_toast(page, "回款已删除", "数据清理")
text = wait_toast(page, "已删除")
if "回款已删除" not in text:
print(" 数据清理 回款删除被拦(业务已终态属预期保护),留档说明:回款保留在开发库")
break
page.wait_for_timeout(600)
base.click_query(page)
@@ -510,10 +515,18 @@ def step_cleanup(page: Page) -> None:
)
# 基础数据:客户 → 产品 → 业务员 → 经理
# 客户被留档的业务引用时删除被拦(属预期保护),宽清处理不误伤主流程
base.open_menu(page, "客户管理", "基础数据")
row = base.find_row(page, "关键词", base.CUSTOMER_COMPANY)
if row.count() > 0:
base.delete_row(page, row, "客户已删除")
row.locator("button:has-text('删除')").click()
page.wait_for_timeout(400)
base.confirm_message_box(page, "删除")
text = wait_toast(page, "已删除")
if "客户已删除" in text:
pass
else:
print(" 数据清理 客户删除被拦(被留档业务引用属预期保护),留档说明:客户保留在开发库")
base.open_menu(page, "产品管理", "基础数据")
row = base.find_row(page, "关键词", base.PRODUCT_NAME)
@@ -521,22 +534,20 @@ def step_cleanup(page: Page) -> None:
base.delete_row(page, row, "产品已删除")
base.open_menu(page, "人员管理", "基础数据")
row = base.find_row(page, "姓名", base.SALESMAN_NAME)
if row.count() > 0:
base.delete_row(page, row, "人员已删除")
row = base.find_row(page, "姓名", base.MANAGER_NAME)
if row.count() > 0:
for person_name in (base.SALESMAN_NAME, base.MANAGER_NAME):
row = base.find_row(page, "姓名", person_name)
if row.count() == 0:
continue
row.locator("button:has-text('删除')").click()
page.wait_for_timeout(400)
base.confirm_message_box(page, "删除")
text = wait_toast(page, "已删除")
if "人员已删除" in text:
print(" 数据清理 经理已删除")
elif "请改为离职" in text or "存在团队成员" in text:
print(" 数据清理 经理仍被引用(属预期保护),留档说明:保留经理")
else:
fail("数据清理", f"清理经理出现未预期提示:{text or '(无)'}")
continue
if "请改为离职" in text or "存在团队成员" in text or "业务归属" in text:
print(f" 数据清理 {person_name} 仍被引用(属预期保护),留档说明:保留人员")
continue
fail("数据清理", f"清理人员出现未预期提示:{text or '(无)'}")
print("✓ 15. 数据清理 回款/发货/业务与基础数据按依赖序清理,被保护项留档")
+8 -3
View File
@@ -85,19 +85,24 @@ def toast_text(page: Page) -> str:
def wait_toast(page: Page, expected: str, timeout_ms: int = 5000) -> str:
"""等待包含 expected 的全局提示出现,返回其文本(超时返回当前文本)。
"""等待包含 expected 的全局提示出现,返回其文本(超时返回最后见到的非空提示)。
直接读 `toast_text` 会在「上一条提示尚未消失、新提示尚未出现」的窗口里
读到旧文本,产生误判;轮询等待把这类时序问题挡在断言之外。
返回值语义:匹配成功返回该条;超时返回**见过的最后一条非空提示** ——
toast 只有 3 秒生命周期,等到超时才回来断言的场景(如「请求已 400、
错误 toast 一闪而过」)若返回最后一次的空串,诊断信息会全部丢失。
"""
deadline = time.monotonic() + timeout_ms / 1000
text = ""
last_seen = ""
while time.monotonic() < deadline:
text = toast_text(page)
if text:
last_seen = text
if expected in text:
return text
page.wait_for_timeout(200)
return text
return last_seen
def expect_toast(page: Page, expected: str, step: str, timeout_ms: int = 5000) -> None: