from __future__ import annotations
import sys
import time
from pathlib import Path
def _bootstrap() -> None:
for ancestor in Path(__file__).resolve().parents:
candidate = ancestor.parent if ancestor.name == "teksilo_probe" else ancestor
if (candidate / "teksilo_probe" / "__init__.py").is_file():
sys.path.insert(0, str(candidate))
return
_bootstrap()
from teksilo_probe import Report, launch_and_attach, navigate, tree
APP = "data-collections"
STEP = 1200.0
SCROLL_PAUSE = 0.6
def rows(session) -> list[dict]:
return tree.find_all(session, role="ListBoxOption")
def visible_span(session) -> str:
realised = rows(session)
return f"{realised[0]['label']}..{realised[-1]['label']}" if realised else "(none)"
def list_container(session) -> dict:
found = tree.find(session, role="ListBox")
if found is None:
raise RuntimeError(
"no ListBox on screen; roles present: "
+ ", ".join(sorted({str(n.get("role")) for n in tree.nodes(session)}))
)
return found
def open_tab(session, report: Report, title: str) -> None:
tab = tree.find(session, role="Tab", label=title)
if tab is None:
raise RuntimeError(f"no {title!r} tab; tabs on screen: {tree.labels(session, role='Tab')}")
navigate.click(session, tab)
session.settle()
def virtualization_leg(session, report: Report) -> dict | None:
realised = rows(session)
report.note(f"realised rows: {len(realised)} of 200 — {visible_span(session)}")
report.check(
0 < len(realised) < 200,
f"the view realises a window of rows, not all 200 (got {len(realised)})",
)
first = tree.find(session, role="ListBoxOption", label="Item 1")
last = tree.find(session, role="ListBoxOption", label="Item 200")
report.check(first is not None, "'Item 1' is realised and has an AT node")
report.check(
last is None,
"'Item 200' has NO AT node — it is below the fold, so no widget exists "
"for it and `find` returns None. That is 'not on screen', not 'absent'",
)
return first
def scroll_leg(session, report: Report) -> None:
container = list_container(session)
for _ in range(2):
session.call("scroll", node=container["id"], dx=0.0, dy=STEP)
time.sleep(SCROLL_PAUSE)
report.note(f"wheel primed; visible {visible_span(session)}")
target = "Item 120"
found = navigate.scroll_until_found(
session,
container,
role="ListBoxOption",
label=target,
row_role="ListBoxOption",
step=STEP,
pause=SCROLL_PAUSE,
limit=30,
)
report.check(
found is not None,
f"`scroll_until_found` realised {target!r}, which no snapshot had shown",
)
if found is not None:
report.note(f"{target} id={found['id']}; visible {visible_span(session)}")
def fresh_id_leg(session, report: Report, before: dict) -> None:
container = list_container(session)
session.call("invoke_action", node=container["id"], action="focus")
session.settle()
session.call("inject_key", key="Home")
session.settle()
time.sleep(0.4)
session.settle()
after = tree.find(session, role="ListBoxOption", label="Item 1")
report.check(after is not None, "'Item 1' is realised again after coming back to the top")
if after is None:
return
report.check(
after["id"] != before["id"],
f"…and it is a NEW widget: id {before['id']} → {after['id']}. A "
"virtualizer destroys a row that leaves its buffer, so an id is stable "
"for a widget's lifetime, not for a probe's",
)
stale = session.try_call("read_node", node=before["id"])
report.check(
(not stale.ok) and stale.code == "NOT_FOUND",
f"the id captured before the scroll is dead: read_node → {stale.code or 'ok'}. "
"Re-find between a scroll and a click; never hold an id across one",
)
def keyboard_leg(session, report: Report) -> None:
container = list_container(session)
session.call("invoke_action", node=container["id"], action="focus")
session.settle()
session.call("inject_key", key="End")
session.settle()
time.sleep(0.4)
session.settle()
last = tree.find(session, role="ListBoxOption", label="Item 200")
report.check(
last is not None,
"one `End` realised 'Item 200' — exactly, and in one call",
)
if last is not None:
report.check(
bool(last.get("selected")),
"…and selected it, so focus and selection followed the view",
)
report.note(f"visible {visible_span(session)}; Item 200 id={last['id']}")
def tree_leg(session, report: Report) -> None:
open_tab(session, report, "TreeView")
time.sleep(0.4)
report.check(
tree.find(session, role="TreeItem", label="Projects") is None,
"'Projects' has no node while 'Documents' is collapsed",
)
branch = tree.find(session, role="TreeItem", label="Documents")
if branch is None:
raise RuntimeError(f"no 'Documents' row; rows: {tree.labels(session, role='TreeItem')}")
report.check(
branch.get("expanded") is False,
"'Documents' advertises itself as collapsed (expanded=False)",
)
session.call("expand", node=branch["id"])
session.settle()
time.sleep(0.5)
session.settle()
report.check(
tree.find(session, role="TreeItem", label="Projects") is not None,
"expanding 'Documents' realised its children",
)
reopened = tree.find(session, role="TreeItem", label="Documents")
report.check(
reopened is not None and reopened["id"] != branch["id"],
f"…and rebuilt the branch itself: id {branch['id']} → "
f"{reopened and reopened['id']}. Even the node you acted on is stale afterwards",
)
def main(argv: list[str]) -> int:
binary = None
keep = False
rest = list(argv)
if "--keep" in rest:
rest.remove("--keep")
keep = True
if "--binary" in rest:
i = rest.index("--binary")
if i + 1 >= len(rest):
print("--binary needs a path", file=sys.stderr)
return 1
binary = rest[i + 1]
del rest[i : i + 2]
if rest:
print(f"unexpected arguments: {' '.join(rest)}\n\n{__doc__}", file=sys.stderr)
return 1
report = Report("data-collections — virtualized ListView / TreeView")
app = session = None
try:
app, session = launch_and_attach(
argv=[binary] if binary else None, app=APP, label=APP,
)
if not tree.wait_for(session, "Repeater", timeout=30):
raise RuntimeError(
"the app never rendered its tab bar. Labels seen: "
+ ", ".join(tree.labels(session)[:20])
)
session.settle()
open_tab(session, report, "ListView")
item_one = virtualization_leg(session, report)
scroll_leg(session, report)
if item_one is not None:
fresh_id_leg(session, report, item_one)
keyboard_leg(session, report)
tree_leg(session, report)
except Exception as exc: report.error(f"{type(exc).__name__}: {exc}")
finally:
if session is not None:
session.close()
if app is not None:
if keep:
report.note(f"--keep: app left running as pid {app.proc.pid}; log {app.log}")
else:
app.terminate()
return report.finish()
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))