"""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, "SessionEvents": True, "RemoteGrants": ["remote-present"], }, "State": "active"}], "Repositories": [{"URL": repositoryUrl, "ID": "example.source", "Name": "Example", "Tentacles": [{"ID": "echo", "Name": "Update probe", "Version": "1.1", "Capabilities": ["presentation", "interaction", "output-observe", "keyboard-inject"]}]}], "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, remote-present." in message, message assert "Repository declares: presentation, interaction, output-observe, keyboard-inject." in message, message assert "accept keyboard input from the remote controller" in message, message assert "Each session requires local approval" 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")