You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
408 lines
20 KiB
408 lines
20 KiB
|
6 days ago
|
# -*- coding: utf-8 -*-
|
||
|
|
"""opportunity-acceptance 票 04:Agent 浏览器自测(Playwright)v2。
|
||
|
|
|
||
|
|
v2 修复:v1 的 networkidle 等待在 file:// + 跨域 fetch 下提前触发导致异步渲染未完成即断言;
|
||
|
|
改为显式条件等待(面板无「加载中」/ 阶段节点就位 / whoBox 已登录)。
|
||
|
|
datetime-local fill 用分钟精度(带秒 Playwright 报 Malformed value)。
|
||
|
|
|
||
|
|
覆盖面:列表多视图 / 详情九子表 / 工作计划三形态+闭环 / 新建双路径 / 暂缓-恢复状态机 / 规则三族 / 用户切换。
|
||
|
|
截图存档 shots/accept-NN-场景.png;结果写 selftest-result.json。
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
import datetime
|
||
|
|
import re
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from playwright.sync_api import sync_playwright
|
||
|
|
|
||
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
||
|
|
|
||
|
|
DEMO = 'file:///D:/code/crm-backend-matt/.scratch/opportunity-e2e/demo/index.html'
|
||
|
|
SHOTS = Path('d:/code/crm-backend-matt/.scratch/opportunity-acceptance/shots')
|
||
|
|
SHOTS.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
FULL_OPP = '749639078307168256' # e2e-A-推进-智慧园区二期:客户3 卡3 跟进4
|
||
|
|
WP_OPP = '749647653033213952' # demo-验收-新建闭环:st=2 五节点,wp 空
|
||
|
|
|
||
|
|
results = [] # [case, verdict, detail]
|
||
|
|
page_errors = []
|
||
|
|
console_errors = []
|
||
|
|
|
||
|
|
|
||
|
|
def rec(case, ok, detail=''):
|
||
|
|
results.append([case, 'PASS' if ok else 'FAIL', detail])
|
||
|
|
print(('PASS ' if ok else 'FAIL '), case, ('| ' + detail) if detail else '', flush=True)
|
||
|
|
|
||
|
|
|
||
|
|
def shot(page, name):
|
||
|
|
page.wait_for_timeout(200)
|
||
|
|
page.screenshot(path=str(SHOTS / (name + '.png')), full_page=True)
|
||
|
|
|
||
|
|
|
||
|
|
def poll(page, fn, timeout):
|
||
|
|
"""Python 侧轮询:fn(page) 返回 truthy 即通过;超时静默(由后续断言判 FAIL)。"""
|
||
|
|
end = time.time() + timeout / 1000.0
|
||
|
|
while time.time() < end:
|
||
|
|
try:
|
||
|
|
if fn(page):
|
||
|
|
return
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
page.wait_for_timeout(300)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_ready(page, timeout=9000):
|
||
|
|
"""等 listBody/subBox/detailBox/ruleListBox 无「加载中」且 boardBox 无 loading 态。"""
|
||
|
|
def fn(pg):
|
||
|
|
for i in ('listBody', 'subBox', 'detailBox', 'ruleListBox'):
|
||
|
|
el = pg.locator('#' + i)
|
||
|
|
if el.count() and '加载中' in (el.text_content() or ''):
|
||
|
|
return False
|
||
|
|
return pg.locator('#boardBox .loading').count() == 0
|
||
|
|
poll(page, fn, timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_stage(page, timeout=9000):
|
||
|
|
poll(page, lambda pg: pg.locator('#stageBar .node').count() > 0, timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_logged(page, timeout=9000):
|
||
|
|
poll(page, lambda pg: '已登录' in (pg.locator('#whoBox').text_content() or ''), timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_expr(page, js_expr, timeout=12000):
|
||
|
|
def fn(pg):
|
||
|
|
try:
|
||
|
|
return bool(pg.evaluate(js_expr))
|
||
|
|
except Exception:
|
||
|
|
return False
|
||
|
|
poll(page, fn, timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_sub_has(page, text, timeout=12000):
|
||
|
|
poll(page, lambda pg: pg.locator('#subBox').count() > 0 and text in (pg.locator('#subBox').text_content() or ''), timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_sub_not(page, text, timeout=12000):
|
||
|
|
poll(page, lambda pg: pg.locator('#subBox').count() > 0 and text not in (pg.locator('#subBox').text_content() or ''), timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def wait_badge(page, text, timeout=12000):
|
||
|
|
def fn(pg):
|
||
|
|
b = pg.locator('#detailBox .badge')
|
||
|
|
return b.count() > 0 and (b.first.text_content() or '').strip() == text
|
||
|
|
poll(page, fn, timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def last_toast(page):
|
||
|
|
return (page.locator('#toast .toast-item').last.text_content() or '').strip()
|
||
|
|
|
||
|
|
|
||
|
|
def dialog_open(page):
|
||
|
|
return page.evaluate("document.getElementById('dlg').open")
|
||
|
|
|
||
|
|
|
||
|
|
def dfoot_btn(page, text):
|
||
|
|
return page.locator('#dlg .dfoot button', has_text=text).first
|
||
|
|
|
||
|
|
|
||
|
|
with sync_playwright() as p:
|
||
|
|
browser = p.chromium.launch(headless=True)
|
||
|
|
page = browser.new_page(viewport={'width': 1440, 'height': 900})
|
||
|
|
page.on('pageerror', lambda e: page_errors.append(str(e)))
|
||
|
|
page.on('console', lambda m: console_errors.append(m.text) if m.type == 'error' else None)
|
||
|
|
page.on('dialog', lambda d: d.accept()) # wpDelete 的原生 confirm
|
||
|
|
|
||
|
|
# ============ 启动 ============
|
||
|
|
page.goto(DEMO)
|
||
|
|
wait_logged(page)
|
||
|
|
wait_ready(page)
|
||
|
|
who = page.locator('#whoBox').text_content() or ''
|
||
|
|
rec('00-启动自动登录(管理员)', '已登录' in who, who.strip())
|
||
|
|
|
||
|
|
# ============ A 列表多视图 ============
|
||
|
|
rows = page.locator('#listBody tr.row')
|
||
|
|
rec('01-列表-我负责的', rows.count() >= 0 and '共' in (page.locator('#listHint').text_content() or ''),
|
||
|
|
(page.locator('#listHint').text_content() or '').strip())
|
||
|
|
shot(page, 'accept-01-列表-我负责的')
|
||
|
|
|
||
|
|
page.locator('#viewBar button', has_text='管理(全量)').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('02-列表-管理全量视图', rows.count() > 0 and '共' in (page.locator('#listHint').text_content() or ''),
|
||
|
|
(page.locator('#listHint').text_content() or '').strip() + ' / ' + str(rows.count()) + ' 行')
|
||
|
|
shot(page, 'accept-02-列表-管理全量')
|
||
|
|
|
||
|
|
page.locator('#viewBar button', has_text='公海').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('03-列表-公海视图', '共' in (page.locator('#listHint').text_content() or ''),
|
||
|
|
(page.locator('#listHint').text_content() or '').strip())
|
||
|
|
shot(page, 'accept-03-列表-公海')
|
||
|
|
|
||
|
|
page.locator('#viewBar button', has_text='管理(全量)').click()
|
||
|
|
wait_ready(page)
|
||
|
|
page.fill('#fKeyword', '智慧园区')
|
||
|
|
page.locator('button', has_text='查询').first.click()
|
||
|
|
wait_ready(page)
|
||
|
|
names = [(page.locator('#listBody tr.row td').nth(i * 9).text_content() or '') for i in range(rows.count())]
|
||
|
|
rec('04-列表-关键词筛选', rows.count() > 0 and all('智慧园区' in n for n in names),
|
||
|
|
str(rows.count()) + ' 行全含关键词')
|
||
|
|
shot(page, 'accept-04-列表-关键词筛选')
|
||
|
|
|
||
|
|
nodes = page.locator('#boardBox .node')
|
||
|
|
rec('05-看板汇总', nodes.count() > 0, str(nodes.count()) + ' 个阶段节点')
|
||
|
|
shot(page, 'accept-05-看板汇总')
|
||
|
|
|
||
|
|
# ============ B 详情九子表(e2e-A-二期) ============
|
||
|
|
page.evaluate("loadDetail('" + FULL_OPP + "')")
|
||
|
|
wait_ready(page)
|
||
|
|
wait_stage(page)
|
||
|
|
title = (page.locator('#detailBox h2').text_content() or '').strip()
|
||
|
|
stage_nodes = page.locator('#stageBar .node')
|
||
|
|
rec('06-详情总览-阶段进度5节点', '智慧园区' in title and stage_nodes.count() == 5,
|
||
|
|
title + ' / 节点 ' + str(stage_nodes.count()))
|
||
|
|
shot(page, 'accept-06-详情总览-阶段进度5节点')
|
||
|
|
|
||
|
|
# 跟进子表(默认):toolbar 按钮在 = 20260829 修复点① 无回归
|
||
|
|
page.locator('#subTabs button', has_text='跟进').first.click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('07-子表-跟进-写跟进按钮在', page.locator('#subBox button', has_text='+ 写跟进').count() == 1,
|
||
|
|
'toolbar 渲染正常(修复点①)')
|
||
|
|
|
||
|
|
# 写跟进闭环(修复点③:成功后 loadDetail 刷新最近有效跟进)
|
||
|
|
before = page.evaluate("fetch('http://localhost:8080/api/opportunity/detail?id=" + FULL_OPP + "',{headers:{Authorization:'Bearer '+S.token}}).then(r=>r.json()).then(b=>b.data.lastValidFollowTime)")
|
||
|
|
page.locator('#subBox button', has_text='+ 写跟进').click()
|
||
|
|
page.wait_for_function("() => document.getElementById('dlg').open", timeout=5000)
|
||
|
|
page.fill('#dlg [name="followContent"]', 'accept-自测跟进:票04浏览器自动化写入')
|
||
|
|
cust_opts = page.locator('#dlg [name="customerId"] option')
|
||
|
|
if cust_opts.count() > 0:
|
||
|
|
cid = cust_opts.first.get_attribute('value')
|
||
|
|
page.select_option('#dlg [name="customerId"]', cid)
|
||
|
|
dfoot_btn(page, '保存').click()
|
||
|
|
wait_ready(page)
|
||
|
|
toast = last_toast(page)
|
||
|
|
after = page.evaluate("fetch('http://localhost:8080/api/opportunity/detail?id=" + FULL_OPP + "',{headers:{Authorization:'Bearer '+S.token}}).then(r=>r.json()).then(b=>b.data.lastValidFollowTime)")
|
||
|
|
today = datetime.date.today().isoformat()
|
||
|
|
rec('08-写跟进-成功并刷新锚点(修复点③)', '跟进已写入' in toast and bool(after) and today in str(after),
|
||
|
|
'toast=' + toast + ' / lastValidFollowTime ' + str(before) + ' -> ' + str(after))
|
||
|
|
shot(page, 'accept-08-写跟进成功')
|
||
|
|
else:
|
||
|
|
rec('08-写跟进-成功并刷新锚点(修复点③)', False, '客户下拉为空,闭环未执行')
|
||
|
|
if dialog_open(page):
|
||
|
|
page.evaluate('closeDlg()')
|
||
|
|
|
||
|
|
# 勘察子表(toolbar 修复点① 另一处)
|
||
|
|
page.locator('#subTabs button', has_text='现场勘察').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('09-子表-现场勘察', page.locator('#subBox button', has_text='+ 新增勘察').count() == 1,
|
||
|
|
'toolbar 渲染正常(修复点①)')
|
||
|
|
shot(page, 'accept-09-子表-现场勘察')
|
||
|
|
|
||
|
|
page.locator('#subTabs button', has_text='关联客户').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('10-子表-关联客户', page.locator('#subBox .kv').count() >= 2, str(page.locator('#subBox .kv').count()) + ' 条')
|
||
|
|
shot(page, 'accept-10-子表-关联客户')
|
||
|
|
|
||
|
|
page.locator('#subTabs button', has_text='团队成员').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('11-子表-团队成员', page.locator('#subBox .kv').count() >= 1, str(page.locator('#subBox .kv').count()) + ' 条')
|
||
|
|
shot(page, 'accept-11-子表-团队成员')
|
||
|
|
|
||
|
|
# 方案卡详情(修复点②:d.fields 键名渲染)
|
||
|
|
page.locator('#subTabs button', has_text='方案卡').click()
|
||
|
|
wait_ready(page)
|
||
|
|
card_rows = page.locator('#subBox > div')
|
||
|
|
if page.locator('#subBox button', has_text='详情').count() > 0:
|
||
|
|
page.locator('#subBox button', has_text='详情').first.click()
|
||
|
|
page.wait_for_function("() => document.getElementById('dlg').open", timeout=5000)
|
||
|
|
kvs = page.locator('#dlg .grid .kv').count()
|
||
|
|
rec('12-子表-方案卡详情(修复点②)', True, 'dialog 打开,字段键值 ' + str(kvs) + ' 项')
|
||
|
|
shot(page, 'accept-12-方案卡详情')
|
||
|
|
page.evaluate('closeDlg()')
|
||
|
|
else:
|
||
|
|
rec('12-子表-方案卡详情(修复点②)', False, '无方案卡')
|
||
|
|
shot(page, 'accept-13-子表-方案卡列表')
|
||
|
|
|
||
|
|
page.locator('#subTabs button', has_text='附件').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('14-子表-附件', '加载中' not in (page.locator('#subBox').text_content() or ''),
|
||
|
|
(page.locator('#subBox').text_content() or '')[:40])
|
||
|
|
shot(page, 'accept-14-子表-附件')
|
||
|
|
|
||
|
|
# 操作日志(修复点④:分形态渲染,ROW_ADD=粗体「新增」)
|
||
|
|
page.locator('#subTabs button', has_text='操作日志').click()
|
||
|
|
wait_ready(page)
|
||
|
|
oplog_html = page.locator('#subBox').inner_html()
|
||
|
|
rec('15-子表-操作日志-分形态渲染(修复点④)', '新增' in oplog_html or 'FIELD_CHANGE' in oplog_html,
|
||
|
|
'长度 ' + str(len(oplog_html)))
|
||
|
|
shot(page, 'accept-15-子表-操作日志')
|
||
|
|
|
||
|
|
page.locator('#subTabs button', has_text='阶段历史').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('16-子表-阶段历史', '加载中' not in (page.locator('#subBox').text_content() or ''),
|
||
|
|
(page.locator('#subBox').text_content() or '')[:40])
|
||
|
|
shot(page, 'accept-16-子表-阶段历史')
|
||
|
|
|
||
|
|
# ============ C 工作计划三形态 + 闭环(WP_OPP) ============
|
||
|
|
page.evaluate("loadDetail('" + WP_OPP + "')")
|
||
|
|
wait_ready(page)
|
||
|
|
wait_stage(page)
|
||
|
|
page.locator('#subTabs button', has_text='工作计划').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('17-子表-工作计划入口', page.locator('#subBox button', has_text='+ 新增工作计划').count() == 1,
|
||
|
|
'票03接入的四端点入口在')
|
||
|
|
|
||
|
|
today = datetime.date.today()
|
||
|
|
plans = [
|
||
|
|
('accept-自测计划-逾期形态', (today - datetime.timedelta(days=1)).isoformat() + 'T10:00'),
|
||
|
|
('accept-自测计划-临期形态', (today + datetime.timedelta(days=2)).isoformat() + 'T10:00'),
|
||
|
|
('accept-自测计划-进行中形态', (today + datetime.timedelta(days=10)).isoformat() + 'T10:00'),
|
||
|
|
]
|
||
|
|
for content, deadline in plans:
|
||
|
|
page.locator('#subBox button', has_text='+ 新增工作计划').click()
|
||
|
|
page.wait_for_function("() => document.getElementById('dlg').open", timeout=5000)
|
||
|
|
page.fill('#dlg [name="planContent"]', content)
|
||
|
|
page.fill('#dlg [name="deadline"]', deadline)
|
||
|
|
dfoot_btn(page, '保存').click()
|
||
|
|
wait_sub_has(page, content) # 新增行渲染出来才继续
|
||
|
|
wait_sub_has(page, 'accept-自测计划-进行中形态') # 等三行数据渲染(hint 常驻文本不可作判定)
|
||
|
|
badges = page.locator('#subBox .badge')
|
||
|
|
badge_texts = [(badges.nth(i).text_content() or '').strip() for i in range(badges.count())]
|
||
|
|
ok3 = all(t in badge_texts for t in ('逾期', '临期', '进行中'))
|
||
|
|
rec('18-工作计划-三形态实时判定', ok3, 'badge 文本集 ' + str(badge_texts))
|
||
|
|
shot(page, 'accept-17-工作计划三形态')
|
||
|
|
|
||
|
|
# 登记完成(进行中那条)
|
||
|
|
row = page.locator('#subBox > div', has_text='进行中形态').first
|
||
|
|
row.locator('button', has_text='登记完成').click()
|
||
|
|
wait_sub_has(page, '完成于')
|
||
|
|
body = page.locator('#subBox').text_content() or ''
|
||
|
|
rec('19-工作计划-登记完成回填finishTime', '已完成' in body and '完成于' in body, 'badge=已完成 + 完成时间显示')
|
||
|
|
shot(page, 'accept-18-工作计划登记完成')
|
||
|
|
|
||
|
|
# 编辑(临期那条改内容,部分更新)
|
||
|
|
row = page.locator('#subBox > div', has_text='临期形态').first
|
||
|
|
row.locator('button', has_text='编辑').click()
|
||
|
|
page.wait_for_function("() => document.getElementById('dlg').open", timeout=5000)
|
||
|
|
page.fill('#dlg [name="planContent"]', 'accept-自测计划-临期形态-已编辑')
|
||
|
|
dfoot_btn(page, '保存').click()
|
||
|
|
wait_sub_has(page, '已编辑')
|
||
|
|
body = page.locator('#subBox').text_content() or ''
|
||
|
|
rec('20-工作计划-编辑部分更新', '已编辑' in body, '内容已更新')
|
||
|
|
shot(page, 'accept-19-工作计划编辑')
|
||
|
|
|
||
|
|
# 删除(逾期那条,软删)
|
||
|
|
row = page.locator('#subBox > div', has_text='逾期形态').first
|
||
|
|
row.locator('button', has_text='删除').click()
|
||
|
|
wait_sub_not(page, '逾期形态', timeout=25000) # 软删后列表不再可见(loadSub 刷新含远程库抖动余量)
|
||
|
|
body = page.locator('#subBox').text_content() or ''
|
||
|
|
rec('21-工作计划-删除软删', '逾期形态' not in body and '已编辑' in body, '逾期条从列表消失,其余保留')
|
||
|
|
shot(page, 'accept-20-工作计划删除')
|
||
|
|
|
||
|
|
# ============ D 新建双路径 ============
|
||
|
|
page.locator('header button', has_text='+ 新建商机').click()
|
||
|
|
page.wait_for_function("() => [...document.querySelectorAll('#dlg [name=provinceCode] option')].some(o => o.value)", timeout=8000)
|
||
|
|
page.locator('#dlg .dfoot button', has_text='创建').click()
|
||
|
|
err = (page.locator('#crtErr').text_content() or '')
|
||
|
|
need = ['商机名称', '商机来源', '省', '市', '项目属地', '招标形式']
|
||
|
|
rec('22-新建-六必填前端拦截', '必填缺失' in err and all(n in err for n in need), err.replace('\n', ' ')[:80])
|
||
|
|
shot(page, 'accept-21-新建-六必填拦截')
|
||
|
|
|
||
|
|
ts = datetime.datetime.now().strftime('%H%M%S')
|
||
|
|
new_name = 'accept-自测-新建' + ts
|
||
|
|
page.fill('#dlg [name="opportunityName"]', new_name)
|
||
|
|
src_val = page.evaluate("[...dlgEl.querySelectorAll('[name=oppSource] option')].find(o=>o.value && o.value!=='opp_source_01').value")
|
||
|
|
page.select_option('#dlg [name="oppSource"]', src_val)
|
||
|
|
prov_val = page.evaluate("[...dlgEl.querySelectorAll('[name=provinceCode] option')].find(o=>o.value).value")
|
||
|
|
page.select_option('#dlg [name="provinceCode"]', prov_val)
|
||
|
|
page.wait_for_function("() => [...document.querySelectorAll('#dlg [name=cityCode] option')].some(o => o.value)", timeout=8000)
|
||
|
|
city_val = page.evaluate("[...dlgEl.querySelectorAll('[name=cityCode] option')].find(o=>o.value).value")
|
||
|
|
page.select_option('#dlg [name="cityCode"]', city_val)
|
||
|
|
page.evaluate("dlgEl.querySelector('[name=localityType]').value = dlgEl.querySelector('[name=localityType] option:nth-child(2)').value")
|
||
|
|
page.evaluate("dlgEl.querySelector('[name=bidForm]').value = dlgEl.querySelector('[name=bidForm] option:nth-child(2)').value")
|
||
|
|
page.locator('#dlg .dfoot button', has_text='创建').click()
|
||
|
|
try: # toast 2.5s 自消失,先抓(成功=「商机已创建,id=...」;失败=err toast)
|
||
|
|
page.wait_for_function("() => document.querySelectorAll('#toast .toast-item').length > 0", timeout=6000)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
toast = last_toast(page)
|
||
|
|
wait_expr(page, "() => (document.querySelector('#detailBox h2')||{textContent:''}).textContent.includes('" + new_name + "')", timeout=15000)
|
||
|
|
wait_expr(page, "() => document.querySelectorAll('#stageBar .node').length === 5", timeout=15000)
|
||
|
|
title = (page.locator('#detailBox h2').text_content() or '').strip()
|
||
|
|
stage_nodes = page.locator('#stageBar .node')
|
||
|
|
created = '商机已创建' in toast and new_name in title and stage_nodes.count() == 5
|
||
|
|
rec('23-新建-成功直达详情落首阶段(D-01)', created, 'toast=' + toast + ' / 节点 ' + str(stage_nodes.count()))
|
||
|
|
shot(page, 'accept-22-新建-成功直达详情')
|
||
|
|
|
||
|
|
# ============ E 状态机:暂缓 -> 恢复(在新建商机上闭环) ============
|
||
|
|
page.locator('#flowBar button', has_text=re.compile('^暂缓$')).click()
|
||
|
|
page.wait_for_function("() => document.getElementById('dlg').open", timeout=5000)
|
||
|
|
dhead = (page.locator('#dlg .dhead').text_content() or '')
|
||
|
|
rec('24-暂缓对话框打开', '暂缓' in dhead, dhead.strip())
|
||
|
|
shot(page, 'accept-23-暂缓对话框')
|
||
|
|
page.evaluate("dlgEl.querySelector('[name=pauseReason]').value = dlgEl.querySelector('[name=pauseReason] option:nth-child(2)').value")
|
||
|
|
page.locator('#dlg .dfoot button', has_text=re.compile('^暂缓$')).click()
|
||
|
|
wait_badge(page, '暂缓中')
|
||
|
|
badge = (page.locator('#detailBox .badge').first.text_content() or '')
|
||
|
|
rec('25-暂缓成功', badge == '暂缓中', 'badge=' + badge)
|
||
|
|
shot(page, 'accept-24-暂缓成功')
|
||
|
|
|
||
|
|
page.locator('#flowBar button', has_text=re.compile('^取消暂缓$')).click()
|
||
|
|
wait_badge(page, '推进中')
|
||
|
|
badge = (page.locator('#detailBox .badge').first.text_content() or '')
|
||
|
|
rec('26-取消暂缓成功', badge == '推进中', 'badge=' + badge)
|
||
|
|
shot(page, 'accept-25-恢复成功')
|
||
|
|
|
||
|
|
# ============ F 规则三族 ============
|
||
|
|
page.locator('nav.tabs button', has_text='规则配置').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('27-规则-公海与提醒', page.locator('#ruleListBox table').count() == 1, '默认族列表渲染')
|
||
|
|
shot(page, 'accept-26-规则-公海与提醒')
|
||
|
|
|
||
|
|
page.locator('#ruleBar button', has_text='商机阶段设置').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rules_txt = page.locator('#ruleListBox').text_content() or ''
|
||
|
|
rec('28-规则-商机阶段设置(预设模板在)', 'OPP_STAGE_TPL_01' in rules_txt, '预设模板保护验证点可见')
|
||
|
|
shot(page, 'accept-27-规则-商机阶段设置')
|
||
|
|
|
||
|
|
page.locator('#ruleBar button', has_text='方案卡模板').click()
|
||
|
|
wait_ready(page)
|
||
|
|
rec('29-规则-方案卡模板', page.locator('#ruleListBox table tr').count() >= 1, str(page.locator('#ruleListBox table tr').count()) + ' 行')
|
||
|
|
shot(page, 'accept-28-规则-方案卡模板')
|
||
|
|
|
||
|
|
page.locator('#ruleListBox tr.row').first.click()
|
||
|
|
page.wait_for_function("() => document.getElementById('dlg').open", timeout=5000)
|
||
|
|
rec('30-规则-详情对话框', True, (page.locator('#dlg .dhead').text_content() or '').strip())
|
||
|
|
shot(page, 'accept-29-规则详情')
|
||
|
|
page.evaluate('closeDlg()')
|
||
|
|
|
||
|
|
# ============ G 用户切换 ============
|
||
|
|
page.select_option('#userSel', '1')
|
||
|
|
wait_logged(page)
|
||
|
|
wait_ready(page)
|
||
|
|
who = (page.locator('#whoBox').text_content() or '').strip()
|
||
|
|
rec('31-切换用户A-数据权限视图', '赖永利' in who and '已登录' in who, who)
|
||
|
|
shot(page, 'accept-30-切换用户A')
|
||
|
|
|
||
|
|
browser.close()
|
||
|
|
|
||
|
|
# ============ 汇总 ============
|
||
|
|
fails = [r for r in results if r[1] == 'FAIL']
|
||
|
|
print('\n==== 自测汇总: ' + str(len(results) - len(fails)) + '/' + str(len(results)) + ' PASS, ' + str(len(fails)) + ' FAIL ====')
|
||
|
|
for f in fails:
|
||
|
|
print('FAIL', f[0], f[2])
|
||
|
|
print('page_errors:', len(page_errors), page_errors[:5])
|
||
|
|
print('console_errors:', len(console_errors), console_errors[:5])
|
||
|
|
|
||
|
|
out = {
|
||
|
|
'summary': {'total': len(results), 'pass': len(results) - len(fails), 'fail': len(fails),
|
||
|
|
'pageErrors': len(page_errors), 'consoleErrors': len(console_errors)},
|
||
|
|
'results': results,
|
||
|
|
'pageErrors': page_errors,
|
||
|
|
'consoleErrors': console_errors,
|
||
|
|
}
|
||
|
|
Path('d:/code/crm-backend-matt/.scratch/opportunity-acceptance/selftest-result.json').write_text(
|
||
|
|
json.dumps(out, ensure_ascii=False, indent=2), encoding='utf-8')
|
||
|
|
print('result -> selftest-result.json')
|