# -*- coding: utf-8 -*- """probe_v3proto.py — v3 原型(demo/v3-prototype.html)Playwright 冒烟 验证口径:默认值入分组列表(最低级守卫/唯一换设/默认值列)+ 键值字段删除(列表/弹窗/载荷无 value、无 isDefault)。 """ import json, pathlib, sys, io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') from playwright.sync_api import sync_playwright HERE = pathlib.Path(__file__).parent PROTO = (HERE / 'demo' / 'v3-prototype.html').resolve().as_uri() SHOTS = HERE / 'shots' SHOTS.mkdir(exist_ok=True) results, console_errors, page_errors = [], [], [] def check(name, cond, detail=''): results.append({'name': name, 'pass': bool(cond), 'detail': detail}) print(('PASS ' if cond else 'FAIL ') + name + ((' | ' + detail) if detail and not cond else '')) with sync_playwright() as p: b = p.chromium.launch(headless=True) pg = b.new_page() pg.on('console', lambda m: console_errors.append(m.text) if m.type == 'error' else None) pg.on('pageerror', lambda e: page_errors.append(str(e))) pg.goto(PROTO) pg.wait_for_selector('#groupBody tr') # 1 分组列表默认值列 headers = [t.inner_text() for t in pg.query_selector_all('#tab-group th')] check('分组列表含「默认值」列', any('默认值' in h for h in headers), str(headers)) # 2 两级组默认值回显 父名/子名 row902 = pg.query_selector('tr[data-gid="902"]') check('行业组默认值回显「信息技术 / 软件开发」', '信息技术 / 软件开发' in row902.query_selector('.cell-default').inner_text()) # 3 两级组设默认弹窗:只列二级项(7 个),一级项不可选 row902.query_selector('text=设置默认值').click() pg.wait_for_selector('#dlgDefaultList input[name=defpick]') radios = pg.query_selector_all('#dlgDefaultList input[name=defpick]') names = [r.evaluate('el => el.closest("label").querySelector("span").textContent') for r in radios] lv1 = {'信息技术', '制造业', '金融保险'} check('两级组弹窗只列二级项(7 个)', len(radios) == 7, f'{len(radios)} {names}') check('一级项不在可选项中', not (set(names) & lv1), str(names)) check('停用二级项也在列(管理口径)', any(r.is_checked() for r in radios)) check('当前默认标记唯一', len(pg.query_selector_all('#dlgDefaultList .cur')) == 1) pg.screenshot(path=str(SHOTS / 'v3-1-default-dlg-two-level.png')) pg.click('#dlgDefault .dfoot button:first-child') # 取消 # 4 一级组弹窗:只列一级项(4 个) pg.query_selector('tr[data-gid="901"] button.check-default, tr[data-gid="901"] td.ops button:first-child').click() pg.wait_for_selector('#dlgDefaultList input[name=defpick]') radios1 = pg.query_selector_all('#dlgDefaultList input[name=defpick]') names1 = [r.evaluate('el => el.closest("label").querySelector("span").textContent') for r in radios1] check('一级组弹窗只列一级项(4 个)', len(radios1) == 4 and '广告' in names1, f'{names1}') pg.click('#dlgDefault .dfoot button:first-child') # 5 未设置组(商机阶段)设默认 → 回显 + 载荷 cell903 = pg.query_selector('tr[data-gid="903"] .cell-default') check('未设置组显示「未设置」', '未设置' in cell903.inner_text()) pg.query_selector('tr[data-gid="903"] td.ops button:first-child').click() pg.wait_for_selector('#dlgDefaultList input[name=defpick]') pg.query_selector('#dlgDefaultList input[name=defpick][value="932"]').check() pg.click('#btnDefaultOk') pg.wait_for_timeout(200) check('设默认后回显「方案报价」', '方案报价' in pg.query_selector('tr[data-gid="903"] .cell-default').inner_text()) payload = pg.query_selector('#payloadBox').inner_text() check('set-default 载荷 groupId+itemId', 'groupId=903' in payload and 'itemId=932' in payload, payload[:120]) # 6 换设唯一:客户来源 广告 → 转介绍,旧默认取消 pg.query_selector('tr[data-gid="901"] td.ops button:first-child').click() pg.wait_for_selector('#dlgDefaultList input[name=defpick][value="912"]') pg.query_selector('#dlgDefaultList input[name=defpick][value="912"]').check() pg.click('#btnDefaultOk') pg.wait_for_timeout(200) check('换设后默认值列更新为「转介绍」', '转介绍' in pg.query_selector('tr[data-gid="901"] .cell-default').inner_text()) pg.query_selector('tr[data-gid="901"] td.ops button:first-child').click() pg.wait_for_selector('#dlgDefaultList input[name=defpick]') check('重开弹窗「当前默认」标记唯一且在新项', len(pg.query_selector_all('#dlgDefaultList .cur')) == 1 and '转介绍' in pg.query_selector('#dlgDefaultList .cur').evaluate('el => el.closest("label").textContent')) pg.screenshot(path=str(SHOTS / 'v3-2-default-switched.png')) pg.click('#dlgDefault .dfoot button:first-child') # 7 切字典项 Tab:表头无键值 pg.click('#tabbtnItem') pg.wait_for_selector('#itemBody tr') item_headers = [t.inner_text() for t in pg.query_selector_all('#tab-item th')] all_th = item_headers + headers check('两个列表表头均无「键值」', not any('键值' in h for h in all_th), str(all_th)) # 8 行业展开:子项行 + ★分组默认在二级项上 pg.query_selector('#fiGroup').select_option('902') pg.wait_for_timeout(100) pg.query_selector('#itemBody .expander[data-exp="921"]').click() pg.wait_for_timeout(100) body_txt = pg.query_selector('#itemBody').inner_text() check('展开二级行可见「软件开发」', '软件开发' in body_txt) check('★分组默认标记在二级项(软件开发)', '软件开发' in body_txt and '★分组默认' in body_txt) pg.screenshot(path=str(SHOTS / 'v3-3-item-list-v3.png')) # 9 新增弹窗:无键值输入、无默认勾选(结构化检查真实控件,说明文字除外) pg.click('#btnAddItem') pg.wait_for_selector('#dlgItem[open]') dlg_labels = [b.inner_text() for b in pg.query_selector_all('#dlgItem .dbody > label b')] check('新增弹窗表单标签无「字典键值」', not any('键值' in t for t in dlg_labels), str(dlg_labels)) check('新增弹窗无勾选框(默认勾选已删)', len(pg.query_selector_all('#dlgItem input[type=checkbox]')) == 0) check('新增弹窗表单标签齐全(分组/名称/编码/排序/状态/描述)', {'所属分组', '字典名称', '字典编码', '排序', '状态', '描述'} <= set(dlg_labels), str(dlg_labels)) st_sel = pg.query_selector('#fiStatus') check('新增弹窗状态下拉默认「启用」', st_sel is not None and st_sel.input_value() == '1') # 10 子项行含编码列;保存载荷无 value / 无 isDefault kid_headers = [t.inner_text() for t in pg.query_selector_all('#dlgItem .kids th')] check('子项行表头含「子项编码」', any('编码' in h for h in kid_headers), str(kid_headers)) pg.fill('#fiName', '客户规模') pg.click('#btnAddKid') kid_inputs = pg.query_selector_all('#kidBody input') check('新增子项行编码可填(不填自动生成)', kid_inputs[1].get_attribute('readonly') is None and '自动生成' in kid_inputs[1].get_attribute('placeholder')) kid_inputs[0].fill('大型企业') kid_inputs[1].fill('dict_test01') pg.click('#btnItemOk') pg.wait_for_timeout(200) payload2 = pg.query_selector('#payloadBox').inner_text() form_part = payload2.split('\n#')[0] # 只看表单载荷部分,排除 mock 说明注释 check('saveOrUpdate 载荷含 children[0].name', 'children[0].name=' in form_part, payload2[:200]) check('saveOrUpdate 载荷含 children[0].code(自填)', 'children[0].code=dict_test01' in form_part, form_part) check('saveOrUpdate 载荷含 status=1(新增默认启用)', 'status=1' in form_part, form_part) check('saveOrUpdate 载荷无 value / 无 isDefault', 'value=' not in form_part and 'isDefault' not in form_part, form_part) pg.screenshot(path=str(SHOTS / 'v3-4-payload-v3.png')) # 11 编辑态:既有子项编码只读(创建后不可改) pg.query_selector('#itemBody td.ops button').click() pg.wait_for_selector('#dlgItem[open]') kid_codes = pg.query_selector_all('#kidBody input.kid-code') check('编辑弹窗回显既有子项行(3 行)', len(kid_codes) == 3, str(len(kid_codes))) check('既有子项编码只读(创建后不可改)', all(c.get_attribute('readonly') is not None for c in kid_codes)) check('编辑态状态回显(信息技术=启用)', pg.query_selector('#fiStatus').input_value() == '1') pg.screenshot(path=str(SHOTS / 'v3-5-edit-kid-code-readonly.png')) pg.click('#dlgItem .dfoot button:first-child') # 12 默认项禁停守卫(mock):编辑当前默认项(转介绍,第 6 步已换设)改停用 → 63013 pg.query_selector('#fiGroup').select_option('901') pg.wait_for_timeout(100) pg.query_selector('tr[data-iid="912"] td.ops button').click() # 转介绍=客户来源当前默认 pg.wait_for_selector('#dlgItem[open]') check('编辑态状态回显(转介绍=启用)', pg.query_selector('#fiStatus').input_value() == '1') pg.select_option('#fiStatus', '0') pg.click('#btnItemOk') pg.wait_for_timeout(100) err_txt = pg.query_selector('#fiErr').inner_text() check('默认项改停用被 63013 拦(mock)', pg.query_selector('#fiErr').get_attribute('hidden') is None and '63013' in err_txt, err_txt) check('拦截后弹窗未关闭', pg.query_selector('#dlgItem[open]') is not None) pg.screenshot(path=str(SHOTS / 'v3-6-default-disable-guard.png')) pg.click('#dlgItem .dfoot button:first-child') b.close() summary = { 'total': len(results), 'passed': sum(1 for r in results if r['pass']), 'console_errors': console_errors, 'page_errors': page_errors, 'ALL_PASS': all(r['pass'] for r in results) and not console_errors and not page_errors, 'results': results, } (HERE / 'v3-proto-result.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8') print(f"\n{summary['passed']}/{summary['total']} PASS, console_errors={len(console_errors)}, page_errors={len(page_errors)}, ALL_PASS={summary['ALL_PASS']}") sys.exit(0 if summary['ALL_PASS'] else 1)