"""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))