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
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)
|
|
|