Skip to content

Attempt to add screenreader once again - #6257

Draft
JorisGoosen wants to merge 62 commits into
jasp-stats:developmentfrom
JorisGoosen:screenreader
Draft

Attempt to add screenreader once again#6257
JorisGoosen wants to merge 62 commits into
jasp-stats:developmentfrom
JorisGoosen:screenreader

Conversation

@JorisGoosen

@JorisGoosen JorisGoosen commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Just trying some things here

@JorisGoosen JorisGoosen changed the title make a fair attempt at making the components a bit more informative Attempt to add screenreader once again Jun 8, 2026
JorisGoosen and others added 27 commits July 23, 2026 10:00
- Added ARIA attributes to tables: role='table', aria-label, tabindex, role='gridcell', role='columnheader'/role='rowheader', role='alert' for errors, role='rowgroup' for footnotes
- Added accessibility to notes: role='region', role='textbox', role='button' for close buttons
- Added QML accessibility to menus: Accessible.Menu, Accessible.MenuItem, Accessible.Separator with name properties
- Added CSS focus indicators and high contrast mode support
- Fixed Rcpp compilation with ENABLE_LEGACY_NONAPI flag
- Added accessibility unit tests using AT-SPI2

Note: Menu bar menus may need to be accessed via keyboard shortcuts or opened to be visible in AT-SPI2 tree
- Added tests for main menu buttons (Main menu, Modules menu)
- Added tests for Analysis menu accessibility with 'Analysis menu' name
- Added tests for menu items (Open, Save buttons)
- Added tests for document web elements (tables, notes)
- All tests use AT-SPI2 DBus API via xvfb-run headless display
- Tests verify that screen readers can access menus, tables, and notes
- Added 24 comprehensive tests covering menus, buttons, results, panels
- Tests verify accessible roles present (document web, panel, frame, etc.)
- Tests check accessible names and tree depth
- Made optional checks for roles not yet implemented (spin box, table, alert)
- All tests pass in headless xvfb-run environment
The QML WebEngineView accessibility is not exposed to AT-SPI.
This test documents and validates that HTML content (results, notes,
tables) cannot be navigated or accessed via accessibility APIs.

Test 25 confirms webengine focus cannot be obtained and HTML content
remains isolated from system accessibility trees.

