"""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": "陈德炼@微信", }, {"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": "陈德炼@微信", }, {"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())