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.
 
 
 
 
 

265 lines
13 KiB

# -*- coding: utf-8 -*-
"""demo 子表功能实测 v3(对齐原型 9 子模块 + 工作计划 Tab,客户信息按拍板跳过)。
v1 教训:MANAGE 第一行商机无关联客户,写跟进被必填校验拦下、弹窗未关挡住后续点击。
v2:先遍历定位 A2(关联客户=园科集团(seed快照)),再按 Tab 逐个实测:
跟进真写 / 勘察真写 / 方案卡 seed 断言+新建弹窗+一客户一卡负路径 / 团队 / 附件 / 操作日志。
v3(验收票 03):+ 工作计划 Tab 闭环——seed 三形态断言(逾期/临期/已完成)、
新增、登记完成(finishTime 服务端回填回显)、删除(软删消失)、空内容前端校验负路径。
"""
import datetime as dt
import io
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from playwright.sync_api import sync_playwright
URL = 'file:///D:/code/crm-backend-matt/.scratch/opportunity-e2e/demo/index.html'
BASE = r'D:\code\crm-backend-matt\.scratch\opportunity-e2e'
A_UID = '744842565802524672' # 赖永利(A)
console_msgs, page_errors, failed = [], [], []
def note(k, v):
print('%-30s %s' % (k, v))
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={'width': 1440, 'height': 960})
page.on('console', lambda m: console_msgs.append((m.type, m.text))
if m.type in ('error', 'warning') else None)
page.on('pageerror', lambda e: page_errors.append(str(e)))
page.on('requestfailed', lambda r: failed.append((r.url, str(r.failure))))
page.on('response', lambda r: failed.append((r.url, 'HTTP %d' % r.status))
if r.status >= 400 else None)
page.goto(URL)
page.wait_for_load_state('networkidle')
page.click('button:has-text("管理(全量)")')
page.wait_for_timeout(1500)
# ---- 遍历定位 A2(支持翻页) ----
target = -1
for pg in (1, 2):
n = page.locator('#listBody tr.row').count()
for i in range(n):
page.locator('#listBody tr.row').nth(i).click()
page.wait_for_selector('#subTabs', timeout=8000)
page.wait_for_timeout(400)
page.click('#subTabs button:has-text("关联客户")')
page.wait_for_timeout(900)
if page.locator('#subBox', has_text='园科集团(seed快照)').count() > 0:
target = i
note('定位 A2', '%d 页第 %d 行(客户=园科集团(seed快照))' % (pg, i + 1))
break
page.click('button[data-tab="list"]')
page.wait_for_timeout(700)
if target >= 0:
break
try:
page.click('.pager button:has-text("下一页")', timeout=3000)
page.wait_for_timeout(1200)
except Exception:
break
if target < 0:
note('定位 A2', '未找到(所有页都没有 seed 客户),终止')
browser.close()
sys.exit(1)
# ---- 1) 跟进 Tab:真实写一条 ----
page.click('#subTabs button:has-text("跟进")')
page.wait_for_timeout(1000)
page.click('button:has-text("+ 写跟进")')
page.wait_for_selector('#dlgForm textarea[name="followContent"]', timeout=8000)
note('跟进弹窗客户下拉', str(page.locator('#dlgForm select[name="customerId"] option').all_text_contents()))
page.fill('#dlgForm textarea[name="followContent"]', 'demo-手工验收-测试跟进')
page.click('#dlgForm button.ok')
try:
page.wait_for_selector('#toast .toast-ok', timeout=6000)
note('写跟进', '成功(toast-ok)')
except Exception:
note('写跟进', '未见成功 toast')
page.wait_for_timeout(1200)
note('跟进回显', '' if page.locator('#subBox', has_text='demo-手工验收-测试跟进').count() else '')
page.screenshot(path=BASE + r'\demo-sub-3-follow-added.png')
# ---- 2) 现场勘察 Tab:seed 断言 + 真实新增 ----
page.click('#subTabs button:has-text("现场勘察")')
page.wait_for_timeout(1000)
note('seed 勘察记录渲染', '' if page.locator('#subBox', has_text='机房位置').count() else '')
page.click('button:has-text("+ 新增勘察")')
page.wait_for_selector('#dlgForm input[name="engineerUserId"]', timeout=8000)
page.fill('#dlgForm input[name="engineerUserId"]', A_UID)
page.fill('#dlgForm input[name="surveySeq"]', 'survey_seq_02')
page.fill('#dlgForm input[name="applyNo"]', 'KC-DEMO-001')
page.fill('#dlgForm textarea[name="surveyDesc"]', 'demo-手工验收-勘察测试')
page.click('#dlgForm button.ok')
try:
page.wait_for_selector('text=勘察已记录', timeout=6000)
note('新增勘察', '成功(toast)')
except Exception:
note('新增勘察', '未见成功 toast')
page.wait_for_timeout(1200)
note('勘察回显', '' if page.locator('#subBox', has_text='demo-手工验收-勘察测试').count() else '')
page.screenshot(path=BASE + r'\demo-sub-4-survey.png')
# ---- 3) 方案卡 Tab:seed 卡断言 + 详情弹窗 + 新建弹窗 + 一客户一卡负路径 ----
page.click('#subTabs button:has-text("方案卡")')
page.wait_for_timeout(1200)
note('seed 卡(已提交/88 万)', '' if page.locator('#subBox', has_text='已提交').count() else '')
page.click('#subBox button:has-text("详情")')
page.wait_for_timeout(1000)
note('卡详情弹窗字段值', page.locator('#dlgForm .grid .kv').count())
page.screenshot(path=BASE + r'\demo-sub-5-card-detail.png')
page.click('#dlgForm button:has-text("取消")')
page.wait_for_timeout(500)
page.click('button:has-text("新建方案卡")')
page.wait_for_timeout(1800)
if page.locator('#dlgForm', has_text='新建方案卡').count():
note('新建卡弹窗·模板显示字段输入框', page.locator('#cardFields input').count())
# 负路径客户取「已被本商机占用的客户」触发 66009;无占用客户则退化用伪 id 触发 66007(同拦截族)
taken = page.evaluate("""async () => {
const r = await fetch(BASE + '/api/opportunity/scheme-card/list?oppId=' + S.oppId,
{headers: {Authorization: 'Bearer ' + S.token}});
const b = await r.json();
return (b.data || []).map(c => String(c.customerId));
}""")
neg_id = taken[0] if taken else '999999999999999999'
neg_code = '66009 一客户一卡' if taken else '66007 客户不属本商机'
page.fill('#dlgForm input[name="customerId"]', neg_id)
page.click('#dlgForm button.ok') # 保存草稿 → 预期被后端拦截
page.wait_for_timeout(1500)
still = page.evaluate("() => document.getElementById('dlg').open")
err_txt = ''
try:
err_txt = page.locator('#toast .toast-err').last.inner_text(timeout=1500)
except Exception:
pass
note('卡保存负路径(%s,negId=%s' % (neg_code, neg_id),
'弹窗仍开=被拦(toast: %s' % err_txt if still else '意外保存成功(异常,需查)')
page.click('#dlgForm button:has-text("取消")')
else:
note('新建卡弹窗', '未出现(无可选模板版本)')
page.screenshot(path=BASE + r'\demo-sub-5-card.png')
# ---- 4) 团队成员 Tab(写接口缺失 D-04/05/06,验证只读渲染) ----
page.click('#subTabs button:has-text("团队成员")')
page.wait_for_timeout(1200)
note('团队成员 .kv 行数', page.locator('#subBox .kv').count())
page.screenshot(path=BASE + r'\demo-sub-6-team.png')
# ---- 5) 附件 Tab(上传需 MinIO,demo 只读渲染) ----
page.click('#subTabs button:has-text("附件")')
page.wait_for_timeout(1200)
note('附件 .kv 行数', page.locator('#subBox .kv').count())
page.screenshot(path=BASE + r'\demo-sub-7-attachment.png')
# ---- 6) 工作计划 Tab(票 03:seed 三形态 + add/登记完成/delete 闭环 + 负路径) ----
page.click('#subTabs button:has-text("工作计划")')
page.wait_for_timeout(1200)
note('wp seed 逾期形态', '' if page.locator('#subBox .badge.b4', has_text='逾期').count() else '')
note('wp seed 临期形态', '' if page.locator('#subBox .badge.b3', has_text='临期').count() else '')
note('wp seed 已完成形态', '' if page.locator('#subBox .badge.b2', has_text='已完成').count() else '')
page.screenshot(path=BASE + r'\demo-sub-8-workplan-seed.png')
wp_content = 'demo-验收-wp-闭环测试'
page.click('button:has-text("+ 新增工作计划")')
page.wait_for_selector('#dlgForm textarea[name="planContent"]', timeout=8000)
# 负路径:空内容直接保存 → 前端预校验 errbox
page.click('#dlgForm button.ok')
page.wait_for_timeout(600)
note('wp 空内容负路径', '' if page.locator('#dlgForm .errbox').count() else '')
# 正路径:填内容 + deadline 未来 7 天(进行中形态)
page.fill('#dlgForm textarea[name="planContent"]', wp_content)
dl = dt.datetime.now() + dt.timedelta(days=7)
page.fill('#dlgForm input[name="deadline"]', dl.strftime('%Y-%m-%dT%H:%M:%S'))
page.click('#dlgForm button.ok')
try:
page.wait_for_selector('#toast .toast-ok', timeout=6000)
note('wp 新增', '成功(toast-ok)')
except Exception:
note('wp 新增', '未见成功 toast')
page.wait_for_timeout(1500)
row = page.locator('#subBox div[style*="border-bottom"]', has_text=wp_content)
note('wp 新增回显(进行中形态)', '' if row.count() and row.locator('.badge.b1', has_text='进行中').count() else '')
page.screenshot(path=BASE + r'\demo-sub-9-workplan-added.png')
# 登记完成 → 服务端回填 finishTime 回显
row.locator('button:has-text("登记完成")').click()
try:
page.wait_for_selector('#toast .toast-ok', timeout=6000)
note('wp 登记完成', '成功(toast-ok)')
except Exception:
note('wp 登记完成', '未见成功 toast')
page.wait_for_timeout(1500)
row2 = page.locator('#subBox div[style*="border-bottom"]', has_text=wp_content)
note('wp 完成回显(已完成+完成时间)', '' if row2.count() and row2.locator('.badge.b2', has_text='已完成').count()
and row2.locator('text=完成于').count() else '')
page.screenshot(path=BASE + r'\demo-sub-10-workplan-finished.png')
# 删除(confirm 对话框自动接受)→ 软删消失
page.once('dialog', lambda d: d.accept())
row2.locator('button:has-text("删除")').click()
try:
page.wait_for_selector('#toast .toast-ok', timeout=6000)
note('wp 删除', '成功(toast-ok)')
except Exception:
note('wp 删除', '未见成功 toast')
page.wait_for_timeout(1500)
note('wp 删除后不可见', '' if page.locator('#subBox', has_text=wp_content).count() == 0 else '')
page.screenshot(path=BASE + r'\demo-sub-11-workplan-deleted.png')
# ---- 7) 新建商机(票 03:六必填省市联动 + 成功反馈直达详情) ----
page.click('button[data-tab="list"]')
page.wait_for_timeout(700)
page.click('header button.primary') # + 新建商机
page.wait_for_selector('#dlgForm input[name="opportunityName"]', timeout=8000)
# 负路径:只填名称直接创建 → 前端六必填逐字段 errbox
page.fill('#dlgForm input[name="opportunityName"]', 'demo-验收-新建闭环')
page.click('#dlgForm button.ok')
page.wait_for_timeout(600)
eb = page.locator('#crtErr .errbox')
note('新建负路径(六必填逐字段提示)', eb.inner_text()[:80].replace('\n', ' ') if eb.count() else '')
page.screenshot(path=BASE + r'\demo-sub-12-create-invalid.png')
# 补齐六必填(来源/省/市/属地/招标形式)→ 成功直达详情
page.select_option('#dlgForm select[name="oppSource"]', value='opp_source_02')
page.select_option('#dlgForm select[name="provinceCode"]', index=1)
page.wait_for_timeout(1200) # 等市级联动
page.select_option('#dlgForm select[name="cityCode"]', index=1)
lt_opts = page.locator('#dlgForm select[name="localityType"] option')
bf_opts = page.locator('#dlgForm select[name="bidForm"] option')
if lt_opts.count() > 1:
page.select_option('#dlgForm select[name="localityType"]', index=1)
if bf_opts.count() > 1:
page.select_option('#dlgForm select[name="bidForm"]', index=1)
page.click('#dlgForm button.ok')
try:
page.wait_for_selector('#toast .toast-ok', timeout=8000)
note('新建成功 toast(含 id)', page.locator('#toast .toast-ok').last.inner_text()[:70])
except Exception:
note('新建成功 toast', '未见')
page.wait_for_timeout(2500)
note('新建后直达详情', '' if page.locator('#detailBox h2', has_text='demo-验收-新建闭环').count() else '')
page.screenshot(path=BASE + r'\demo-sub-13-create-ok-detail.png')
# ---- 8) 操作日志 Tab ----
page.click('#subTabs button:has-text("操作日志")')
page.wait_for_timeout(1500)
note('操作日志条数', page.locator('#subBox div[style*="border-bottom"]').count())
page.screenshot(path=BASE + r'\demo-sub-8-oplog.png')
browser.close()
print()
print('== 健康检查 ==')
print('console error/warning:', len(console_msgs))
for t, m in console_msgs:
print(' [%s] %s' % (t, m))
print('page errors:', len(page_errors))
for e in page_errors:
print(' ', e)
print('失败请求(>=400/网络):', len(failed))
for u, w in failed:
print(' ', u, w)