# -*- 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])