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 = "dialogs-and-popovers"
MAIN = 1
OVERLAY_PAUSE = 0.8
def window_ids(session) -> set[int]:
return {w["id"] for w in session.call("list_windows")}
def focused_ids(session, window_id: int | None = None) -> list[int]:
return [n["id"] for n in tree.nodes(session, window_id=window_id) if n.get("focused")]
def parents_of(nodes: list[dict]) -> dict[int, int]:
return {
child: node["id"]
for node in nodes
for child in (node.get("children") or [])
}
def ancestors_of(nodes: list[dict], node_id: int) -> list[int]:
parents = parents_of(nodes)
chain, current = [], node_id
while current in parents:
current = parents[current]
chain.append(current)
return chain
def press(session, node: dict, *, kind: str = "mouse") -> None:
box = node["bounds"]
session.call(
"inject_pointer",
x=box["x"] + box["width"] / 2.0,
y=box["y"] + box["height"] / 2.0,
action="click",
kind=kind,
)
session.settle()
time.sleep(OVERLAY_PAUSE)
session.settle()
def button(session, label: str, *, window_id: int | None = None) -> dict:
found = tree.find(session, role="Button", label=label, window_id=window_id)
if found is None:
raise RuntimeError(
f"no {label!r} button in window {window_id or 'default'}; buttons: "
+ ", ".join(tree.labels(session, role="Button", window_id=window_id))
)
return found
def modal_scope(
session, report: Report, before: set[int], what: str
) -> tuple[int, bool] | None:
opened = sorted(window_ids(session) - before)
overlays = session.call("get_overlays").get("count") or 0
report.check(
len(opened) == 1 or overlays > 0,
f"the {what} opened (native window ids: {opened or 'none'}; "
f"in-tree overlays: {overlays})",
)
if opened:
report.check(
len(opened) == 1 and overlays == 0,
"…as a native modal child WINDOW, and `get_overlays` is empty "
f"because a window is not an overlay (overlays={overlays}). This "
"is the trap: a probe that looks in `get_overlays` here sees zero "
"and reports a modal that is plainly on screen as never opened",
)
return opened[0], True
if overlays:
report.note(
f"…as an in-tree modal overlay ({overlays} layers — scrim and "
"panel), because this backend has no input-blocking child window "
"for `Auto` to resolve to. Every read below addresses the parent "
"tree instead of a modal window"
)
return MAIN, False
return None
def dismissed(session, report: Report, scope: int, native: bool, how: str) -> None:
if native:
report.check(scope not in window_ids(session), f"{how} closed the modal window")
else:
report.check(
(session.call("get_overlays").get("count") or 0) == 0,
f"{how} closed the modal overlay",
)
def modal_opened(session, before: set[int]) -> bool:
return bool(window_ids(session) - before) or bool(
session.call("get_overlays").get("count") or 0
)
def result_readout(session) -> str | None:
for node in tree.nodes(session, window_id=MAIN):
value = str(node.get("value") or "")
if node.get("role") == "Label" and "dismissal=" in value:
return value
return None
def dialog_leg(session, report: Report) -> None:
trigger = button(session, "Open dialog")
before = window_ids(session)
press(session, trigger)
resolved = modal_scope(session, report, before, "Dialog")
if resolved is None:
return
modal, native = resolved
panel = tree.find(session, role="Dialog", window_id=modal)
report.check(
panel is not None and panel.get("label") == "Review Changes",
f"the modal window carries Role::Dialog named 'Review Changes' "
f"(got {panel and panel.get('label')!r})",
)
focused = focused_ids(session, window_id=modal)
default_button = tree.find(session, role="Button", label="Cancel", window_id=modal)
report.check(
bool(focused) and default_button is not None and focused[0] == default_button["id"],
"focus moved into the dialog, onto its default button ('Cancel') — "
"`ModalRequest::focus_target`, so a platform button order that puts the "
"default last still gets it focused",
)
if default_button is not None:
session.call("invoke_action", node=default_button["id"], action="click", window_id=modal)
session.settle()
time.sleep(OVERLAY_PAUSE)
session.settle()
dismissed(session, report, modal, native, "dismissing through the dialog's own action")
back = focused_ids(session, window_id=MAIN)
nodes = tree.nodes(session, window_id=MAIN)
report.check(
bool(back)
and (back[0] == trigger["id"] or trigger["id"] in ancestors_of(nodes, back[0])),
f"…and focus returned to the trigger that opened it (focus={back}, "
f"trigger={trigger['id']})",
)
def message_box_leg(session, report: Report) -> None:
before = window_ids(session)
navigate.click(session, button(session, "Save changes?"))
session.settle()
time.sleep(OVERLAY_PAUSE)
session.settle()
resolved = modal_scope(session, report, before, "MessageBox")
if resolved is None:
return
modal, native = resolved
focused = focused_ids(session, window_id=modal)
save = tree.find(session, role="Button", label="Save", window_id=modal)
report.check(
bool(focused) and save is not None and focused[0] == save["id"],
"focus landed on the declared default button ('Save'), not the first "
"focusable descendant ('Discard')",
)
session.call("inject_key", key="Escape", window_id=modal)
session.settle()
time.sleep(OVERLAY_PAUSE)
session.settle()
dismissed(session, report, modal, native, "Escape")
answer = result_readout(session)
report.check(
answer is not None and "dismissal=Escape" in answer,
f"…through its escape button, which the app recorded: {answer!r}. The "
"modal vanishing is not on its own evidence that the right path ran — "
"and the readout names the route, so a click-outside cannot pass as "
"an Escape",
)
def popover_leg(session, report: Report) -> None:
trigger = button(session, "Show popover")
focus_before = focused_ids(session)
press(session, trigger)
report.check(
session.call("get_overlays").get("count") == 1,
"the popover IS an in-tree overlay — `get_overlays` counts it, and it "
"opened no window",
)
nodes = tree.nodes(session)
body = tree.find(
nodes,
role="Label",
value="Use popovers for compact contextual actions without leaving the current surface.",
)
focused = [n["id"] for n in nodes if n.get("focused")]
report.check(
bool(focused) and body is not None and focused[0] in ancestors_of(nodes, body["id"]),
"focus moved into the popover's content (the focused node is an "
"ancestor of the popover's body text, in the same snapshot)",
)
session.call("inject_key", key="Escape")
session.settle()
time.sleep(OVERLAY_PAUSE)
session.settle()
report.check(
session.call("get_overlays").get("count") == 0, "Escape dismissed the popover",
)
back = focused_ids(session)
report.check(
back in ([trigger["id"]], focus_before),
f"…and focus returned to the trigger that opened it, or, where the "
f"press gave the trigger no focus, to where it was before "
f"(before {focus_before}, trigger {trigger['id']}, now {back})",
)
def two_press_leg(session, report: Report) -> None:
press(session, button(session, "Show popover"), kind="touch")
if session.call("get_overlays").get("count") != 1:
raise RuntimeError("the popover did not reopen for the two-press check")
underneath = button(session, "Welcome")
before = window_ids(session)
press(session, underneath, kind="touch")
report.check(
(session.call("get_overlays").get("count") or 0) == 0,
"press 1 (touch, outside the popover) dismissed it",
)
report.check(
not modal_opened(session, before),
"…and did NOT reach the button underneath — the press that dismissed is "
"spent on the dismissal",
)
press(session, underneath, kind="touch")
report.check(
modal_opened(session, before),
"press 2 activated the control underneath (new windows: "
f"{sorted(window_ids(session) - before) or 'none'}; overlays: "
f"{session.call('get_overlays').get('count')})",
)
opened = sorted(window_ids(session) - before)
for extra in opened:
session.try_call("inject_key", key="Escape", window_id=extra)
session.settle()
if not opened:
session.try_call("inject_key", key="Escape", window_id=MAIN)
session.settle()
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("dialogs-and-popovers — overlay lifecycle and focus")
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, "Dialogs and Popovers", timeout=30):
raise RuntimeError(
"the app never rendered its heading. Labels seen: "
+ ", ".join(tree.labels(session)[:20])
)
session.settle()
dialog_leg(session, report)
message_box_leg(session, report)
popover_leg(session, report)
two_press_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:]))