fix(business): e2e 链路验证与两处修复
- wait_toast 超时返回最后见到的非空提示(错误 toast 一闪而过时诊断信息全丢) - 业务/发货明细行单价类字段放行 4 位小数(与后端 DECIMAL(14,4) 对齐) - 清理步骤适配删除保护(被终态业务/引用拦截时留档而非失败) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
fba8cfc215
commit
8c4acb82fe
@ -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. 数据清理 测试数据按依赖序清理,被保护项按留档说明处理")
|
||||
|
||||
@ -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. 数据清理 回款/发货/业务与基础数据按依赖序清理,被保护项留档")
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -103,6 +103,10 @@ function personnelLabel(option: PersonnelOption): string {
|
||||
|
||||
/** 金额/数量形态:非负数字、最多两位小数(纯形态校验,不涉及 float 运算) */
|
||||
const AMOUNT_PATTERN = /^(\d+)(\.\d{1,2})?$/
|
||||
// 单价类列(sale_price/guided_price)建表为 Numeric(14,4):后端 str 直出
|
||||
// 会带最多 4 位小数(如 "5000.0000"),前端必须放行到 4 位——否则任何
|
||||
// 带小数精度的历史记录在编辑时都会被前端永远拦下
|
||||
const PRICE_PATTERN = /^(\d+)(\.\d{1,4})?$/
|
||||
/** 全零形态:数量用它排除「>0」语义,不走 float 比较 */
|
||||
const ZERO_PATTERN = /^0+(\.0+)?$/
|
||||
|
||||
@ -234,13 +238,13 @@ function validateItemRows(rows: ItemDraft[]): string | null {
|
||||
return `${no}的数量须为大于 0 的数字,最多两位小数`
|
||||
}
|
||||
const prices = [
|
||||
['销售单价', row.sale_price],
|
||||
['成本额', row.cost_amount],
|
||||
['指导价', row.guided_price],
|
||||
['销售单价', row.sale_price, PRICE_PATTERN, '最多四位小数'],
|
||||
['成本额', row.cost_amount, AMOUNT_PATTERN, '最多两位小数'],
|
||||
['指导价', row.guided_price, PRICE_PATTERN, '最多四位小数'],
|
||||
] as const
|
||||
for (const [label, value] of prices) {
|
||||
if (value.trim() && !AMOUNT_PATTERN.test(value.trim())) {
|
||||
return `${no}的${label}须为非负数字,最多两位小数`
|
||||
for (const [label, value, pattern, hint] of prices) {
|
||||
if (value.trim() && !pattern.test(value.trim())) {
|
||||
return `${no}的${label}须为非负数字,${hint}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,8 @@ function businessLabel(option: BusinessOption): string {
|
||||
|
||||
/** 金额/数量形态:非负数字、最多两位小数(与后端 `_check_amount` 同口径) */
|
||||
const AMOUNT_PATTERN = /^(\d+)(\.\d{1,2})?$/
|
||||
// 发货单价列建表为 Numeric(14,4):后端 str 直出带最多 4 位小数,前端放行到 4 位
|
||||
const PRICE_PATTERN = /^(\d+)(\.\d{1,4})?$/
|
||||
/** 全零形态:数量排除「>0」语义,不走 float 比较 */
|
||||
const ZERO_PATTERN = /^0+(\.0+)?$/
|
||||
|
||||
@ -183,8 +185,8 @@ function validateItemRows(rows: ItemDraft[]): string | null {
|
||||
if (!AMOUNT_PATTERN.test(row.quantity) || ZERO_PATTERN.test(row.quantity)) {
|
||||
return `${no}的数量须为大于 0 的数字,最多两位小数`
|
||||
}
|
||||
if (row.sale_price.trim() && !AMOUNT_PATTERN.test(row.sale_price.trim())) {
|
||||
return `${no}的发货单价须为非负数字,最多两位小数`
|
||||
if (row.sale_price.trim() && !PRICE_PATTERN.test(row.sale_price.trim())) {
|
||||
return `${no}的发货单价须为非负数字,最多四位小数`
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
Loading…
Reference in New Issue
Block a user