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.
 
 
 
 
 

119 lines
6.3 KiB

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