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, tree
APP = "rich-text-editor"
MARK = "PROBE-42 "
PREEDIT = "にほん"
COMMIT = "日本"
EDIT_PAUSE = 0.8
def editor(session) -> dict:
found = tree.find(session, role="MultilineTextInput")
if found is None:
raise RuntimeError(
"no editable RichTextEditor on screen; roles present: "
+ ", ".join(sorted({str(n.get("role")) for n in tree.nodes(session)}))
)
return found
def text_runs_under(session, root_id: int) -> list[str]:
nodes = tree.nodes(session)
by_id = {n["id"]: n for n in nodes}
out: list[str] = []
def walk(node_id: int) -> None:
node = by_id.get(node_id)
if node is None:
return
if node.get("role") == "TextRun" and node.get("value"):
out.append(str(node["value"]))
for child in node.get("children") or []:
walk(child)
walk(root_id)
return out
def subtree_ids(session, root_id: int) -> set[int]:
nodes = tree.nodes(session)
by_id = {n["id"]: n for n in nodes}
seen: set[int] = set()
stack = [root_id]
while stack:
current = stack.pop()
if current in seen or current not in by_id:
continue
seen.add(current)
stack.extend(by_id[current].get("children") or [])
return seen
def runs_containing(session, needle: str) -> list[str]:
return [
str(n["value"])
for n in tree.nodes(session)
if n.get("role") == "TextRun" and needle in str(n.get("value") or "")
]
def opening_line(session) -> str:
runs = text_runs_under(session, editor(session)["id"])
return runs[0] if runs else ""
def settle_edit(session) -> None:
session.settle()
time.sleep(EDIT_PAUSE)
session.settle()
def typing_leg(session, report: Report) -> str:
before = opening_line(session)
report.note(f"document opens with {before!r}")
report.check(
MARK.strip() not in before,
f"the document does not already contain {MARK.strip()!r} — so finding it "
"later can only mean this probe put it there",
)
session.call("type_text", node=editor(session)["id"], text=MARK)
settle_edit(session)
after = opening_line(session)
report.check(
after.startswith(MARK),
f"typed text landed at the caret: {before!r} → {after!r}",
)
inside = subtree_ids(session, editor(session)["id"])
elsewhere = [
str(n["value"])
for n in tree.nodes(session)
if n.get("role") == "TextRun"
and MARK.strip() in str(n.get("value") or "")
and n["id"] not in inside
]
report.check(
bool(elsewhere),
f"…and in the read-only preview, outside the editor's subtree: "
f"{elsewhere[:1]}. One TextDocument, two views",
)
return before
def undo_leg(session, report: Report, before: str) -> None:
session.call("inject_key", key="Z", command=True)
settle_edit(session)
report.check(
opening_line(session) == before,
f"undo restored the opening line exactly: {opening_line(session)!r}",
)
report.check(
not runs_containing(session, MARK.strip()),
"…and the marker is gone from every pane, preview included",
)
def ime_leg(session, report: Report) -> None:
session.call("type_ime", node=editor(session)["id"], preedit=PREEDIT)
settle_edit(session)
composing = runs_containing(session, PREEDIT)
report.check(
bool(composing),
f"the uncommitted preedit {PREEDIT!r} is laid out in the document "
f"({len(composing)} run(s)) — composition is observable, not just its result",
)
session.call("type_ime", node=editor(session)["id"], commit=COMMIT)
settle_edit(session)
report.check(
bool(runs_containing(session, COMMIT)),
f"committing replaced it with {COMMIT!r}",
)
report.check(
not runs_containing(session, PREEDIT),
f"…and left no trace of the preedit — {PREEDIT!r} is gone from the document",
)
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("rich-text-editor — typing, IME and undo")
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, "RichTextEditor", timeout=45):
raise RuntimeError(
"the document never rendered. Labels seen: "
+ ", ".join(tree.labels(session)[:20])
)
session.settle()
before = typing_leg(session, report)
undo_leg(session, report, before)
ime_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:]))