Files
cthulhu/tests/native_tentacle_updates.py
2026-11-01 12:21:55 -05:00

78 lines
3.9 KiB
Python

"""Private GTK update confirmation and keyboard workflow; no installed packages."""
import select
from native_x11 import wait_for
def update_snapshot():
repositoryUrl = "https://example.org/tentacles"
return {
"Entries": [{"Entry": {
"ID": "probe", "LocalID": "echo", "Name": "Update probe",
"Repository": repositoryUrl, "RepositoryID": "example.source",
"Version": "1.0", "Enabled": True, "Presentation": True,
}, "State": "active"}],
"Repositories": [{"URL": repositoryUrl, "ID": "example.source", "Name": "Example",
"Tentacles": [{"ID": "echo", "Name": "Update probe", "Version": "1.1",
"Capabilities": ["presentation", "interaction"]}]}],
"Updates": [{"ID": "probe", "LocalID": "echo", "Name": "Update probe",
"InstalledVersion": "1.0", "AvailableVersion": "1.1",
"RepositoryURL": repositoryUrl, "RepositoryID": "example.source"}],
}
def check_page(pyatspi, process, window, named, walk, focus, key, request, send, initial):
tab = next(node for node in walk(window)
if node.name == "Tentacles" and node.getRole() == pyatspi.ROLE_PAGE_TAB)
assert tab.parent.querySelection().selectChild(tab.getIndexInParent()), "cannot select Tentacles tab"
table = next(node for node in walk(window)
if node.name == "Tentacles" and node.getRole() == pyatspi.ROLE_TABLE)
focus(table)
key("Home")
update = named("Update")
wait_for(lambda: update.getState().contains(pyatspi.STATE_SENSITIVE), "Update did not enable")
# Tab from the table skips disabled Install and reaches Update.
key("Tab")
wait_for(lambda: update.getState().contains(pyatspi.STATE_FOCUSED), "Tab did not reach Update")
key("Tab")
wait_for(lambda: named("Settings").getState().contains(pyatspi.STATE_FOCUSED), "Update trapped Tab")
key("shift+Tab")
wait_for(lambda: update.getState().contains(pyatspi.STATE_FOCUSED), "Shift+Tab did not reach Update")
def confirmation():
for app in pyatspi.Registry.getDesktop(0):
for node in app:
if node and node.getRole() in (pyatspi.ROLE_DIALOG, pyatspi.ROLE_ALERT):
if any("Current capabilities:" in child.name for child in walk(node)):
return node
return None
key("space")
dialog = wait_for(confirmation, "keyboard activation did not open confirmation")
message = " ".join(node.name for node in walk(dialog))
assert "version 1.0 to version 1.1" in message, message
assert "Current capabilities: presentation." in message, message
assert "Repository declares: presentation, interaction." in message, message
cancel = next(node for node in walk(dialog) if node.name == "Cancel")
focus(cancel)
key("space")
wait_for(lambda: not confirmation(), "Cancel did not close confirmation")
assert not select.select([process.stdout], [], [], 0.2)[0], "Cancel sent a management request"
# The native button action uses the same path as keyboard activation.
update.queryAction().doAction(0)
dialog = wait_for(confirmation, "button action did not open confirmation")
accept = next(node for node in walk(dialog)
if node.name == "Update" and node.getRole() == pyatspi.ROLE_PUSH_BUTTON)
focus(accept)
key("Return")
value = request()
assert value["Tentacle"] == {"Action": "upgrade", "ID": "echo", "Entry": None,
"URL": "https://example.org/tentacles"}, value["Tentacle"]
send(dict(initial, Message="Update accepted."))
wait_for(lambda: update.getState().contains(pyatspi.STATE_SENSITIVE), "response left controls busy")
focus(named("Cancel"))
key("space")
assert process.wait(timeout=5) == 0
print("PASS: Update Tab/Shift+Tab, keyboard/button activation, versions/capabilities, Cancel, upgrade request")