# -*- coding: utf-8 -*- """demo 子表功能实测 v2(对齐原型 9 子模块,客户信息按拍板跳过)。 v1 教训:MANAGE 第一行商机无关联客户,写跟进被必填校验拦下、弹窗未关挡住后续点击。 v2:先遍历定位 A2(关联客户=园科集团(seed快照)),再按 Tab 逐个实测: 跟进真写 / 勘察真写 / 方案卡 seed 断言+新建弹窗+一客户一卡负路径 / 团队 / 附件 / 操作日志。 """ 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()) page.fill('#dlgForm input[name="customerId"]', '920000000000000100') page.click('#dlgForm button.ok') # 保存草稿 → 预期被一客户一卡(66009)拦 page.wait_for_timeout(1500) still = page.locator('#dlgForm', has_text='新建方案卡').count() > 0 err_txt = '' try: err_txt = page.locator('#toast .toast-err').last.inner_text(timeout=1500) except Exception: pass note('一客户一卡负路径', '弹窗仍开=被拦(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 ---- 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)