115 lines
6.1 KiB
Python
115 lines
6.1 KiB
Python
"""Missing-file acceptance checks for the real private reader/preferences pair."""
|
|
import json
|
|
import subprocess
|
|
import tomllib
|
|
|
|
from native_x11 import wait_for
|
|
|
|
|
|
def check_no_config(window, tabs, server, configPath):
|
|
import pyatspi
|
|
|
|
def walk(node):
|
|
yield node
|
|
for child in node:
|
|
if child is not None:
|
|
yield from walk(child)
|
|
|
|
def controls_for(currentWindow):
|
|
nodes = list(walk(currentWindow))
|
|
controls = {node.name.rstrip(":"): node for node in nodes if node.name}
|
|
controls.update({node.name.rstrip(":"): node for node in nodes
|
|
if node.getRole() in (pyatspi.ROLE_SLIDER, pyatspi.ROLE_COMBO_BOX)})
|
|
return controls
|
|
|
|
def key(*keys):
|
|
subprocess.run(["xdotool", "key", "--clearmodifiers", *keys], check=True)
|
|
|
|
def focus(control):
|
|
assert control.queryComponent().grabFocus(), control.name
|
|
wait_for(lambda: control.getState().contains(pyatspi.STATE_FOCUSED), "focus missing: " + control.name)
|
|
|
|
def saved_speech():
|
|
if configPath.exists():
|
|
data = tomllib.loads(configPath.read_text())
|
|
return data["profiles"][data["active-profile"]]["speech"]
|
|
|
|
def find_window():
|
|
for application in pyatspi.Registry.getDesktop(0):
|
|
for candidate in application:
|
|
if candidate and candidate.name == "Cthulhu Preferences":
|
|
return candidate
|
|
|
|
assert not configPath.exists(), "opening preferences unexpectedly created a configuration"
|
|
assert tabs.querySelection().selectChild(2)
|
|
controls = controls_for(window)
|
|
assert not any(name.startswith(("Use server default", "Use Default voice", "Use automatic uppercase"))
|
|
for name in controls)
|
|
expected = {"Rate": 59, "Pitch": 4, "Volume": 9} if server.defaultsSupported else {"Rate": 50, "Pitch": 5, "Volume": 5}
|
|
for name, value in expected.items():
|
|
slider = controls[name]
|
|
assert slider.getRole() == pyatspi.ROLE_SLIDER, (name, slider.getRoleName())
|
|
assert slider.getState().contains(pyatspi.STATE_SENSITIVE), name
|
|
assert slider.queryValue().currentValue == value, (name, slider.queryValue().currentValue, value)
|
|
for previous, following in (("Volume", "Pitch"), ("Pitch", "Rate")):
|
|
focus(controls[previous]); key("Tab")
|
|
wait_for(lambda: controls[following].getState().contains(pyatspi.STATE_FOCUSED), "Tab traversal failed")
|
|
key("shift+Tab")
|
|
wait_for(lambda: controls[previous].getState().contains(pyatspi.STATE_FOCUSED), "Shift+Tab traversal failed")
|
|
|
|
voiceType = controls["Voice type"]
|
|
assert voiceType.querySelection().selectChild(1)
|
|
wait_for(lambda: controls["Pitch"].queryValue().currentValue == 7, "automatic uppercase pitch missing")
|
|
assert voiceType.querySelection().selectChild(0)
|
|
wait_for(lambda: controls["Pitch"].queryValue().currentValue == expected["Pitch"], "Default pitch did not return")
|
|
focus(controls["Apply"]); key("space")
|
|
wait_for(lambda: saved_speech() is not None, "Apply did not create the missing config")
|
|
speech = saved_speech()
|
|
assert not speech.get("default-voice"), speech
|
|
assert not speech.get("uppercase-voice"), speech
|
|
wait_for(lambda: controls["Apply"].getState().contains(pyatspi.STATE_SENSITIVE), "Apply stayed busy")
|
|
|
|
expectedRaw = {}
|
|
for name, scale, offset in (("Rate", 2, 98), ("Pitch", 20, 90), ("Volume", 15, 35)):
|
|
slider = controls[name]
|
|
focus(slider); key("Down" if name == "Volume" else "Up")
|
|
expected[name] += -1 if name == "Volume" else 1
|
|
expectedRaw[name.upper()] = int(scale * expected[name] - offset)
|
|
wait_for(lambda: slider.queryValue().currentValue == expected[name], name + " did not change directly")
|
|
wait_for(lambda: any(all(record["settings"][field] == value for field, value in expectedRaw.items())
|
|
for record in server.snapshot()), name + " preview did not reach speech backend")
|
|
focus(controls["Apply"]); key("space")
|
|
def saved_values_match():
|
|
voice = saved_speech().get("default-voice", {})
|
|
return all(voice.get(field) == expected[name] for name, field in
|
|
(("Rate", "rate"), ("Pitch", "average-pitch"), ("Volume", "gain")))
|
|
wait_for(saved_values_match, "slider values were not saved")
|
|
assert not saved_speech().get("uppercase-voice"), saved_speech()
|
|
wait_for(lambda: controls["Apply"].getState().contains(pyatspi.STATE_SENSITIVE), "Apply stayed busy")
|
|
focus(controls["Cancel"]); key("space")
|
|
wait_for(lambda: find_window() is None, "preferences did not close")
|
|
key("Insert+space")
|
|
reopened = wait_for(find_window, "preferences did not reopen")
|
|
controls = controls_for(reopened)
|
|
tabs = next(node for node in walk(reopened) if node.getRole() == pyatspi.ROLE_PAGE_TAB_LIST)
|
|
assert tabs.querySelection().selectChild(2)
|
|
for name, value in expected.items():
|
|
assert controls[name].queryValue().currentValue == value, (name, value)
|
|
assert controls["Voice type"].querySelection().selectChild(1)
|
|
wait_for(lambda: controls["Pitch"].queryValue().currentValue == max(7, expected["Pitch"] + 2),
|
|
"untouched Uppercase pitch stopped following Default")
|
|
assert controls["Voice type"].querySelection().selectChild(0)
|
|
focus(controls["Rate"]); key("Up")
|
|
changedRaw = dict(expectedRaw, RATE=expectedRaw["RATE"] + 2)
|
|
wait_for(lambda: any(record["settings"] == changedRaw for record in server.snapshot()), "unsaved preview missing")
|
|
focus(controls["Cancel"]); key("space")
|
|
wait_for(lambda: find_window() is None, "Cancel did not close preferences")
|
|
assert saved_values_match(), "Cancel changed the saved settings"
|
|
key("Insert+space")
|
|
reopened = wait_for(find_window, "preferences did not reopen after Cancel")
|
|
controls = controls_for(reopened)
|
|
assert controls["Rate"].queryValue().currentValue == expected["Rate"], "Cancel retained the unsaved rate"
|
|
(configPath.parent / "speech-defaults.json").write_text(json.dumps({
|
|
"initialConfigMissing": True, "serverDefaultsSupported": server.defaultsSupported,
|
|
"savedValues": expected, "savedBackendValues": expectedRaw}, indent=2))
|