Case study
OklyPlay
A soundboard for streamers I built because every existing one falls apart the moment you take the mouse away.
Problem
Every streaming soundboard I found assumes you're looking at it: custom-drawn buttons and grids, controls with no name a screen reader can read, hotkeys that silently collide with whatever else is running. I wanted one that runs entirely from the keyboard, with every control properly labelled.
wxPython gets you real native controls instead of custom-drawn ones, which is most of the accessibility story already, and sounddevice gets you a mixer that can actually run more than one bus at once, layered sounds stacking, exclusive buses cutting each other off, that kind of thing. Neither of those pieces is the hard part though. The hard part is that wxPython's own accessibility support has holes, and you only find them by using the thing with a screen reader, not by reading a widget's documentation.
Approach
Two of those holes mattered enough to build around. Setting a custom accessible name on a wx.Choice or a ListBox breaks the screen reader's ability to move through the items inside it, so those controls keep wx's own default accessible object and get a plain SetName() instead, everything else gets a real custom AccessibleName class. And wx.SpinCtrl doesn't expose its internal text box as a child a screen reader can reach through Python at all, on Windows it's a native sibling window, so I walk the Win32 window handles with ctypes to find it and label that directly.
I tested the mixer itself the same way I'd use it live: dozens of keyboard-only passes through layered buses, exclusive buses, crossfading, hotkey conflicts, checking with NVDA after every change that a control still says what it's supposed to say. And I turned the part of that testing that doesn't need a human, whether a dialog control actually has the accessible name it claims to have, into an automated suite, TestAccessibilityLabeling, that asserts it directly instead of trusting a comment.
Outcome
That test suite existed for a while without actually protecting anything, it only ran if I remembered to run pytest by hand. Wiring it into GitHub Actions turned up two assertions that had gone stale, checking for a shortened label that didn't match what the dialog actually said anymore, silently wrong since whenever the label was last edited. Not a screen reader bug, but exactly the kind of thing an unrun test suite hides. It's gated on every push now.
Two releases out, v1.0.0 and v1.1.0, each one building and shipping itself through GitHub Actions the day I tag it, changelog and all. If a screen reader bug shows up in this one, it's a real bug, not a caveat in the readme.
Built with
- Python
- wxPython
- sounddevice
- PyInstaller
- GitHub Actions
- pytest
Links