# -*- coding: utf-8 -*- """v2 原型页交互验证 + 截图(纯前端 mock,无需后端)""" import io, sys, json from pathlib import Path sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') from playwright.sync_api import sync_playwright BASE = Path(__file__).parent URL = (BASE / 'demo' / 'v2-prototype.html').as_uri() SHOTS = BASE / 'shots' SHOTS.mkdir(exist_ok=True) results, console_errors = [], [] def check(name, cond, detail=''): results.append((name, bool(cond), detail)) print(('PASS ' if cond else 'FAIL ') + name + (' | ' + detail if detail else '')) with sync_playwright() as p: b = p.chromium.launch(headless=True) pg = b.new_page(viewport={'width': 1500, 'height': 950}) pg.on('console', lambda m: console_errors.append(m.text) if m.type == 'error' else None) pg.on('pageerror', lambda e: console_errors.append(str(e))) pg.goto(URL) pg.wait_for_selector('#iBody tr') # 1) 列表只出一级:顶层行不含「二级」badge 的行应为一级;总行数=一级数+展开子项数 rows = pg.locator('#iBody > tr').count() check('列表渲染', rows > 0, f'rows={rows}') check('默认展开 i1(互联网)', pg.locator('tr.lv2row').count() >= 3, f"lv2row={pg.locator('tr.lv2row').count()}") check('停用子项随行内嵌返回(社交停用可见)', '停用示例' in pg.locator('#iBody').inner_text()) pg.screenshot(path=str(SHOTS / 'v2-00-list.png'), full_page=True) # 2) 展开/收起 pg.locator('.expander', has_not=pg.locator('.none')).nth(1).click() pg.wait_for_timeout(120) lv2 = pg.locator('tr.lv2row').count() check('点展开块追加子项行', lv2 >= 5, f'lv2row={lv2}') pg.screenshot(path=str(SHOTS / 'v2-01-expand.png'), full_page=True) # 3) 复合新增:加子项后键值字段应禁用(=名称提示) pg.get_by_role('button', name='+ 新建字典项(复合)').click() pg.fill('#fiName', '新能源汽车') pg.get_by_role('button', name='+ 添加二级字典项').click() krows = pg.locator('#kidsBody tr') krows.nth(0).locator('input').nth(0).fill('整车') pg.get_by_role('button', name='+ 添加二级字典项').click() krows.nth(1).locator('input').nth(0).fill('电池') check('复合新增:键值字段禁用', pg.locator('#fiValue').is_disabled()) check('键值提示=名称口径', '键值=名称' in pg.locator('#fiValueHint').inner_text() or '取字典名称' in pg.locator('#fiValueHint').inner_text()) pg.screenshot(path=str(SHOTS / 'v2-02-add-composite.png')) # 4) 保存 → 载荷预览 + 列表出现新一级项(键值=名称) pg.locator('#dlgFoot button.ok').click() pg.wait_for_timeout(150) pv = pg.locator('#payloadBox').inner_text() check('载荷预览含 children 绑定语法', 'children[0].name=整车' in pv) check('复合新增不传顶层 value', '(复合新增:不传顶层 value' in pv) body = pg.locator('#iBody').inner_text() check('新一级项键值=名称落库', '新能源汽车' in body) pg.screenshot(path=str(SHOTS / 'v2-03-payload.png'), full_page=True) # 5) 编辑 i1:删行被引用子项(游戏)→ 63009 拦 pg.get_by_role('button', name='编辑').nth(0).click() pg.get_by_role('button', name='删行').nth(1).click() # i102 游戏(referenced) pg.locator('#dlgFoot button.ok').click() pg.wait_for_timeout(150) err = pg.locator('#fiOut').inner_text() check('全量替换守卫 63009', '63009' in err, err.strip()[:80]) pg.screenshot(path=str(SHOTS / 'v2-04-63009.png')) pg.get_by_role('button', name='恢复').click() # 6) 删行未引用子项(社交,第3行)→ 保存成功,列表少一行 pg.get_by_role('button', name='删行').nth(2).click() # 电商(0)、游戏(1)、社交(2) pg.locator('#dlgFoot button.ok').click() pg.wait_for_timeout(150) pv2 = pg.locator('#payloadBox').inner_text() check('缺失子项=删除注释', '删除' in pv2 and 'i103' in pv2) check('载荷仍带保留子项 id', 'children[0].id=i101' in pv2) tbody = pg.locator('#iBody').inner_text() check('社交已从列表消失', '社交' not in tbody) # 7) 单项新增(无子项)键值必填 63001 pg.get_by_role('button', name='+ 新建字典项(复合)').click() pg.fill('#fiName', '无子项测试项') check('无子项:键值字段启用', not pg.locator('#fiValue').is_disabled()) pg.locator('#dlgFoot button.ok').click() pg.wait_for_timeout(120) check('单项新增 63001 拦截', '63001' in pg.locator('#fiOut').inner_text()) # 8) 默认值互斥:勾选默认 + 子项默认 → 拦 pg.fill('#fiValue', 'v1') pg.check('#fiDefault') pg.get_by_role('button', name='+ 添加二级字典项').click() pg.locator('#kidsBody tr').nth(0).locator('.kdef').check() pg.locator('#kidsBody tr').nth(0).locator('input').nth(0).fill('子项甲') pg.locator('#dlgFoot button.ok').click() pg.wait_for_timeout(120) check('同载荷多默认拦截', '只允许一行' in pg.locator('#fiOut').inner_text()) pg.screenshot(path=str(SHOTS / 'v2-05-validations.png')) # 9) 停用默认项 63013(行内操作):潜在客户(i2)为 g2 默认且 referenced pg.locator('#dlg').evaluate('d => d.close()') idx = pg.locator('#iBody tr').evaluate_all("els => els.findIndex(e => e.innerText.includes('潜在客户'))") pg.locator('#iBody tr').nth(idx).get_by_role('button', name='停用').click() pg.wait_for_timeout(120) check('默认项禁停 63013', '63013' in pg.locator('#toast').inner_text()) # 10) 删除被引用项 63009 pg.locator('#iBody tr').nth(idx).get_by_role('button', name='删除').click() pg.wait_for_timeout(120) check('被引用删除 63009', '63009' in pg.locator('#toast').inner_text()) pg.screenshot(path=str(SHOTS / 'v2-06-guards.png'), full_page=True) b.close() fails = [r for r in results if not r[1]] out = {'total': len(results), 'fail': len(fails), 'console_errors': console_errors, 'results': [{'name': n, 'pass': ok, 'detail': d} for n, ok, d in results]} (BASE / 'v2-proto-result.json').write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding='utf-8') print(f"\n== {len(results) - len(fails)}/{len(results)} PASS, console_errors={len(console_errors)} ==") sys.exit(1 if fails or console_errors else 0)