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.
349 lines
20 KiB
349 lines
20 KiB
# -*- coding: utf-8 -*-
|
|
"""customer-e2e-r2 票 05 · demo Playwright 扩展冒烟(chromium headless)
|
|
被测: .scratch/customer-e2e/demo/index.html?api=http://localhost:8080
|
|
基线: demo_smoke10.py 11 步(登录/列表/汇总卡/新建/编辑/详情页签/成员/导入/交割/网络层)
|
|
新增: 战略协议 CRUD(五端点+快照同步) / 查重设置保存回读+恢复 / F6 批量工商(NO_HIT) / F8 duplicateStrategy=OVERWRITE 回显
|
|
附带断言: 网络层零 PUT/DELETE、零 /api/customer/<digits>/、console/page error 零
|
|
清理: dedup 恢复原值(无条件兜底) + 协议清扫 + e2c-r2demo-浏览器* archive 正门
|
|
"""
|
|
import json, re, time, pathlib, sys, io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
ROOT = pathlib.Path(r"e:\code\crm-backend-matt\.scratch\customer-e2e")
|
|
SHOTS = ROOT / "shots-r2"; SHOTS.mkdir(exist_ok=True)
|
|
DEMO = ROOT / "demo" / "index.html"
|
|
XLSX = ROOT / "_import-heavy.xlsx"
|
|
ADMIN = "739564171091247104"
|
|
BUDDY = "744842318024015872"
|
|
FULL_ID = "750844477165273088" # e2c-全字段-科技(有联系人)
|
|
MEMBER_ID = "750844483406397440" # e2c-成员-样例
|
|
TS = time.strftime("%H%M%S")
|
|
NEW_NAME = f"e2c-r2demo-浏览器-{TS}"
|
|
AG_AMOUNT = "300万-r2"
|
|
|
|
import urllib.request, urllib.parse
|
|
def post(url, data, token=None):
|
|
req = urllib.request.Request(url, data=urllib.parse.urlencode(data).encode(), method="POST")
|
|
req.add_header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
|
|
if token: req.add_header("Authorization", "Bearer " + token)
|
|
with urllib.request.urlopen(req, timeout=15) as r:
|
|
return json.loads(r.read().decode())
|
|
def get_json(url, token=None):
|
|
req = urllib.request.Request(url)
|
|
if token: req.add_header("Authorization", "Bearer " + token)
|
|
with urllib.request.urlopen(req, timeout=15) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
tok = get_json(f"http://localhost:8080/api/auth/debug/token?userId={ADMIN}")["data"]
|
|
# 查重设置为全局单例:起测前抓原值,收尾无条件恢复(UI 内另做保存回读演练)
|
|
dedup_orig = get_json("http://localhost:8080/api/rule/customer/dedup", tok).get("data") or {}
|
|
print("dedup 原值:", json.dumps(dedup_orig, ensure_ascii=False))
|
|
|
|
results = []
|
|
def step(name, ok, note=""):
|
|
results.append({"step": name, "ok": bool(ok), "note": note})
|
|
print(("PASS " if ok else "FAIL ") + name + (" | " + note if note else ""))
|
|
|
|
console_errors, page_errors, api_urls = [], [], []
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True)
|
|
page = browser.new_page()
|
|
page.set_default_timeout(20000)
|
|
page.on("console", lambda m: console_errors.append(m.text) if m.type == "error" else None)
|
|
page.on("pageerror", lambda e: page_errors.append(str(e)))
|
|
def on_resp(r):
|
|
u = r.url
|
|
if "/api/customer" in u or "/api/rule" in u:
|
|
api_urls.append(f"{r.request.method} {u}")
|
|
page.on("response", on_resp)
|
|
|
|
# 0. 打开 demo(?api= 注入本地后端)
|
|
page.goto(DEMO.as_uri() + "?api=http://localhost:8080")
|
|
page.wait_for_load_state("networkidle")
|
|
page.screenshot(path=str(SHOTS / "00-open.png"))
|
|
|
|
# 1. 登录(uid 选择器默认 admin)
|
|
page.click("button:has-text('uid 登录')")
|
|
page.wait_for_selector(".toast-item:has-text('登录成功')")
|
|
step("登录 debug/token + /auth/me", True)
|
|
|
|
# 2. 列表(新契约 workspace 表单字段)
|
|
page.wait_for_selector("#listBody tr td:not(.empty)", timeout=15000)
|
|
rows = page.locator("#listBody tr").count()
|
|
info = page.text_content("#pageInfo").strip()
|
|
step("列表 workspace/page", rows > 0 and "第" in info, f"rows={rows}, {info}")
|
|
|
|
# 3. 汇总卡
|
|
page.wait_for_selector("#summaryBox .card", timeout=15000)
|
|
cards = page.locator("#summaryBox .card").count()
|
|
step("汇总卡 workspace/board/summary", cards > 0, f"cards={cards}")
|
|
|
|
# 4. 新建(/create,含 L2 相似命中 confirmSimilar 双分支)
|
|
page.click("button:has-text('+ 新建客户')")
|
|
page.wait_for_selector("#cName")
|
|
page.fill("#cName", NEW_NAME)
|
|
page.fill("#cType", "customer_type_01")
|
|
page.fill("#cProv", "440000"); page.fill("#cCity", "440100"); page.fill("#cDist", "440103")
|
|
page.fill("#cInd", "gov")
|
|
page.fill("#cOwner", ADMIN)
|
|
page.click(".dfoot button.ok")
|
|
confirm_needed = False
|
|
try:
|
|
page.wait_for_selector(".toast-item:has-text('创建成功')", timeout=6000)
|
|
except Exception:
|
|
confirm_needed = True
|
|
page.wait_for_selector("#cOut .simbox", timeout=8000)
|
|
page.click("#cOut button:has-text('确认创建')")
|
|
page.wait_for_selector(".toast-item:has-text('创建成功')", timeout=15000)
|
|
toast = page.text_content(".toast-item:has-text('创建成功')").strip()
|
|
m = re.search(r"创建成功 id=(\d+)", toast)
|
|
new_id = m.group(1) if m else ""
|
|
page.wait_for_selector("dialog#dlg:not([open])", state="attached")
|
|
step("新建客户 create", bool(new_id), f"id={new_id}, confirmSimilar={'是' if confirm_needed else '否'}")
|
|
page.screenshot(path=str(SHOTS / "01-created.png"))
|
|
|
|
# 5. 编辑(detail?id= → edit?id= 带 version/isBizNegotiated/isChild)
|
|
page.click(f"#listBody td.row:has-text('{NEW_NAME}')")
|
|
page.wait_for_selector("#headBox b", timeout=15000)
|
|
page.click("#headBox button:has-text('编辑')")
|
|
page.wait_for_selector("#eName")
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('保存成功')", timeout=15000)
|
|
step("编辑客户 detail?id= + edit?id=(CAS)", True, f"customer={NEW_NAME}")
|
|
page.screenshot(path=str(SHOTS / "02-edited.png"))
|
|
|
|
# 6. 详情页签(seed 全字段客户:六页签含新增 agreements 全点一遍)
|
|
page.reload(); page.wait_for_load_state("networkidle")
|
|
page.click("nav.tabs button[data-tab='detail']")
|
|
page.fill("#didInput", FULL_ID)
|
|
page.click("button:has-text('打开')")
|
|
page.wait_for_selector("#headBox b", timeout=15000)
|
|
sub_ok, sub_note = True, []
|
|
for s in ["contacts", "follow", "opps", "agreements", "members", "oplog"]:
|
|
page.click(f"#detailPanel .subtabs button[data-s='{s}']")
|
|
page.wait_for_timeout(700)
|
|
if page.locator("#subBox .errbox").count():
|
|
sub_ok = False
|
|
sub_note.append(page.text_content("#subBox .errbox").strip()[:80])
|
|
step("详情页签六页签无错误(含 agreements)", sub_ok, "; ".join(sub_note))
|
|
page.click("#detailPanel .subtabs button[data-s='contacts']")
|
|
page.wait_for_selector("#subBox table", timeout=15000)
|
|
contact_rows = page.locator("#subBox table tbody tr").count()
|
|
step("联系人列表 contacts?customerId=", contact_rows > 0, f"rows={contact_rows}")
|
|
page.screenshot(path=str(SHOTS / "03-detail-tabs.png"))
|
|
|
|
# 7. 成员加/移除(member/add 数组表单 → member/remove)
|
|
page.reload(); page.wait_for_load_state("networkidle")
|
|
page.click("nav.tabs button[data-tab='detail']")
|
|
page.fill("#didInput", MEMBER_ID)
|
|
page.click("button:has-text('打开')")
|
|
page.wait_for_selector("#headBox b", timeout=15000)
|
|
page.click("#detailPanel .subtabs button[data-s='members']")
|
|
page.wait_for_timeout(700)
|
|
if page.locator("#subBox tr:has-text('曾偲青') button:has-text('移除')").count():
|
|
page.click("#subBox tr:has-text('曾偲青') button:has-text('移除')")
|
|
page.wait_for_selector(".toast-item:has-text('已移除')", timeout=10000)
|
|
page.wait_for_timeout(700)
|
|
before = page.locator("#subBox table tbody tr").count()
|
|
page.click("#subBox button:has-text('+ 添加协同人')")
|
|
page.wait_for_selector("#mIds")
|
|
page.fill("#mIds", BUDDY)
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('成员已添加')", timeout=15000)
|
|
page.wait_for_selector("#subBox:has-text('曾偲青')", timeout=15000)
|
|
page.click("#subBox tr:has-text('曾偲青') button:has-text('移除')")
|
|
page.wait_for_selector(".toast-item:has-text('已移除')", timeout=15000)
|
|
page.wait_for_timeout(700)
|
|
step("成员加/移除 member/add+remove", True, f"rows {before}->{page.locator('#subBox table tbody tr').count()}")
|
|
page.screenshot(path=str(SHOTS / "04-members.png"))
|
|
|
|
# 8.【新入口·协议 CRUD】自建客户(owner=admin,与 BUDDY 归属解耦)五端点 + 快照同步
|
|
page.reload(); page.wait_for_load_state("networkidle")
|
|
page.click("nav.tabs button[data-tab='detail']")
|
|
page.fill("#didInput", new_id)
|
|
page.click("button:has-text('打开')")
|
|
page.wait_for_selector("#headBox b", timeout=15000)
|
|
page.click("#detailPanel .subtabs button[data-s='agreements']")
|
|
page.wait_for_selector("#subBox button:has-text('+ 新增协议')", timeout=15000)
|
|
page.click("#subBox button:has-text('+ 新增协议')")
|
|
page.wait_for_selector("dialog#dlg[open] #agLevel", timeout=10000)
|
|
page.select_option("#agLevel", "2")
|
|
page.fill("#agAmount", AG_AMOUNT)
|
|
page.fill("#agRemark", "r2 UI 冒烟协议")
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('协议已保存')", timeout=15000)
|
|
page.wait_for_selector(f"#subBox tr:has-text('{AG_AMOUNT}')", timeout=15000)
|
|
ags = page.evaluate("window._agreements || []")
|
|
aid = next((str(a["id"]) for a in ags if str(a.get("amount") or "") == AG_AMOUNT), "")
|
|
step("协议新增 agreement/create", bool(aid), f"agreementId={aid}, level=2, amount={AG_AMOUNT}")
|
|
|
|
page.click("#detailPanel .subtabs button[data-s='info']")
|
|
page.wait_for_selector("#subBox .grid", timeout=15000)
|
|
snap = page.evaluate("(()=>{const kv=[...document.querySelectorAll('#subBox .kv')].find(x=>x.querySelector('b')&&x.querySelector('b').textContent==='战略协议');return kv?kv.querySelector('span').textContent:''})()")
|
|
step("客户快照同步 strategic_agreement_level", snap in ("二级", "2"), f"info.grid 战略协议={snap!r}")
|
|
page.screenshot(path=str(SHOTS / "05-agreement-created.png"))
|
|
|
|
page.click("#detailPanel .subtabs button[data-s='agreements']")
|
|
page.wait_for_selector(f"#subBox tr:has-text('{AG_AMOUNT}') button:has-text('编辑')", timeout=15000)
|
|
page.click(f"#subBox tr:has-text('{AG_AMOUNT}') button:has-text('编辑')")
|
|
page.wait_for_selector("dialog#dlg[open] #agLevel", timeout=10000)
|
|
prefill_ok = page.input_value("#agLevel") == "2"
|
|
page.select_option("#agLevel", "3")
|
|
page.click(".dfoot button.ok")
|
|
page.wait_for_selector(".toast-item:has-text('协议已保存')", timeout=15000)
|
|
page.wait_for_selector(f"#subBox tr:has-text('{AG_AMOUNT}'):has-text('三级')", timeout=15000)
|
|
step("协议编辑 agreement/edit(回显+升级三级)", prefill_ok, f"prefill level=2 -> 3, id={aid}")
|
|
|
|
page.click(f"#subBox tr:has-text('{AG_AMOUNT}') button:has-text('详情')")
|
|
page.wait_for_selector("dialog#dlg[open]:has-text('协议详情')", timeout=10000)
|
|
detail_ok = "三级" in (page.text_content("dialog#dlg") or "")
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(400)
|
|
step("协议详情 agreement/detail?id=", detail_ok, f"id={aid}, 等级=三级")
|
|
|
|
page.once("dialog", lambda d: d.accept()) # 原生 confirm('确认删除该战略协议?')
|
|
page.click(f"#subBox tr:has-text('{AG_AMOUNT}') button:has-text('删除')")
|
|
page.wait_for_selector(".toast-item:has-text('已删除')", timeout=15000)
|
|
page.wait_for_selector("#subBox td.empty:has-text('无数据')", timeout=15000)
|
|
step("协议删除 agreement/delete(confirm 流+列表空)", True, f"id={aid}, 删空后快照回 NULL")
|
|
page.screenshot(path=str(SHOTS / "06-agreement-deleted.png"))
|
|
|
|
# 9.【新入口·查重设置】保存 → 重载回读 → 恢复原值(全局单例)
|
|
page.click("nav.tabs button[data-tab='settings']")
|
|
page.wait_for_selector(".toast-item:has-text('查重设置已加载')", timeout=15000)
|
|
orig_ui = {k: page.input_value(k) for k in ["#dpMaster", "#dpName", "#dpMode", "#dpThreshold", "#dpPhone"]}
|
|
page.select_option("#dpMaster", "1"); page.select_option("#dpName", "1")
|
|
page.select_option("#dpMode", "1")
|
|
page.fill("#dpThreshold", "66")
|
|
page.select_option("#dpPhone", "0")
|
|
page.click("button[onclick='saveDedup()']")
|
|
page.wait_for_selector(".toast-item:has-text('查重设置已保存')", timeout=15000)
|
|
page.click("nav.tabs button[data-tab='settings']") # 重进触发 loadDedup 回读
|
|
page.wait_for_selector(".toast-item:has-text('查重设置已加载')", timeout=15000)
|
|
page.wait_for_timeout(900) # 防 toast 残留竞态:等 GET 回来覆盖控件值
|
|
rb = {k: page.input_value(k) for k in ["#dpMaster", "#dpName", "#dpMode", "#dpThreshold", "#dpPhone"]}
|
|
rb_ok = rb["#dpMaster"] == "1" and rb["#dpMode"] == "1" and rb["#dpThreshold"] == "66" and rb["#dpPhone"] == "0"
|
|
step("查重设置 save→回读(dedup/save 全量覆盖)", rb_ok, f"改后={rb}")
|
|
# 恢复原值(UI 面)
|
|
page.select_option("#dpMaster", orig_ui["#dpMaster"]); page.select_option("#dpName", orig_ui["#dpName"])
|
|
page.select_option("#dpMode", orig_ui["#dpMode"]); page.fill("#dpThreshold", orig_ui["#dpThreshold"])
|
|
page.select_option("#dpPhone", orig_ui["#dpPhone"])
|
|
page.click("button[onclick='saveDedup()']")
|
|
page.wait_for_selector(".toast-item:has-text('查重设置已保存')", timeout=15000)
|
|
page.click("nav.tabs button[data-tab='settings']")
|
|
page.wait_for_selector(".toast-item:has-text('查重设置已加载')", timeout=15000)
|
|
page.wait_for_timeout(900)
|
|
back = {k: page.input_value(k) for k in ["#dpMaster", "#dpName", "#dpMode", "#dpThreshold", "#dpPhone"]}
|
|
restore_ok = back == orig_ui
|
|
step("查重设置恢复原值", restore_ok, f"原值={orig_ui} 恢复后={back}")
|
|
page.screenshot(path=str(SHOTS / "07-dedup-settings.png"))
|
|
|
|
# 10.【新入口·F6 批量工商】勾选自建客户 → 批量更新 → NO_HIT 徽标(stub 如实呈现)
|
|
page.click("nav.tabs button[data-tab='list']")
|
|
page.wait_for_timeout(600)
|
|
page.click(f"#listBody tr:has-text('{NEW_NAME}') input[type='checkbox']")
|
|
page.wait_for_timeout(300)
|
|
pick = page.text_content("#pickHint").strip()
|
|
page.click("button[onclick='batchCompanyUpdate()']")
|
|
page.wait_for_selector("dialog#dlg[open]:has-text('批量工商更新结果')", timeout=15000)
|
|
page.wait_for_timeout(600)
|
|
cu_row = page.locator(f"dialog#dlg tbody tr:has-text('{NEW_NAME}')").count()
|
|
cu_hit = page.locator(f"dialog#dlg tbody tr:has-text('{NEW_NAME}'):has-text('未命中')").count()
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_timeout(400)
|
|
step("F6 批量工商 company-lookup/batch-update(stub 恒 NO_HIT)",
|
|
"已勾选 1 条" in pick and cu_row == 1 and cu_hit == 1, f"{pick}; 结果行={cu_row}, NO_HIT 徽标={cu_hit}")
|
|
page.click(f"#listBody tr:has-text('{NEW_NAME}') input[type='checkbox']") # 反勾还原
|
|
page.screenshot(path=str(SHOTS / "08-f6-batch.png"))
|
|
|
|
# 11.【新入口·F8 导入策略】duplicateStrategy=OVERWRITE 响应回显
|
|
page.click("nav.tabs button[data-tab='import']")
|
|
page.wait_for_timeout(800)
|
|
page.select_option("#impStrategy", "OVERWRITE")
|
|
page.set_input_files("#impFile", str(XLSX))
|
|
page.click("button:has-text('上传预检')")
|
|
page.wait_for_selector(".toast-item:has-text('预检完成')", timeout=30000)
|
|
toast = page.text_content(".toast-item:has-text('预检完成')").strip()
|
|
step("导入 upload 预检 + duplicateStrategy=OVERWRITE 回显", "策略=OVERWRITE" in toast, toast[:110])
|
|
page.screenshot(path=str(SHOTS / "09-f8-import.png"))
|
|
|
|
# 12. 交割(page 列表 + preview,只读不 initiate)
|
|
page.click("nav.tabs button[data-tab='transfer']")
|
|
page.wait_for_timeout(900)
|
|
tr_rows = page.locator("#trBody tr").count()
|
|
has_err = page.locator("#trBody .errbox").count()
|
|
page.click("button:has-text('预览名下待交接客户')")
|
|
page.wait_for_selector("dialog#dlg[open]", timeout=15000)
|
|
page.wait_for_timeout(600)
|
|
prev_fail = page.locator(".toast-item:has-text('预览失败')").count()
|
|
prev_rows = page.locator("dialog#dlg table tbody tr").count()
|
|
page.keyboard.press("Escape")
|
|
step("交割 transfer/page + preview(不 initiate,规避 BUDDY 圈定副作用)",
|
|
tr_rows >= 0 and has_err == 0 and prev_fail == 0, f"交接单行={tr_rows}, 预览行={prev_rows}")
|
|
page.screenshot(path=str(SHOTS / "10-transfer.png"))
|
|
|
|
# 13. 网络层断言:新契约形态(零 PUT/DELETE、零 /api/customer/<digits>/ 路径变量)
|
|
old_style = [u for u in api_urls if re.search(r"/api/customer/\d+/", u)]
|
|
bad_method = [u for u in api_urls if u.startswith(("PUT ", "DELETE "))]
|
|
step("网络层零旧契约形态", not old_style and not bad_method,
|
|
f"old_style={old_style[:3]} bad_method={bad_method[:3]} (共{len(api_urls)}个请求)")
|
|
browser.close()
|
|
|
|
# ---- 正门清理:协议清扫 + dedup 无条件兜底恢复 + e2c-r2demo-浏览器* archive ----
|
|
cleaned_agreements, archived = [], []
|
|
def sweep_agreements(cid, token):
|
|
try:
|
|
lst = get_json(f"http://localhost:8080/api/customer/agreement/list?customerId={cid}", token).get("data") or []
|
|
rows = lst if isinstance(lst, list) else (lst.get("content") or [])
|
|
for a in rows:
|
|
try:
|
|
post(f"http://localhost:8080/api/customer/agreement/delete", {"id": a["id"]}, token)
|
|
cleaned_agreements.append(str(a["id"]))
|
|
except Exception as ex:
|
|
print("agreement delete fail", a.get("id"), ex)
|
|
except Exception as ex:
|
|
print("agreement list fail", cid, ex)
|
|
if new_id:
|
|
sweep_agreements(new_id, tok)
|
|
# dedup 兜底:无论 UI 恢复是否成功,把起测前原值 POST 回去
|
|
try:
|
|
post("http://localhost:8080/api/rule/customer/dedup/save", {
|
|
"masterEnabled": 1 if dedup_orig.get("masterEnabled") else 0,
|
|
"nameEnabled": 1 if dedup_orig.get("nameEnabled") else 0,
|
|
"nameMatchMode": dedup_orig.get("nameMatchMode") if dedup_orig.get("nameMatchMode") is not None else 2,
|
|
"similarityThreshold": dedup_orig.get("similarityThreshold") if dedup_orig.get("similarityThreshold") is not None else 80,
|
|
"phoneEnabled": 1 if dedup_orig.get("phoneEnabled") else 0,
|
|
}, tok)
|
|
now_dedup = get_json("http://localhost:8080/api/rule/customer/dedup", tok).get("data") or {}
|
|
dedup_restored = (bool(now_dedup.get("masterEnabled")) == bool(dedup_orig.get("masterEnabled"))
|
|
and str(now_dedup.get("similarityThreshold")) == str(dedup_orig.get("similarityThreshold")))
|
|
step("查重设置兜底恢复=原值(单例)", dedup_restored, f"orig={dedup_orig} now={now_dedup}")
|
|
except Exception as ex:
|
|
print("dedup restore fail:", ex)
|
|
# 客户归档(含历史孤儿)
|
|
found = post("http://localhost:8080/api/customer/workspace/page",
|
|
{"workspace": "mine", "keyword": "e2c-r2demo-浏览器", "archiveStatus": 1, "current": 1, "size": 50}, tok)
|
|
ids = [str(r["id"]) for r in (found.get("data") or {}).get("content", [])]
|
|
for cid in ids:
|
|
sweep_agreements(cid, tok)
|
|
try:
|
|
post("http://localhost:8080/api/customer/archive", {"id": cid}, tok)
|
|
archived.append(cid)
|
|
except Exception as ex:
|
|
print("archive fail", cid, ex)
|
|
print("cleanup agreements:", cleaned_agreements, "archived:", archived)
|
|
|
|
# 汇总
|
|
summary = {"ts": TS, "new_customer": NEW_NAME, "new_id": new_id if new_id else "",
|
|
"console_errors": console_errors, "page_errors": page_errors,
|
|
"dedup_orig": dedup_orig, "archived": archived, "cleaned_agreements": cleaned_agreements,
|
|
"results": results,
|
|
"all_pass": all(r["ok"] for r in results) and not console_errors and not page_errors}
|
|
(ROOT / "demo-smoke-r2-result.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
print("\n== SUMMARY ==")
|
|
for r in results:
|
|
print(("PASS " if r["ok"] else "FAIL ") + r["step"] + (" | " + r["note"] if r["note"] else ""))
|
|
print("console_errors:", len(console_errors), "page_errors:", len(page_errors))
|
|
print("ALL_PASS:", summary["all_pass"])
|
|
|