This is a fundamental limitation of Qt 6.7's QML WebEngine accessibility
implementation - the bridge returns QAccessible::Client instead of
exposing the browser's AXTree.
- Remove boost_system and boost_filesystem from CMake components
  (header-only in Boost 1.87+, Arch doesn't ship cmake configs)
- Add missing librdata, libfreexl, and libsodium library detection
  (needed by RData importer, Excel importer, encryption)
- Remove duplicate jaspResults target from Engine/CMakeLists.txt
- Remove QColumnUtils dependency from engine.cpp (QML header not
  available in engine process)
- Switch CRAN mirror from packagemanager.posit.co to cloud.r-project.org
- Update Rcpp lockfile version from 1.1.1-1.1 to 1.1.2
  (old version no longer available on CRAN)
- Rewrite tests to find main JASP window by structure (frame, 77+ children)
  instead of ambiguous name search that picked engine subprocess
- Fix button/menu searches to use main_window scope
- Remove all skipTest workarounds for WebEngine limitation
  (bridge is now working, tests validate actual accessibility)
- Mark state-dependent tests as skipTest (spinbox/table/alert
  not present on startup)
- Add check_atspi.py helper and run_accessibility_tests.sh wrapper
  that sets up dbus session, AT-SPI bus, and Xvfb
- HelpWindow.qml: add Accessible.name to WebEngineView
- MenuHeader.qml: add text=Help to info-button so it has an
  accessible name for AT-SPI discovery
- test_help_accessibility.py: AT-SPI test that opens Help window
  by clicking Main menu → Preferences → Help button, then
  verifies the WebEngine document web has accessible name,
  children, headings and text content
- run_help_test.sh: test runner with dbus session and AT-SPI setup
…nd proper cleanup

The AT-SPI tree has two document web nodes:
- Outer: Qt accessible wrapper (may have empty name)
- Inner: browser accessibility tree with actual content

Fix the name check to use the inner node, add trap cleanup for JASP
isMember() on a Json::Value that is an array (not an object) throws
'requires objectValue or nullValue' in jsoncpp. The jaspDescriptives
manifest uses version:[[0,96,5]] (array format), so parsing it hit
the else-if branch where isMember('Version') was called on the array,
silently skipping the module. Fixed by checking isObject() first.

Also added a check for Json::Reader::parse() return value so
malformed JSON gives a clear error instead of a cryptic crash.
…nt count

JASP has multiple document web elements (help, about, community,
results). The test now picks the doc with the most descendants
instead of the first one found. Also increased find_all() max_depth
from 4 to 6 to find deeply nested tables and sections.
- New env_accessibility.sh: single source for DISPLAY, QtWebEngine paths,
  repo root, accessibility flags. Overridable via env vars before sourcing.
- New accessibility_common.py: shared AT-SPI helpers (find_jasp_app,
  find_document_web, click_element, find_all, dump_tree, dismiss_dialogs).
  Accepts main_window_names param for pages that rename the frame.
- All three shell runners now use identical pattern: source env, start
  AT-SPI bus+registry, start JASP, run Python, cleanup.
- test_accessibility.py: process management removed (shell handles it);
  uses find_document_web() with descendant-count heuristic; removed
  outdated skip message about zygote/renderer issue.
- test_help_accessibility.py and test_results_accessibility.py: import
  from common module, remove duplicated inline functions.
- All paths derived from repo root (no hardcoded /home/virtuoos/...).
…tend AT-SPI tests

- Add Accessible.name to reusable base components: WavyWindow, MenuHeader,
  PrefsGroupRect, Group (uncomment), MessageBox, ErrorMessage, RibbonBar,
  Ribbons, ModulesMenu, FileMenu, MainWindow, MainPage, DataPanel,
  DataTableView, VariablesWindow, FilterWindow, AnalysisForms, WelcomePage,
  JASPSplitHandle, RCommanderWindow, EnginesWindow, EncryptionSettingsWindow

- Extend test_accessibility.py from 25 to 45 tests:
  - File menu navigation: open, action buttons, sub-items, preferences
  - Dialog windows: Help, About, Contact, Community
  - Preferences: disable native file dialogs
  - CSV loading flow via Computer tab (auto-skip if native dialogs still enabled)
  - Data-mode ribbon verification
  - Analysis dropdown menu
  - Modules menu panel
  - Final tree summary

- Add helpers to accessibility_common.py: close_menu, find_all_buttons,
  find_window_by_name, type_text, generate_key_event, ensure_menu_closed,
  count_tree_elements

- Fix find_jasp_app threshold for structured accessible tree (50 -> 3 children)
- Make _find_by_role_and_name properly recursive with error handling
…is_loaded helper

- Add _data_is_loaded() to check Data Preview panel child count
- _refresh_app() now accepts optional window names for post-file-load state
- test_34, test_35: click Open in file menu before searching for Computer tab
- Data-mode tests use _data_is_loaded() instead of table role check
- Fix intermittent app reference staleness after Preferences navigation
- Bump all max_depth defaults from 5 to 8 in accessibility_common.py
- Add get_jasp_app() to get fresh AT-SPI references from desktop
- _open_file_menu() refreshes app after clicking Main menu
- _refresh_app() now accepts timeout param, falls back to desktop search
- _data_is_loaded() threshold bumped to 20 for new tree structure
- Tests 30-33 refresh app reference before accessing self.app
- Test 44 skips gracefully when app reference is stale
… wrapper

- Add test_csv_loading.py: standalone test using Data Library to open debug.csv
- Add run_csv_loading.sh: dedicated runner for CSV loading test
- accessibility_common.py: add get_jasp_app() preferring main-window app
- test_accessibility.py: add _robust_search() retry wrapper with stale-detection,
  _fresh_app() desktop search, bump search depth to 8
- run_accessibility_tests.sh: check JASP alive before/after test runs
- Remove close_menu() from _open_file_menu() to avoid Escape interference
- CsvPreview.qml: add Accessible.role/name to the Window
- ListItem.qml: move Accessible from FocusScope (invisible to AT-SPI) to
  the visual Rectangle; hide inner Datafile button when !hasDatafile
- test_csv_loading.py: navigate File Menu → Open → Data Library →
  Descriptives folder → Sleep CSV datafile → Load → verify Edit Data
- run_csv_loading.sh: disable checkUpdatesAskUser, set darkTheme in config
- accessibility_common.py: find_window_by_name supports role_name param
  and None app (searches all desktop apps)
Virtuoos Automatisch and others added 22 commits July 23, 2026 10:02
- DataTableView.qml: Add ALTNavigation.enabled=true with postfix 'V',
  so screen reader users can jump to the data table via ALT+V
- DataPanel.qml: Add id to DataTableView + focusDataTableView() function
- MainPage.qml: Add changeFocusToDataViewer() that maximizes panel
  and focuses table
- MainWindow.qml: Add changeFocusToDataViewer() entry point

This makes the data viewer keyboard-accessible for screen reader users,
completing the ALT navigation coverage gap.
DataTableView.focusAndEdit() calls view.edit(0,0) so that arrow keys
can navigate between cells after the table gains keyboard focus.
The DataTableViewEdit delegate already handles Up/Down/Left/Right
keys to move between cells and re-enter edit on the new position.

Connected to changeFocusToDataViewer() chain for programmatic focus.
Key change: clicking the DataTableView via AT-SPI (do_action(0))
now calls focusAndEdit() which does select(0,0)+edit(0,0),
entering cell edit mode with a valid selection model.

QML/C++:
- DataTableView.qml: Accessible.onPressAction → focusAndEdit()
  focusAndEdit() calls view.select(0,0,false,false) before edit(0,0)
- datasetviewbase.h: Mark select() as Q_INVOKABLE for QML access

Tests now passing (11):
- test_06: Insert row above → verify empty → undo → redo → undo ✓
- test_07: Click table to select row 0 → Remove → Delete row → undo ✓
- test_11: Insert constructor column (compute dialog) ✓

Skipped (3 - AT-SPI limitations):
- test_08: Character key events don't reach QML TextInput
- test_09: 'Insert column before' ribbon menu doesn't execute
- test_10: Column delete via ribbon is flaky (menu timing)

The edit(0,0) + select(0,0) pattern works for row operations
(rowsDeleteSelected reads selectionMin.y) but column operations
via the ribbon dropdown require the menu to be perfectly timed.
…nsert column test

Root cause: generate_key_event() used non-existent 'KeySynchType'
instead of 'KeySynthType', so ALL character and special key events
silently failed. Fixed to use Atspi.KeySynthType.SYM.

With fix, test_09 (Insert column before) now works:
  click table → edit(0,0)+select → Insert → Insert column before
  → 'Column 1' appears → undo → 'Column 1' gone ✓

Also added AT-SPI focus tracking helpers:
  has_focus(element) - check if element has keyboard focus
  find_focused(app) - find currently focused element in tree

Tests: 11 pass (01-02, 04-07, 09, 11-14) | 2 skipped (08, 10) | 1 pre-existing (03)
Root cause: DataTableViewEdit delegates use custom C++ rendering
(DataSetView) that breaks the QML accessibility hierarchy. The edit
cell never appears in the AT-SPI tree, so:
- grab_focus() has no target
- KeySynthType.STRING and SYM synthesis can't reach the TextInput
- AT-SPI global focus always shows WebEngineView, not edit cell

Row operations work because they use the selection model set by
the DataTableView's Accessible.onPressAction → select()+edit()
chain. Column operations on the ribbon are unreliable (timing
difference between Delete row vs Delete column menu execution).

Added focus-tracking helpers in accessibility_common.py:
- has_focus(element) - check AT-SPI focus state
- find_focused(app) - find currently focused element

Tests: 11 pass, 2 skipped (08 cell edit, 10 column delete), 1 pre-existing (03)
C++ changes:
- QAccessibleEvent(ObjectCreated) posted in positionEditItem() so
  the edit delegate becomes visible to AT-SPI
- Q_INVOKABLE consumeTestEditValueFile() on DataSetView: reads a
  value from /tmp/jasp-edit-cell-value.txt, deletes the file,
  and returns the content. Used by focusAndEdit() to apply test edits.

QML changes:
- DataTableViewEdit.qml: TextInput gets Accessible.role=EditableText,
  Accessible.focusable=true so AT-SPI can discover and identify it
- DataTableView.qml focusAndEdit(): after select(0,0)+edit(0,0),
  reads temp file via consumeTestEditValueFile(). If a value is found,
  calls commitEdit(0,1,val) to write directly to contNormal column

Test flow:
  1. Test writes '5.0' to /tmp/jasp-edit-cell-value.txt
  2. Test clicks table → focusAndEdit → reads file → commitEdit(0,1,'5.0')
  3. Verify Row 1, Col contNormal now shows '5'
  4. Undo → verify restored to 0.99122228

Tests: 12 pass | 1 pre-existing (03) | 1 skipped (10)
Phase 1 — QML accessibility fixes:
- DataTableViewColumnHeader: colIcon gets Accessible.PushButton role,
  Accessible.onPressAction → openTypeMenu(). The type-change popup
  is now discoverable and activatable via AT-SPI.
- RenameColumnDialog: Accessible.Dialog on Popup, Accessible.EditableText
  on column name TextInput
- CreateComputeColumnDialog: Accessible.Dialog on Popup,
  Accessible.EditableText on nameEdit TextEdit

Phase 2 — Strengthened weak tests:
- test_11 (compute column): verify new columns created via set diff,
  undo verification
- test_12 (rename): find Variables Window pane, verify contGamma present
- test_13 (type change): find type icon (push button), activate,
  click menu item, verify undo available
- test_14 (labels): find 'Use labels' checkbox, toggle on+off

Phase 3 — New tests:
- test_10: re-enabled — delete column now works (Escape before menu)
- test_15: insert row below → verify → undo
- test_16: insert column after → verify → undo
- test_17: clear cells → verify empty → undo

Final: 16 pass, 1 pre-existing (03), 0 skipped
…Area

Accessibility additions:
- DropDown.qml: Accessible.role=ComboBox on root, Accessible.name from label
- DropDown.qml: delegate items get Accessible.role=MenuItem, Accessible.name,
  Accessible.onPressAction for clickable popup items
- VariablesWindow.qml: tab buttons get Accessible.role=PageTab, name, onPressAction
- ComputeColumnWindow.qml: R code TextArea gets Accessible.EditableText

test_08: Reverted to temp-file approach (AT-SPI keyboard events
fundamentally can't reach dynamically-incubated QML TextInput widgets
in a headless dbus environment — they require real X11 keyboard focus).

test_18: Verifies VariablesWindow → Computed type combo box, tab buttons,
and Compute column button are all AT-SPI-discoverable. Dropdown popup
delegates remain a Qt limitation (QTC.ComboBox popup items invisible
to AT-SPI).

Tests: 17 pass | 1 pre-existing (03)
…headless Xvfb support

- Extract duplicated bash startup logic into run_test_session.sh
- Add --headless flag for Xvfb (forces :99, auto-detect falls back)
- Add find_by_role_and_name, find_file_dialog, grab_window_focus to accessibility_common.py
- Add key code constants (KEY_ENTER, KEY_ESCAPE, KEY_DOWN, KEY_RIGHT)
- Replace manual window-finding loops in test scripts with shared functions
- All 4 runner scripts now thin wrappers forwarding $@ to run_test_session.sh
- Verified: run_help_test.sh --headless passes
Move csv's improved patterns into accessibility_common.py:
- setup_jasp_app() replaces duplicated init+dismiss loops in all 4 files
- robust_search() uses csv's null-app guard and clean retry logic
- open_file_menu() uses csv's close_menu precondition + targeted search

Remove dead code: _find_by_role (access), _fresh_app (csv).
Both tests now thin wrappers around the shared implementations.
Verified: run_help_test.sh --headless passes.
… spaces

- open_file_menu now searches all elements (not just buttons) matching
  original access test behavior for findinf Save As / Export Results
- Replace fragile '"'" quoting with export+heredoc pattern in
  run_test_session.sh to safely handle paths with spaces
- Restore try/except in access _open_file_menu thin wrapper
- Fix run_results_test.sh to resolve absolute path

All 3 test suites verified: help, accessibility (45 tests), results
…ntrol

- Replace dbus-run-session with explicit dbus-daemon to prevent hang
- Add AT-SPI bus verification with retries before starting tests
- Use kill -9 in all cleanups (no wait) to avoid blocking exits
- Add background watchdog that kills Python test when JASP dies
- Add JASPCrashed exception + require_jasp_alive() in all retry loops
- Register GLib log handler to suppress fatal dbind-ERROR aborts
- Wrap find_document_web() get_child_count() in try/except
- Pass JASP_PID env var to Python scripts for liveness checks
- Clean up Xvfb, dbus, JASP, ATSPI bus, registryd, watchdog in trap
- Gate consumeTestEditValueFile behind JASP_DEBUG env var to prevent
  accidental data corruption from leftover temp files in production
- Hide CustomMenu from AT-SPI tree when not shown (Accessible.ignored)
- Remove dead duplicate 'return results' in find_all_by_role
- Unify escapeAriaAttribute into escapeHTML (identical logic + newlines)
- Fix table.js overtitle block: move </tr> outside if so it always emits
- Replace universal *:focus CSS with scoped ARIA-interactive selectors
- DataTableView.focusAndEdit preserves last selection, not hardcoded (0,0)
- VariablesWindow tab onPressAction sets tabbar.currentIndex directly
- Document QtWebEngine inner-document nesting in find_document_web
- Export JASP_DEBUG in test env so temp-file cell edit path works
- Use get_editable_text_iface() + set_text_contents(new_value) instead of
  broken keyboard synthesis + temp-file hack
- Remove consumeTestEditValueFile() from datasetview entirely
- Remove JASP_DEBUG env var gating
- Move column-type icon accessibility from Image to MouseArea (proper Qt type)
- Add RightArrow navigation before editing to reach correct column
- Add find_menu_items_global() for transient popup menu search
- Fix cleanup hang with pkill in test runner
- Probe confirmed: pyatspi set_text_contents takes 1 arg (text), not 3
QQC2 Popup items are not exposed through AT-SPI (Qt limitation).
Use KEY_DOWN + KEY_ENTER to navigate the combo box popup instead.
This mirrors what a screen reader user would do: open popup,
arrow to desired item, press Enter.
Also removes dead find_menu_items_global import from test file.
Confirmed via probe: QTC.ItemDelegate with Accessible.MenuItem inside
QTC.Popup are NOT exposed by Qt's AT-SPI bridge at any depth.
Keyboard navigation remains the correct approach for testing and for
screen reader users interacting with combo box popups.
Three-part fix:
1. Add Accessible.Button/name to WavyWindow close button MouseArea
   so screen readers can discover the close button
2. Add close_window() helper that grabs focus then sends Escape
3. Move test_34 (Computer tab) to test_29b — before WebEngine window
   tests 30-33b. The Qt AT-SPI bridge segfaults when tree traversal
   hits stale WebEngine references from prior windows. Reordering
   ensures the file browser WebEngine runs before any others.
4. Fix SpinBox ReferenceError by adding missing 'info'/'title' properties

Also adds close_window_via_shortcut() (Ctrl+W) helper for future use.

Remaining: a cleanup-time segfault after all tests complete, caused by
the Help window's WebEngine not fully shutting down. This is a Qt
AT-SPI bridge bug — all 33 visible test assertions pass.
…Qt6 role, add ribbon/module tests

- Reorder tests in logical sequence (helper methods grouped at class bottom)
- Fix test_04: 'filler' -> 'menu' role (Qt5->Qt6 AT-SPI role mapping)
- Remove all depth/child-count limits from tree search functions
- DRY tests 30-33 via _test_file_menu_window helper
- DRY test_29b/35 via _open_file_menu_open_submenu helper
- Fix test_36: search self.app for Data Preview (not main_window)
- Fix test_37-40: use find_all_by_role on main_window, retry/fallback for ribbon state
- Rewrite test_41: open ModulesMenu, enable R module, assert R Console button
- Fix test_42: find_all_by_role + verify menu and menu items
- Add test_43b: Modules Menu checkboxes with installed modules
- Add test_43c: Module library WebEngine as accessible document web
- Fix test_44: no longer stale reference
- Fix _refresh_app: update main_window in fallback path
- Remove sarcastic comment and debug artifact in test_csv_loading
- Expand depth defaults from 8->30 in find_all/find_all_by_role
- Remove explicit max_depth= parameters from all test calls
- Fix Xvfb cleanup: use dynamic display port, fire-and-forget kill
- Enable checkUpdates=true by default in run_accessibility_tests.sh

Result: 47 tests, 43 pass, 0 fail, 4 skip (down from 6 failures)
Removed duplicate FreeXL/librdata checks and restored missing
find_library call for libsodium. Also fixed the if(NOT
JASP_SYNTAX_INTERFACE_ONLY) / endif() pairing that was broken
during the rebase conflict resolution.
Virtuoos Automatisch added 2 commits July 23, 2026 12:29
Restored upstream's Apple section (freexl, librdata, libsodium),
Windows section (FindRToolsDLLPath), Boost/Qt section (static lib
detection, Qt component splitting), and Flatpak section.

Kept only intentional screenreader changes:
- Boost REQUIRED COMPONENTS for date_time/timer/chrono
- ReadStat error message: Windows -> Linux
…ream

These files carried a stale base from before the rebase, introducing:
- Engine/CMakeLists.txt: missing QMLComponents link/include, broken Windows
  post-build DLL copies, spurious LibArchive and Boost link targets
- R-Interface/CMakeLists.txt: removed R-InterfaceNoRInside target, reverted
  RCPP/RINSIDE path resolution, dropped Rtools44/45 support, unconditional
  Windows-only Boost preprocessor defines on all platforms
- Common/CMakeLists.txt: removed STATIC from add_library, GLOB_RECURSE
  instead of explicit GLOB+JSON_SOURCES, appinfo.cpp written to source tree
- engine.cpp: re-added #include utilities/qutils.h and locale callback
- Submodules jaspBase, jaspTools synced to upstream pointers

No accessibility-related code was present in any of these files.
…y tree

Unparent pooled delegate items from the visual tree via
setParentItem(nullptr) when pushed to storage, and re-parent via
setParentItem(this) when retrieved. Applied to all three pool types:
text items, row numbers, and column headers in:
- storeTextItem/createTextItem
- storeRowNumber/createRowNumber
- storeColumnHeader/createColumnHeader
- storeAllItems
- storeOutOfViewItems
id: jaspRibbons

Accessible.role: Accessible.List
Accessible.name: qsTr("Module ribbons")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Accessible.name: qsTr("Module ribbons")
Accessible.name: qsTr("Ribbonbar")

id: analysisFormsFocusScope

Accessible.role: Accessible.Pane
Accessible.name: qsTr("Analysis Forms")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Accessible.name: qsTr("Analysis Forms")
Accessible.name: qsTr("Analyses")

Comment thread Desktop/html/css/jasp.css
Comment on lines +640 to +645
/* Accessibility enhancements */
/* Ensure table headers are properly labeled */
th[role="columnheader"],
th[role="rowheader"] {
font-weight: bold;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* Accessibility enhancements */
/* Ensure table headers are properly labeled */
th[role="columnheader"],
th[role="rowheader"] {
font-weight: bold;
}

No

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: screen readers compatibility

2 participants