Files
cthulhu/tests/native_tentacle_extensions.py

101 lines
5.6 KiB
Python

"""Private native controls for declarative Tentacle preference pages."""
import copy
from native_x11 import wait_for
def probe_pages():
return [{"Session": "private-probe", "Revision": 1,
"id": "probe", "title": "Extension probe", "command": "apply", "load": True,
"elements": [{"id": "text", "label": "Extension text", "kind": 2,
"required": True, "max_length": 32},
{"id": "choice", "label": "Extension choice", "kind": 1,
"items": [{"id": "first", "label": "First choice"},
{"id": "second", "label": "Second choice"}]},
{"id": "enabled", "label": "Extension enabled", "kind": 3}],
"values": {"text": {"text": "Initial"}, "choice": {"selected": "first"},
"enabled": {"checked": False}}}]
def check_page(pyatspi, process, window, nodes, walk, focus, key, request, send, initial):
def find(name, role=None):
return next(node for node in nodes if node.name == name and
(role is None or node.getRole() == role))
def focused(node):
return any(child.getState().contains(pyatspi.STATE_FOCUSED) for child in walk(node))
tabs = next(node for node in nodes if node.getRole() == pyatspi.ROLE_PAGE_TAB_LIST)
index = next(index for index, node in enumerate(tabs) if node.name == "Extension probe")
assert tabs.querySelection().selectChild(index)
text = find("Extension text", pyatspi.ROLE_TEXT)
choice = find("Extension choice", pyatspi.ROLE_COMBO_BOX)
enabled = find("Extension enabled", pyatspi.ROLE_CHECK_BOX)
cancelButton = find("Cancel", pyatspi.ROLE_PUSH_BUTTON)
assert text.queryText().getText(0, -1) == "Initial"
labels = [node for node in nodes if node.name == "Extension text" and
node.getRole() == pyatspi.ROLE_LABEL]
assert labels and any(relation.getRelationType() == pyatspi.RELATION_LABEL_FOR
for relation in labels[0].getRelationSet()), "missing label buddy"
status = find(initial["Message"])
focus(text)
key("Tab")
key("Down")
# GTK focuses an internal toggle button that need not expose FOCUSED in
# AT-SPI. A keyboard selection change proves that Tab reached the combo.
wait_for(lambda: choice.querySelection().getSelectedChild(0).name == "Second choice",
"Tab did not reach the combo or Down did not change its value")
key("Tab")
wait_for(lambda: focused(enabled), "extension combo trapped Tab")
key("space")
key("shift+Tab")
key("Up")
wait_for(lambda: choice.querySelection().getSelectedChild(0).name == "First choice",
"Shift+Tab did not return to the combo")
key("space")
wait_for(lambda: any(node.getRole() == pyatspi.ROLE_MENU and
node.getState().contains(pyatspi.STATE_SHOWING) for node in walk(choice)),
"extension combo popup did not open from the keyboard")
key("End", "Return")
wait_for(lambda: choice.querySelection().getSelectedChild(0).name == "Second choice",
"popup selection was not accepted")
key("shift+Tab")
wait_for(lambda: focused(text), "extension combo trapped Shift+Tab")
key("ctrl+a")
key("e", "d", "i", "t")
wait_for(lambda: text.queryText().getText(0, -1) == "edit", "entry edit missing")
# A status refresh with unchanged revisions must preserve the active draft.
send({"ExtensionsOnly": True, "ExtensionPages": initial["ExtensionPages"]})
wait_for(lambda: focused(text), "refresh changed focus")
assert text.queryText().getText(0, -1) == "edit"
focus(find("Apply", pyatspi.ROLE_PUSH_BUTTON)); key("space")
draft = request()
page = draft["ExtensionPages"][0]
assert page["Session"] == "private-probe" and page["Revision"] == 1
assert page["Values"] == {"text": {"text": "edit"}, "choice": {"selected": "second"},
"enabled": {"checked": True}}, page
send({"Accepted": False, "CanSave": True, "Message": "Probe rejected"})
wait_for(lambda: status.name == "Probe rejected", "rejection missing")
assert text.queryText().getText(0, -1) == "edit", "rejection discarded draft"
focus(find("Apply", pyatspi.ROLE_PUSH_BUTTON)); key("space")
draft = request()
pages = copy.deepcopy(initial["ExtensionPages"])
pages[0]["Revision"] = 2
pages[0]["values"] = draft["ExtensionPages"][0]["Values"]
send({"Accepted": True, "CanSave": True, "Values": draft["Values"],
"ExtensionPages": pages, "Message": "Probe applied"})
wait_for(lambda: status.name == "Probe applied", "apply response missing")
assert tabs.querySelection().getSelectedChild(0).name == "Extension probe", "Apply changed the selected tab"
nodes = list(walk(window))
text = find("Extension text", pyatspi.ROLE_TEXT)
assert text.queryText().getText(0, -1) == "edit", "applied values not restored"
# Removing a focused page must leave the core controls usable.
focus(text)
send({"ExtensionsOnly": True, "ExtensionPages": []})
wait_for(lambda: not any(node.name == "Extension probe" and
node.getRole() == pyatspi.ROLE_PAGE_TAB for node in tabs),
"revoked extension tab survived")
assert tabs.querySelection().getSelectedChild(0).name == "General", "removal did not return to a core page"
focus(cancelButton); key("space")
assert process.wait(timeout=5) == 0
print("PASS: extension labels, entries, combos, checkboxes, Tab/Shift+Tab, draft retention, Apply rejection/acceptance and focused-page removal", flush=True)