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.
 
 
 
 
 

142 lines
6.8 KiB

# -*- coding: utf-8 -*-
"""票 12 自测 v2:demo 新建对话框 opp-create-fields 7 类字段 + 看板泳道(展示名取卡 + offset/limit)。
v2 修正:oppSource 按 value 精确选(v1 index=1 落在 opp_source_01 线索转入触发关联线索必填);
客户区块改手输 id+名快照(v1 实测 customer/search oppId=0 被后端 66002 拒绝:商机不存在: 0);
创建后调 detail API 断言新字段落库回显。
"""
import io
import re
import sys
import requests
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'
SHOT = r'D:\code\crm-backend-matt\.scratch\opportunity-e2e\demo\shot-12-'
BASE = 'https://ai.itc.vip/crm-api'
UID_MAIN = '739564171091247104' # 罗伟健:测试环境可用(赖永利 id 在测试环境 40401,待补导)
console_msgs, page_errors, failed = [], [], []
def sniff(page):
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) if isinstance(r.failure, str) else (r.failure or {}).get('errorText', '?'))))
page.on('response', lambda r: failed.append((r.url, 'HTTP %d' % r.status)) if r.status >= 400 else None)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={'width': 1500, 'height': 950})
sniff(page)
page.goto(URL)
page.wait_for_load_state('networkidle')
page.wait_for_timeout(2000)
# 测试环境模式:boot 不自动登录,uid 直登罗伟健
page.fill('#uidInput', UID_MAIN)
page.click('text=uid 登录')
page.wait_for_load_state('networkidle')
page.wait_for_timeout(2500)
# 1) 泳道渲染断言 + 截图
lanes = page.locator('.lane')
lane_n = lanes.count()
lane_heads = [lanes.nth(i).locator('.laneHead').inner_text().replace('\n', ' ') for i in range(lane_n)]
cards = page.locator('.laneCard').count()
more_btns = page.locator('.laneFoot button').count()
page.screenshot(path=SHOT + '09-lanes.png', full_page=False)
print('1) 泳道列数=%d 卡片=%d 「更多」按钮=%d' % (lane_n, cards, more_btns))
for h in lane_heads:
print(' 列:', h)
if more_btns:
page.locator('.laneFoot button').first.click()
page.wait_for_timeout(1500)
foot = page.locator('.laneFoot').first.inner_text()
print(' 点击「更多」后:卡片总数=%d 列脚注=%r' % (page.locator('.laneCard').count(), foot))
page.screenshot(path=SHOT + '10-lanes-more.png', full_page=False)
# 2) 打开新建对话框 + 截图
page.click('text=+ 新建商机')
page.wait_for_timeout(800)
page.screenshot(path=SHOT + '11-create-dialog.png', full_page=False)
dlg = page.locator('dialog:visible')
print('2) 新建对话框可见=%s 字段数(name=*)=%d' % (
dlg.count() > 0, page.locator('dialog [name]').count()))
# 三级级联:省 → 市 → 区县(区县码折叠进 cityCode)
page.select_option('dialog [name="provinceCode"]', index=1)
page.wait_for_timeout(1200)
city_opts = page.locator('dialog [name="cityCode"] option').count()
page.select_option('dialog [name="cityCode"]', index=1)
page.wait_for_timeout(1200)
dist_opts = page.locator('dialog [name="districtCode"] option').count()
if dist_opts > 1:
page.select_option('dialog [name="districtCode"]', index=1)
print('3) 三级级联:市选项=%d 区县选项=%d(选中区县码=%s' % (
city_opts, dist_opts, page.locator('dialog [name="districtCode"]').input_value() or ''))
# 填表:客户手输 id+名(=1 主要意向路径,验证主表冗余)
page.fill('dialog [name="opportunityName"]', 'demo-12-新字段验收')
page.select_option('dialog [name="oppSource"]', 'opp_source_02')
page.select_option('dialog [name="localityType"]', index=1)
page.select_option('dialog [name="bidForm"]', index=1)
page.fill('dialog [name="projectAmount"]', '880000.50')
page.fill('dialog [name="address"]', '天府大道北段1700号环球中心E1')
page.fill('dialog [name="customerId"]', '99001')
page.fill('dialog [name="customerName"]', '智慧园区客户')
page.select_option('dialog [name="customerRole"]', 'customer_role_01')
page.select_option('dialog [name="isPrimaryIntended"]', '1')
page.fill('dialog [name="keyContactName"]', '王工')
page.fill('dialog [name="keyContactCompany"]', '甲方Y公司')
page.fill('dialog [name="remark"]', 'demo自测:项目概况字段')
page.click('dialog button:has-text("创建")')
page.wait_for_timeout(2500)
# 3) 创建结果:toast 或 errbox
toast_txt = page.locator('.toast-item').last.inner_text() if page.locator('.toast-item').count() else ''
err_txt = page.locator('#crtErr .errbox').inner_text() if page.locator('#crtErr .errbox').count() else ''
print('4) 创建结果 toast=%r err=%r' % (toast_txt[:80], err_txt[:200]))
page.screenshot(path=SHOT + '13-create-result.png', full_page=False)
page.wait_for_timeout(1500)
page.screenshot(path=SHOT + '14-detail-after-create.png', full_page=False)
browser.close()
# 4) detail API 断言新字段落库
m = re.search(r'id=(\d+)', toast_txt)
if m:
new_id = m.group(1)
tok = requests.get(BASE + '/api/auth/debug/token', params={'userId': UID_MAIN}).json()['data']
d = requests.get(BASE + '/api/opportunity/detail', headers={'Authorization': 'Bearer ' + tok},
params={'id': new_id}).json()
dv = d.get('data') or {}
checks = {
'projectAmount': dv.get('projectAmount'),
'address': dv.get('address'),
'remark(项目概况)': dv.get('remark'),
'cityCode(区县码)': dv.get('cityCode'),
'customerId': dv.get('customerId'),
'primaryCustomerId(=1冗余)': dv.get('primaryCustomerId'),
'primaryCustomerNameSnapshot': dv.get('primaryCustomerNameSnapshot'),
'keyContactId(留空)': dv.get('keyContactId'),
'keyContactNameSnapshot': dv.get('keyContactNameSnapshot'),
'keyContactCompanySnapshot': dv.get('keyContactCompanySnapshot'),
}
print('5) detail 断言(id=%s):' % new_id)
for k, v in checks.items():
print(' %-28s = %r' % (k, v))
else:
print('5) 创建失败,跳过 detail 断言')
print('== console error/warning:', len(console_msgs))
for t, x in console_msgs[:20]:
print(' [%s] %s' % (t, x[:300]))
print('== page errors:', len(page_errors))
for e in page_errors[:10]:
print(' ', e[:300])
print('== failed requests:', len(failed))
for u, f in failed[:20]:
print(' ', f, '', u[:200])