Keep the last in-template copy on main so history can restore it; product continues in jiangchang-ui-recorder. Co-authored-by: Cursor <cursoragent@cursor.com>
384 lines
14 KiB
Python
384 lines
14 KiB
Python
"""Smoke test for flow document API + surface routing + kit helpers (v0.10)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
if str(_HERE) not in sys.path:
|
|
sys.path.insert(0, str(_HERE))
|
|
|
|
import artifacts as art
|
|
import desktop_uia as uia
|
|
import kit_browser
|
|
import kit_wake
|
|
import skip_policy
|
|
import target
|
|
import user_prefs
|
|
from surfaces.registry import get_driver
|
|
from drivers.mobile import MobileDriver
|
|
from jiangchang_skill_core.rpa.page_dump import build_page_dump_result
|
|
|
|
|
|
def _assert_selection_api() -> None:
|
|
assert callable(uia.window_info_under_cursor)
|
|
assert callable(uia.find_wecom_search_result_window)
|
|
assert callable(uia.resolve_capture_target)
|
|
try:
|
|
_info, _cands, meta = uia.resolve_capture_target(
|
|
under_cursor=False,
|
|
prefer_wecom_search_popup=False,
|
|
)
|
|
assert "strategy" in meta
|
|
except RuntimeError as exc:
|
|
assert "未找到可拾取" in str(exc)
|
|
print("OK selection api")
|
|
|
|
|
|
def _assert_kit_wake_detect() -> None:
|
|
assert kit_wake.looks_like_wecom(class_name="WeWorkWindow", title="企业微信")
|
|
assert kit_wake.resolve_wake_profile(class_name="SearchResultWindow2") == "wecom_search"
|
|
assert not kit_wake.looks_like_wecom(class_name="Chrome_WidgetWin_1", title="Google")
|
|
r = kit_wake.auto_ensure_if_wecom(class_name="NotWeCom", title="x")
|
|
assert r.get("skipped") and r.get("reason") == "not_wecom"
|
|
print("OK kit_wake detect")
|
|
|
|
|
|
def _assert_surface_routing() -> None:
|
|
assert (
|
|
target.resolve_surface(process_name="chrome.exe", class_name="Chrome_WidgetWin_1")
|
|
== target.SURFACE_BROWSER
|
|
)
|
|
assert (
|
|
target.resolve_surface(process_name="msedge.exe", class_name="Chrome_WidgetWin_1")
|
|
== target.SURFACE_BROWSER
|
|
)
|
|
assert (
|
|
target.resolve_surface(process_name="WXWork.exe", class_name="WeWorkWindow")
|
|
== target.SURFACE_WINDOWS
|
|
)
|
|
assert get_driver(target.SURFACE_WINDOWS).driver_name == "uiautomation"
|
|
assert get_driver(target.SURFACE_BROWSER).driver_name == "playwright"
|
|
mobile = MobileDriver(surface=target.SURFACE_ANDROID)
|
|
try:
|
|
mobile.dump(info=None, selection_meta={})
|
|
raise AssertionError("mobile dump should raise")
|
|
except RuntimeError as exc:
|
|
assert "SURFACE_UNSUPPORTED" in str(exc)
|
|
print("OK surface routing")
|
|
|
|
|
|
def _assert_user_prefs() -> None:
|
|
with tempfile.TemporaryDirectory(prefix="ui_prefs_") as tmp:
|
|
path = Path(tmp) / "user_prefs.json"
|
|
# monkeypatch prefs path
|
|
old = user_prefs._PREFS_PATH
|
|
try:
|
|
user_prefs._PREFS_PATH = path
|
|
user_prefs.save_prefs(mode="windows", start_url="https://example.com")
|
|
loaded = user_prefs.load_prefs()
|
|
assert loaded["mode"] == "windows"
|
|
assert loaded["start_url"] == "https://example.com"
|
|
user_prefs.save_prefs(mode="browser", start_url="")
|
|
loaded_empty = user_prefs.load_prefs()
|
|
assert loaded_empty["start_url"] == ""
|
|
user_prefs.save_prefs(mode="mobile", start_url="https://a.test")
|
|
loaded2 = user_prefs.load_prefs()
|
|
assert loaded2["mode"] == target.SURFACE_ANDROID
|
|
finally:
|
|
user_prefs._PREFS_PATH = old
|
|
print("OK user_prefs")
|
|
|
|
|
|
def _assert_skip_policy() -> None:
|
|
# blank + empty locatable → skip
|
|
assert (
|
|
skip_policy.should_skip_empty_dump(
|
|
surface="browser",
|
|
url="about:blank",
|
|
locatable_count=0,
|
|
nodes_kept=1,
|
|
sparse=True,
|
|
)
|
|
== "blank_or_newtab_empty"
|
|
)
|
|
# blank URL but rich content (nav race like step05) → keep
|
|
assert (
|
|
skip_policy.should_skip_empty_dump(
|
|
surface="browser",
|
|
url="about:blank",
|
|
locatable_count=74,
|
|
nodes_kept=97,
|
|
sparse=False,
|
|
)
|
|
is None
|
|
)
|
|
# real page → keep
|
|
assert (
|
|
skip_policy.should_skip_empty_dump(
|
|
surface="browser",
|
|
url="https://www.douyin.com/jingxuan",
|
|
locatable_count=150,
|
|
nodes_kept=160,
|
|
sparse=False,
|
|
)
|
|
is None
|
|
)
|
|
# windows never skipped by this policy
|
|
assert (
|
|
skip_policy.should_skip_empty_dump(
|
|
surface="windows",
|
|
url="",
|
|
locatable_count=0,
|
|
nodes_kept=1,
|
|
sparse=True,
|
|
)
|
|
is None
|
|
)
|
|
# same URL with content is NOT skipped (scroll-safe)
|
|
assert (
|
|
skip_policy.should_skip_empty_dump(
|
|
surface="browser",
|
|
url="https://www.douyin.com/jingxuan",
|
|
locatable_count=20,
|
|
nodes_kept=30,
|
|
sparse=False,
|
|
)
|
|
is None
|
|
)
|
|
print("OK skip_policy")
|
|
|
|
|
|
def _assert_active_page_resolve() -> None:
|
|
"""Unit-level: visible tab wins over sticky pages[0]."""
|
|
|
|
class _FakePage:
|
|
def __init__(self, url: str, visible: bool, *, closed: bool = False) -> None:
|
|
self.url = url
|
|
self._visible = visible
|
|
self._closed = closed
|
|
|
|
def is_closed(self) -> bool:
|
|
return self._closed
|
|
|
|
def evaluate(self, _script: str) -> str:
|
|
return "visible" if self._visible else "hidden"
|
|
|
|
def title(self) -> str:
|
|
return self.url
|
|
|
|
class _FakeCtx:
|
|
def __init__(self, pages: list) -> None:
|
|
self.pages = pages
|
|
|
|
search = _FakePage("https://www.douyin.com/search/foo", visible=False)
|
|
detail = _FakePage("https://www.douyin.com/video/123", visible=True)
|
|
ctx = _FakeCtx([search, detail])
|
|
|
|
page, meta = kit_browser.resolve_active_page(ctx, preferred=search)
|
|
assert page is detail, (page.url, meta)
|
|
assert meta["resolve"] == "visibility"
|
|
assert meta["page_count"] == 2
|
|
assert meta["visible_count"] == 1
|
|
|
|
# No visible → preferred if still open
|
|
both_hidden = _FakeCtx(
|
|
[
|
|
_FakePage("https://a.test/1", visible=False),
|
|
_FakePage("https://a.test/2", visible=False),
|
|
]
|
|
)
|
|
preferred = both_hidden.pages[0]
|
|
page2, meta2 = kit_browser.resolve_active_page(both_hidden, preferred=preferred)
|
|
assert page2 is preferred
|
|
assert meta2["resolve"] == "preferred_fallback"
|
|
|
|
# Closed preferred → first open
|
|
closed_pref = _FakePage("https://gone", visible=False, closed=True)
|
|
open_only = _FakePage("https://alive", visible=False)
|
|
page3, meta3 = kit_browser.resolve_active_page(
|
|
_FakeCtx([closed_pref, open_only]), preferred=closed_pref
|
|
)
|
|
assert page3 is open_only
|
|
assert meta3["resolve"] == "first_open"
|
|
print("OK active page resolve")
|
|
|
|
|
|
def _assert_page_dump_kit() -> None:
|
|
if not kit_browser.kit_page_dump_available():
|
|
raise RuntimeError(
|
|
"jiangchang_skill_core.rpa.page_dump 不可用;"
|
|
"请 pip install -e jiangchang-platform-kit>=1.2.11"
|
|
)
|
|
result = build_page_dump_result(
|
|
url="https://example.com",
|
|
title="Example",
|
|
a11y_snapshot={
|
|
"role": "RootWebArea",
|
|
"name": "Example",
|
|
"children": [{"role": "button", "name": "OK", "children": []}],
|
|
},
|
|
dom_nodes=[
|
|
{
|
|
"tag": "button",
|
|
"role": "button",
|
|
"id": "",
|
|
"name_attr": "",
|
|
"testid": "",
|
|
"aria_label": "OK",
|
|
"placeholder": "",
|
|
"title": "",
|
|
"alt": "",
|
|
"href": "",
|
|
"type": "submit",
|
|
"text": "OK",
|
|
"disabled": False,
|
|
"rect": {},
|
|
}
|
|
],
|
|
)
|
|
assert result["stats"]["locatable_count"] >= 1
|
|
assert result["locatable"][0].get("primary_locator")
|
|
print("OK page_dump kit builder")
|
|
|
|
|
|
def _assert_label() -> None:
|
|
msg_page = [
|
|
{"control_type": "TreeItemControl", "name": "groupbuddyname", "value": "某群"},
|
|
{"control_type": "TreeItemControl", "name": "send_rich_edit", "value": "你好"},
|
|
{"control_type": "TreeItemControl", "name": "con_op", "value": "消息"},
|
|
]
|
|
searching = [
|
|
{"control_type": "TreeItemControl", "name": "groupbuddyname", "value": "徐书建"},
|
|
{"control_type": "TreeControl", "name": "searchedit", "value": "陈德炼"},
|
|
{"control_type": "TreeItemControl", "name": "send_rich_edit", "value": "陈德炼"},
|
|
]
|
|
selected = [
|
|
{
|
|
"control_type": "TreeItemControl",
|
|
"name": "groupbuddyname",
|
|
"value": "陈德炼<c #99ffffff>@微信</c>",
|
|
},
|
|
{"control_type": "TreeItemControl", "name": "send_rich_edit", "value": "您好"},
|
|
]
|
|
contacts = [
|
|
{"control_type": "TreeItemControl", "name": "panel_title_label", "value": "RPA之家"},
|
|
{"control_type": "TreeItemControl", "name": "main_chattext", "value": "新的客户"},
|
|
]
|
|
new_customer = [
|
|
{"control_type": "TreeItemControl", "name": "panel_title_label", "value": "新的客户"},
|
|
{"control_type": "TreeItemControl", "name": "ww_btn_text", "value": "添加"},
|
|
]
|
|
add_empty = [
|
|
{"control_type": "TreeItemControl", "name": "title", "value": "添加客户"},
|
|
{"control_type": "TreeControl", "name": "searchedit", "value": ""},
|
|
{"control_type": "WindowControl", "name": "添加客户", "value": ""},
|
|
]
|
|
add_result = [
|
|
{"control_type": "TreeItemControl", "name": "title", "value": "添加客户"},
|
|
{"control_type": "TreeControl", "name": "searchedit", "value": "13316463772"},
|
|
{"control_type": "TreeItemControl", "name": "main_chattext", "value": "Nancy"},
|
|
{"control_type": "TreeItemControl", "name": "ww_btn_text", "value": "添加为"},
|
|
{"control_type": "TreeControl", "name": "search_result", "value": ""},
|
|
]
|
|
|
|
a = art.derive_step_label(window_title="企业微信", locatable=msg_page)
|
|
b = art.derive_step_label(window_title="企业微信", locatable=searching)
|
|
c = art.derive_step_label(window_title="企业微信", locatable=selected)
|
|
d = art.derive_step_label(window_title="企业微信", locatable=contacts)
|
|
e = art.derive_step_label(window_title="企业微信", locatable=new_customer)
|
|
f = art.derive_step_label(window_title="添加客户", locatable=add_empty)
|
|
g = art.derive_step_label(window_title="添加客户", locatable=add_result)
|
|
|
|
assert "输入你好" in a or "某群" in a, a
|
|
assert "搜索陈德炼" in b, b
|
|
assert "陈德炼" in c and "输入您好" in c, c
|
|
assert "RPA之家" in d, d
|
|
assert "新的客户" in e and "添加" in e, e
|
|
assert f.startswith("添加客户"), f
|
|
assert "13316463772" in g or "搜索13316463772" in g, g
|
|
assert "会话列表" not in a and "会话列表" not in b and "会话列表" not in c
|
|
print(f"OK labels:\n {a}\n {b}\n {c}\n {d}\n {e}\n {f}\n {g}")
|
|
|
|
|
|
def main() -> int:
|
|
_assert_selection_api()
|
|
_assert_kit_wake_detect()
|
|
_assert_surface_routing()
|
|
_assert_user_prefs()
|
|
_assert_skip_policy()
|
|
_assert_active_page_resolve()
|
|
_assert_page_dump_kit()
|
|
_assert_label()
|
|
with tempfile.TemporaryDirectory(prefix="ui_flow_smoke_") as tmp:
|
|
root = Path(tmp)
|
|
flow_dir = art.create_flow(root, flow_name="demo-flow")
|
|
flow = art.load_flow(flow_dir)
|
|
|
|
loc = [
|
|
{
|
|
"control_type": "TreeItemControl",
|
|
"name": "groupbuddyname",
|
|
"value": "陈德炼<c #99ffffff>@微信</c>",
|
|
},
|
|
{"control_type": "TreeItemControl", "name": "send_rich_edit", "value": "您好"},
|
|
]
|
|
label = art.resolve_step_label(
|
|
flow, window_title="企业微信", window_class="WeWorkWindow", locatable=loc
|
|
)
|
|
step_dir, idx, name = art.add_step_dir(
|
|
flow_dir,
|
|
flow,
|
|
window_title="企业微信",
|
|
window_class="WeWorkWindow",
|
|
locatable=loc,
|
|
label=label,
|
|
)
|
|
assert idx == 1
|
|
assert "陈德炼" in name, name
|
|
art.write_json(
|
|
step_dir / "meta.json",
|
|
art.build_meta(
|
|
window={"title": "企业微信", "class_name": "WeWorkWindow", "pid": 1, "hwnd": 2},
|
|
scene="demo",
|
|
surface="windows",
|
|
driver="uiautomation",
|
|
),
|
|
)
|
|
art.register_step(
|
|
flow_dir,
|
|
flow,
|
|
step_index=idx,
|
|
step_name=name,
|
|
window={"title": "企业微信", "class_name": "WeWorkWindow", "pid": 1, "hwnd": 2},
|
|
stats={"findall_count": 10, "locatable_count": 5, "nodes_kept": 3, "uia_sparse": False},
|
|
label=label,
|
|
surface="windows",
|
|
)
|
|
# browser-shaped step registration
|
|
blow = art.load_flow(flow_dir)
|
|
art.register_step(
|
|
flow_dir,
|
|
blow,
|
|
step_index=2,
|
|
step_name="02_browser-demo",
|
|
window={"title": "精选", "class_name": "PlaywrightPage", "pid": 3, "hwnd": 4},
|
|
stats={"findall_count": 20, "locatable_count": 12, "nodes_kept": 20, "uia_sparse": False},
|
|
label="浏览器精选",
|
|
surface="browser",
|
|
url="https://www.douyin.com/jingxuan",
|
|
)
|
|
flow = art.end_flow(flow_dir, art.load_flow(flow_dir))
|
|
assert flow["status"] == "ended"
|
|
summary = (flow_dir / "SUMMARY.md").read_text(encoding="utf-8")
|
|
assert "surface: `browser`" in summary or "surface: `windows`" in summary
|
|
print(f"OK flow={flow_dir.name} step={name}")
|
|
print("OK ui_inspector flow smoke passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|