101 lines
5.6 KiB
Python
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)
|