Compare commits
10 Commits
main
...
accessibil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d5d9a3f1d | ||
| 3bce527500 | |||
| 3c631d29d4 | |||
| 5e35667794 | |||
| aa17b8b340 | |||
| fb88f2a9dc | |||
| 2371c8d694 | |||
| 623b974e56 | |||
| 6882b9a643 | |||
| c4e6ef142b |
@@ -1286,6 +1286,7 @@ Emacs and macOS / GNUstep
|
|||||||
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
|
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
|
||||||
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
|
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
|
||||||
* Mac / GNUstep Events:: How window system events are handled.
|
* Mac / GNUstep Events:: How window system events are handled.
|
||||||
|
* Accessibility:: Screen reader and Zoom support on macOS.
|
||||||
* GNUstep Support:: Details on status of GNUstep support.
|
* GNUstep Support:: Details on status of GNUstep support.
|
||||||
|
|
||||||
Emacs and Haiku
|
Emacs and Haiku
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Support}), but we hope to improve it in the future.
|
|||||||
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
|
* Mac / GNUstep Basics:: Basic Emacs usage under GNUstep or macOS.
|
||||||
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
|
* Mac / GNUstep Customization:: Customizations under GNUstep or macOS.
|
||||||
* Mac / GNUstep Events:: How window system events are handled.
|
* Mac / GNUstep Events:: How window system events are handled.
|
||||||
|
* Accessibility:: Screen reader and Zoom support on macOS.
|
||||||
* GNUstep Support:: Details on status of GNUstep support.
|
* GNUstep Support:: Details on status of GNUstep support.
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
@@ -272,6 +273,107 @@ and return the result as a string. You can also use the Lisp function
|
|||||||
services and receive the results back. Note that you may need to
|
services and receive the results back. Note that you may need to
|
||||||
restart Emacs to access newly-available services.
|
restart Emacs to access newly-available services.
|
||||||
|
|
||||||
|
@node Accessibility
|
||||||
|
@section Accessibility (macOS)
|
||||||
|
@cindex VoiceOver
|
||||||
|
@cindex accessibility (macOS)
|
||||||
|
@cindex screen reader (macOS)
|
||||||
|
@cindex Zoom, cursor tracking (macOS)
|
||||||
|
@cindex accessibility, Zoom (macOS)
|
||||||
|
@cindex echo area, accessibility (macOS)
|
||||||
|
@cindex completion announcements, accessibility (macOS)
|
||||||
|
|
||||||
|
When built with the Cocoa interface on macOS, Emacs exposes buffer
|
||||||
|
content, cursor position, mode lines, and interactive elements to the
|
||||||
|
macOS accessibility subsystem. This enables use with VoiceOver,
|
||||||
|
Apple's built-in screen reader, and with other assistive technology
|
||||||
|
such as macOS Zoom.
|
||||||
|
|
||||||
|
Toggle VoiceOver with @kbd{Cmd-F5} (or via System Settings,
|
||||||
|
Accessibility, VoiceOver). When Emacs is focused, VoiceOver announces
|
||||||
|
the buffer name and current line. Standard Emacs navigation produces
|
||||||
|
speech feedback:
|
||||||
|
|
||||||
|
@itemize @bullet
|
||||||
|
@item
|
||||||
|
Arrow keys read individual characters (left/right) or full lines
|
||||||
|
(up/down).
|
||||||
|
@item
|
||||||
|
@kbd{M-f} and @kbd{M-b} announce words.
|
||||||
|
@item
|
||||||
|
@kbd{C-n} and @kbd{C-p} read the destination line.
|
||||||
|
@item
|
||||||
|
Shift-modified movement announces selected or deselected text.
|
||||||
|
@item
|
||||||
|
@key{TAB} and @kbd{S-@key{TAB}} navigate interactive elements
|
||||||
|
(buttons, completion candidates) within a buffer.
|
||||||
|
@end itemize
|
||||||
|
|
||||||
|
The @file{*Completions*} buffer announces each completion candidate
|
||||||
|
as you navigate, even while keyboard focus remains in the minibuffer.
|
||||||
|
|
||||||
|
Echo area messages are announced automatically. When a background
|
||||||
|
operation completes and displays a message (e.g., @samp{Git finished},
|
||||||
|
@samp{Wrote file}), VoiceOver reads it without requiring any action.
|
||||||
|
Messages are suppressed while the minibuffer is active (i.e., while
|
||||||
|
you are typing a command) to avoid interrupting prompt reading.
|
||||||
|
During keyboard navigation, echo area messages are temporarily
|
||||||
|
suppressed (for 0.5 seconds) to prevent them from interrupting the
|
||||||
|
cursor-position announcement.
|
||||||
|
|
||||||
|
VoiceOver's rotor browse cursor stays synchronized with the Emacs
|
||||||
|
cursor after large programmatic jumps (for example, heading navigation
|
||||||
|
in Org mode, @code{xref-find-definitions}, or @code{imenu}).
|
||||||
|
|
||||||
|
@subheading Zoom Cursor Tracking
|
||||||
|
|
||||||
|
When macOS Zoom is enabled (System Settings, Accessibility, Zoom,
|
||||||
|
with @samp{Follow keyboard focus} active), Emacs reports its cursor
|
||||||
|
position to the Zoom subsystem after every redisplay cycle. The
|
||||||
|
zoomed viewport automatically tracks the Emacs insertion point as you
|
||||||
|
type or navigate. During completion, Zoom follows the selected
|
||||||
|
candidate rather than the text cursor. This integration is always
|
||||||
|
active when Zoom is enabled; no Emacs-side toggle is needed.
|
||||||
|
|
||||||
|
@vindex ns-accessibility-enabled
|
||||||
|
@cindex disabling accessibility (macOS)
|
||||||
|
To disable the VoiceOver accessibility interface (e.g., to
|
||||||
|
eliminate overhead on systems where a screen reader is not in use),
|
||||||
|
set @code{ns-accessibility-enabled} to @code{nil}. The default value
|
||||||
|
is @code{nil}; Emacs sets it to @code{t} automatically at startup if
|
||||||
|
it detects an active assistive technology. Zoom cursor tracking
|
||||||
|
operates independently and is not affected by this variable.
|
||||||
|
|
||||||
|
@subheading Known Limitations
|
||||||
|
|
||||||
|
@itemize @bullet
|
||||||
|
@item
|
||||||
|
Very large buffers (tens of megabytes) may cause slow initial
|
||||||
|
accessibility text extraction. Once cached, subsequent queries
|
||||||
|
are fast.
|
||||||
|
@item
|
||||||
|
Mode-line text extraction handles only character glyphs. Mode lines
|
||||||
|
using icon fonts (e.g., icon-based mode lines)
|
||||||
|
produce incomplete accessibility text.
|
||||||
|
@item
|
||||||
|
Window configuration changes (splits, deletions, new buffers) trigger
|
||||||
|
a full rebuild of the accessibility virtual element tree, which may
|
||||||
|
cause a brief pause in VoiceOver announcements.
|
||||||
|
@item
|
||||||
|
Right-to-left (bidi) text is exposed correctly as buffer content,
|
||||||
|
but hit-testing of screen positions to character ranges assumes
|
||||||
|
left-to-right layout.
|
||||||
|
@item
|
||||||
|
Completion popup tracking (for Zoom and VoiceOver) is limited to
|
||||||
|
buffers smaller than 10,000 characters. Larger buffers are assumed
|
||||||
|
not to be completion popups and are skipped to avoid excessive work
|
||||||
|
per redisplay cycle.
|
||||||
|
@end itemize
|
||||||
|
|
||||||
|
This support is available only on the Cocoa build. GNUstep has a
|
||||||
|
different accessibility model and is not yet supported.
|
||||||
|
|
||||||
|
|
||||||
@node GNUstep Support
|
@node GNUstep Support
|
||||||
@section GNUstep Support
|
@section GNUstep Support
|
||||||
|
|
||||||
|
|||||||
26
etc/NEWS
26
etc/NEWS
@@ -4544,6 +4544,24 @@ accessibility features. Enable Emacs via (System Settings, Privacy &
|
|||||||
Security, Accessibility...) and add the Emacs.app installed directory to
|
Security, Accessibility...) and add the Emacs.app installed directory to
|
||||||
the enabled application list.
|
the enabled application list.
|
||||||
|
|
||||||
|
+++
|
||||||
|
** VoiceOver accessibility support on macOS.
|
||||||
|
Emacs now exposes buffer content, cursor position, and interactive
|
||||||
|
elements to the macOS accessibility subsystem (VoiceOver). Standard
|
||||||
|
navigation keys produce speech feedback: arrow keys read characters
|
||||||
|
and lines, 'M-f'/'M-b' announce words, and shift-modified movement
|
||||||
|
reports selected text. Echo area messages (e.g., "Wrote file",
|
||||||
|
"Compilation finished") are announced automatically without user
|
||||||
|
interaction. The VoiceOver rotor cursor stays synchronized after
|
||||||
|
large programmatic jumps such as xref, imenu, or Org heading
|
||||||
|
navigation. Pressing 'TAB' navigates interactive spans (buttons,
|
||||||
|
completion candidates) within a buffer. Completion
|
||||||
|
frameworks that render via overlays or child frames announce the
|
||||||
|
selected candidate.
|
||||||
|
|
||||||
|
Set 'ns-accessibility-enabled' to nil to disable the accessibility
|
||||||
|
interface and eliminate the associated overhead.
|
||||||
|
|
||||||
---
|
---
|
||||||
** Process execution has been optimized on Android.
|
** Process execution has been optimized on Android.
|
||||||
The run-time performance of subprocesses on recent Android releases,
|
The run-time performance of subprocesses on recent Android releases,
|
||||||
@@ -4679,11 +4697,3 @@ GNU General Public License for more details.
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
Local variables:
|
|
||||||
coding: utf-8
|
|
||||||
mode: outline
|
|
||||||
mode: emacs-news
|
|
||||||
paragraph-separate: "[ ]"
|
|
||||||
end:
|
|
||||||
|
|||||||
177
src/nsterm.h
177
src/nsterm.h
@@ -453,6 +453,143 @@ enum ns_return_frame_mode
|
|||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
|
||||||
|
Accessibility virtual elements (macOS / Cocoa only)
|
||||||
|
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
#ifdef NS_IMPL_COCOA
|
||||||
|
@class EmacsView;
|
||||||
|
|
||||||
|
/* Base class for virtual accessibility elements attached to EmacsView. */
|
||||||
|
@interface EmacsAccessibilityElement : NSAccessibilityElement
|
||||||
|
@property (nonatomic, unsafe_unretained) EmacsView *emacsView;
|
||||||
|
/* Lisp window object --- safe across GC cycles.
|
||||||
|
GC safety: these Lisp_Objects are NOT visible to GC via staticpro
|
||||||
|
or the specpdl stack. This is safe because:
|
||||||
|
(1) Emacs GC runs only on the main thread, at well-defined safe
|
||||||
|
points during Lisp evaluation --- never during redisplay.
|
||||||
|
(2) Accessibility elements are owned by EmacsView which belongs to
|
||||||
|
an active frame; windows referenced here are always reachable
|
||||||
|
from the frame's window tree until rebuildAccessibilityTree
|
||||||
|
updates them during the next redisplay cycle.
|
||||||
|
(3) AX getters dispatch_sync to main before accessing Lisp state,
|
||||||
|
so GC cannot run concurrently with any access to lispWindow.
|
||||||
|
(4) validWindow checks WINDOW_LIVE_P before dereferencing.
|
||||||
|
Note: this relies on non-moving GC. If Emacs adopts a moving
|
||||||
|
collector (MPS), these fields must be registered as GC roots. */
|
||||||
|
@property (nonatomic, assign) Lisp_Object lispWindow;
|
||||||
|
- (struct window *)validWindow; /* Returns live window or NULL. */
|
||||||
|
- (NSRect)screenRectFromEmacsX:(int)x y:(int)y width:(int)w height:(int)h;
|
||||||
|
@end
|
||||||
|
|
||||||
|
/* A visible run: maps a contiguous range of accessibility indices
|
||||||
|
to a contiguous range of buffer character positions. Invisible
|
||||||
|
text is skipped, so ax_start values are consecutive across runs
|
||||||
|
while charpos values may have gaps. */
|
||||||
|
typedef struct ns_ax_visible_run
|
||||||
|
{
|
||||||
|
ptrdiff_t charpos; /* Buffer charpos where this visible run starts. */
|
||||||
|
ptrdiff_t length; /* Number of visible Emacs characters in this run. */
|
||||||
|
NSUInteger ax_start; /* Starting index in the accessibility string. */
|
||||||
|
NSUInteger ax_length; /* Length in accessibility string (UTF-16 units). */
|
||||||
|
} ns_ax_visible_run;
|
||||||
|
|
||||||
|
/* Virtual AXTextArea element --- one per visible Emacs window (buffer). */
|
||||||
|
@interface EmacsAccessibilityBuffer
|
||||||
|
: EmacsAccessibilityElement <NSAccessibility>
|
||||||
|
{
|
||||||
|
ns_ax_visible_run *visibleRuns;
|
||||||
|
NSUInteger visibleRunCount;
|
||||||
|
NSUInteger *lineStartOffsets; /* AX index for each line. */
|
||||||
|
NSUInteger lineCount; /* Entries in lineStartOffsets. */
|
||||||
|
NSMutableArray *cachedInteractiveSpans;
|
||||||
|
BOOL interactiveSpansDirty;
|
||||||
|
/* Deferred overlay visibility detection. The overlay notification
|
||||||
|
branch invalidates the text cache and records the old visible text
|
||||||
|
length here; ensureTextCache posts ValueChanged after rebuild if
|
||||||
|
the length changed. This avoids running Lisp (ns_ax_buffer_text)
|
||||||
|
synchronously in the notification dispatch path, which interferes
|
||||||
|
with redisplay state. */
|
||||||
|
BOOL pendingOverlayCheck;
|
||||||
|
NSUInteger pendingOverlayTextLen;
|
||||||
|
}
|
||||||
|
@property (nonatomic, copy) NSString *cachedText;
|
||||||
|
@property (nonatomic, assign) modiff_count cachedTextModiff;
|
||||||
|
@property (nonatomic, assign) modiff_count cachedOverlayModiff;
|
||||||
|
@property (nonatomic, assign) ptrdiff_t cachedTextStart;
|
||||||
|
@property (nonatomic, assign) modiff_count cachedModiff;
|
||||||
|
@property (nonatomic, assign) modiff_count cachedCharsModiff;
|
||||||
|
@property (nonatomic, assign) ptrdiff_t cachedPoint;
|
||||||
|
@property (nonatomic, assign) BOOL cachedMarkActive;
|
||||||
|
@property (nonatomic, assign) struct buffer *cachedBuffer;
|
||||||
|
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
|
||||||
|
@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayStart;
|
||||||
|
@property (nonatomic, assign) ptrdiff_t cachedCompletionOverlayEnd;
|
||||||
|
@property (nonatomic, assign) ptrdiff_t cachedCompletionPoint;
|
||||||
|
@property (nonatomic, assign) BOOL pendingEditNotification;
|
||||||
|
- (void)invalidateTextCache;
|
||||||
|
- (NSInteger)lineForAXIndex:(NSUInteger)idx;
|
||||||
|
- (NSRange)rangeForLine:(NSUInteger)line textLength:(NSUInteger)tlen;
|
||||||
|
- (ptrdiff_t)charposForAccessibilityIndex:(NSUInteger)ax_idx;
|
||||||
|
- (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface EmacsAccessibilityBuffer (Notifications)
|
||||||
|
- (void)postTextChangedNotification:(ptrdiff_t)point;
|
||||||
|
- (void)postFocusedCursorNotification:(ptrdiff_t)point
|
||||||
|
direction:(NSInteger)direction
|
||||||
|
granularity:(NSInteger)granularity
|
||||||
|
markActive:(BOOL)markActive
|
||||||
|
oldMarkActive:(BOOL)oldMarkActive;
|
||||||
|
- (void)postCompletionAnnouncementForBuffer:(struct buffer *)b
|
||||||
|
point:(ptrdiff_t)point;
|
||||||
|
- (void)postAccessibilityNotificationsForFrame:(struct frame *)f;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface EmacsAccessibilityBuffer (InteractiveSpans)
|
||||||
|
- (void)invalidateInteractiveSpans;
|
||||||
|
@end
|
||||||
|
|
||||||
|
/* Virtual AXStaticText element --- one per mode line. */
|
||||||
|
@interface EmacsAccessibilityModeLine : EmacsAccessibilityElement
|
||||||
|
@end
|
||||||
|
|
||||||
|
/* Span types for interactive AX child elements.
|
||||||
|
Used to distinguish span types during scanning; all map to AXButton role at present. */
|
||||||
|
typedef NS_ENUM(NSInteger, EmacsAXSpanType)
|
||||||
|
{
|
||||||
|
EmacsAXSpanTypeNone = -1,
|
||||||
|
EmacsAXSpanTypeButton = 0,
|
||||||
|
EmacsAXSpanTypeCompletionItem = 1,
|
||||||
|
EmacsAXSpanTypeWidget = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* A lightweight AX element representing one interactive text span
|
||||||
|
(button, link, checkbox, completion candidate, etc.) within a buffer
|
||||||
|
window. Exposed as AX child of EmacsAccessibilityBuffer so VoiceOver
|
||||||
|
Tab navigation can reach individual interactive elements. */
|
||||||
|
@interface EmacsAccessibilityInteractiveSpan : EmacsAccessibilityElement
|
||||||
|
|
||||||
|
@property (nonatomic, assign) ptrdiff_t charposStart;
|
||||||
|
@property (nonatomic, assign) ptrdiff_t charposEnd;
|
||||||
|
@property (nonatomic, assign) EmacsAXSpanType spanType;
|
||||||
|
@property (nonatomic, copy) NSString *spanLabel;
|
||||||
|
@property (nonatomic, unsafe_unretained)
|
||||||
|
EmacsAccessibilityBuffer *parentBuffer;
|
||||||
|
|
||||||
|
- (NSAccessibilityRole) accessibilityRole;
|
||||||
|
- (NSString *) accessibilityLabel;
|
||||||
|
- (NSRect) accessibilityFrame;
|
||||||
|
- (BOOL) isAccessibilityElement;
|
||||||
|
- (BOOL) isAccessibilityFocused;
|
||||||
|
- (void) setAccessibilityFocused: (BOOL) focused;
|
||||||
|
|
||||||
|
@end
|
||||||
|
#endif /* NS_IMPL_COCOA */
|
||||||
|
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
||||||
The main Emacs view
|
The main Emacs view
|
||||||
@@ -471,6 +608,37 @@ enum ns_return_frame_mode
|
|||||||
#ifdef NS_IMPL_COCOA
|
#ifdef NS_IMPL_COCOA
|
||||||
char *old_title;
|
char *old_title;
|
||||||
BOOL maximizing_resize;
|
BOOL maximizing_resize;
|
||||||
|
NSMutableArray *accessibilityElements;
|
||||||
|
/* See GC safety comment on EmacsAccessibilityElement.lispWindow. */
|
||||||
|
Lisp_Object lastSelectedWindow;
|
||||||
|
Lisp_Object lastRootWindow;
|
||||||
|
BOOL accessibilityTreeValid;
|
||||||
|
int lastLeafCount;
|
||||||
|
@public /* Accessed by ns_ax_post_updates_error (C function). */
|
||||||
|
BOOL accessibilityUpdating;
|
||||||
|
BOOL childFrameCompletionActive;
|
||||||
|
char *childFrameLastCandidate;
|
||||||
|
char *childFrameLastBufferName;
|
||||||
|
modiff_count childFrameLastModiff;
|
||||||
|
/* Last BUF_CHARS_MODIFF seen for echo_area_buffer[0]. Used by
|
||||||
|
postEchoAreaAnnouncementIfNeeded to detect new echo area messages
|
||||||
|
independently of the per-element notification cycle. */
|
||||||
|
modiff_count lastEchoCharsModiff;
|
||||||
|
/* Last known point of the selected window's buffer. Used to detect
|
||||||
|
cursor movement and suppress echo area announcements during
|
||||||
|
navigation (which would interrupt line reading). */
|
||||||
|
ptrdiff_t lastSelectedBufferPoint;
|
||||||
|
/* Timestamp of the most recent cursor movement. Echo area
|
||||||
|
announcements are suppressed for NS_AX_ECHO_SUPPRESS_SECONDS
|
||||||
|
after the last movement so that asynchronous updates (eldoc,
|
||||||
|
which-function-mode) arriving in subsequent redisplay cycles
|
||||||
|
do not interrupt VoiceOver line reading. */
|
||||||
|
NSTimeInterval lastCursorMoveTimestamp;
|
||||||
|
/* Timestamp of the most recent text edit (typing). Child frame
|
||||||
|
completion announcements are suppressed for a brief window after
|
||||||
|
text edits to prevent the completion candidate announcement from
|
||||||
|
interrupting VoiceOver character echo. */
|
||||||
|
NSTimeInterval lastTextEditTimestamp;
|
||||||
#endif
|
#endif
|
||||||
BOOL font_panel_active;
|
BOOL font_panel_active;
|
||||||
NSFont *font_panel_result;
|
NSFont *font_panel_result;
|
||||||
@@ -528,6 +696,15 @@ enum ns_return_frame_mode
|
|||||||
- (void)windowWillExitFullScreen;
|
- (void)windowWillExitFullScreen;
|
||||||
- (void)windowDidExitFullScreen;
|
- (void)windowDidExitFullScreen;
|
||||||
- (void)windowDidBecomeKey;
|
- (void)windowDidBecomeKey;
|
||||||
|
|
||||||
|
#ifdef NS_IMPL_COCOA
|
||||||
|
/* Accessibility support. */
|
||||||
|
- (void)rebuildAccessibilityTree;
|
||||||
|
- (void)invalidateAccessibilityTree;
|
||||||
|
- (void)postAccessibilityUpdates;
|
||||||
|
- (void)postEchoAreaAnnouncementIfNeeded;
|
||||||
|
- (void)announceChildFrameCompletion;
|
||||||
|
#endif
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4834
src/nsterm.m
4834
src/nsterm.m
File diff suppressed because it is too large
Load Diff
257
test/src/nsterm-accessibility-tests.el
Normal file
257
test/src/nsterm-accessibility-tests.el
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
;;; nsterm-accessibility-tests.el --- ERT tests for NS accessibility -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2026 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
;; Author: Martin Sukany <martin@sukany.cz>
|
||||||
|
;; Keywords: accessibility, tests
|
||||||
|
|
||||||
|
;; This file is part of GNU Emacs.
|
||||||
|
|
||||||
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||||||
|
;; it under the terms of the GNU General Public License as published by
|
||||||
|
;; the Free Software Foundation, either version 3 of the License, or
|
||||||
|
;; (at your option) any later version.
|
||||||
|
|
||||||
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
||||||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
;; GNU General Public License for more details.
|
||||||
|
|
||||||
|
;; You should have received a copy of the GNU General Public License
|
||||||
|
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Tests for the NS (macOS Cocoa) accessibility interface.
|
||||||
|
;;
|
||||||
|
;; These tests verify:
|
||||||
|
;; - The `ns-accessibility-enabled' variable exists and is configurable
|
||||||
|
;; - DEFSYM symbols used by the accessibility code are correctly interned
|
||||||
|
;; - The accessibility code does not break standard Emacs operations
|
||||||
|
;;
|
||||||
|
;; Running:
|
||||||
|
;; make -C test TESTS=src/nsterm-accessibility-tests
|
||||||
|
;; Or from a running Emacs:
|
||||||
|
;; M-x ert RET t RET
|
||||||
|
;;
|
||||||
|
;; Note: GUI-dependent tests (VoiceOver interaction, Zoom tracking)
|
||||||
|
;; cannot be automated and must be tested manually. See the
|
||||||
|
;; ``Accessibility'' section in the macOS appendix of the Emacs manual.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
|
||||||
|
;; These tests only make sense on NS (Cocoa) builds.
|
||||||
|
(defvar ns-accessibility-tests-skip
|
||||||
|
(not (eq window-system 'ns))
|
||||||
|
"Non-nil means skip NS-specific tests (not an NS build).")
|
||||||
|
|
||||||
|
;;; --- Variable existence and type tests ---
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-enabled-exists ()
|
||||||
|
"Test that `ns-accessibility-enabled' is bound."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(should (boundp 'ns-accessibility-enabled)))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-enabled-is-boolean ()
|
||||||
|
"Test that `ns-accessibility-enabled' holds a boolean value."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(should (booleanp ns-accessibility-enabled)))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-enabled-settable ()
|
||||||
|
"Test that `ns-accessibility-enabled' can be toggled."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(let ((original ns-accessibility-enabled))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(setq ns-accessibility-enabled t)
|
||||||
|
(should (eq ns-accessibility-enabled t))
|
||||||
|
(setq ns-accessibility-enabled nil)
|
||||||
|
(should (eq ns-accessibility-enabled nil)))
|
||||||
|
(setq ns-accessibility-enabled original))))
|
||||||
|
|
||||||
|
;;; --- DEFSYM symbol tests ---
|
||||||
|
;; Verify that the accessibility DEFSYM symbols are correctly interned.
|
||||||
|
;; These symbols are used for text property scanning in interactive
|
||||||
|
;; span detection and completion candidate extraction.
|
||||||
|
;; These symbols are always present in standard Emacs (from widget.el,
|
||||||
|
;; button.el, etc.). The tests verify that DEFSYM did not use
|
||||||
|
;; namespaced names like 'ns-ax-widget' which would be wrong.
|
||||||
|
|
||||||
|
(ert-deftest ns-ax-defsym-widget ()
|
||||||
|
"Test that the `widget' symbol used by span scanning is interned."
|
||||||
|
(should (intern-soft "widget")))
|
||||||
|
|
||||||
|
(ert-deftest ns-ax-defsym-button ()
|
||||||
|
"Test that the `button' symbol used by span scanning is interned."
|
||||||
|
(should (intern-soft "button")))
|
||||||
|
|
||||||
|
(ert-deftest ns-ax-defsym-completion ()
|
||||||
|
"Test that the `completion' symbol is interned."
|
||||||
|
(should (intern-soft "completion")))
|
||||||
|
|
||||||
|
(ert-deftest ns-ax-defsym-completion-list-mode ()
|
||||||
|
"Test that `completion-list-mode' symbol is interned."
|
||||||
|
(should (intern-soft "completion-list-mode")))
|
||||||
|
|
||||||
|
(ert-deftest ns-ax-defsym-backtab ()
|
||||||
|
"Test that `backtab' symbol is interned."
|
||||||
|
(should (intern-soft "backtab")))
|
||||||
|
|
||||||
|
;;; --- Buffer operation safety tests ---
|
||||||
|
;; Ensure the accessibility code does not interfere with basic
|
||||||
|
;; buffer operations.
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-buffer-create-kill ()
|
||||||
|
"Test that creating and killing buffers works with accessibility enabled."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(let ((ns-accessibility-enabled t))
|
||||||
|
(let ((buf (generate-new-buffer " *ns-ax-test*")))
|
||||||
|
(should (buffer-live-p buf))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "Test content for accessibility.\n")
|
||||||
|
(should (= (point-max) 33)))
|
||||||
|
(kill-buffer buf)
|
||||||
|
(should-not (buffer-live-p buf)))))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-large-buffer ()
|
||||||
|
"Test that large buffers do not cause issues."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(let ((ns-accessibility-enabled t))
|
||||||
|
(let ((buf (generate-new-buffer " *ns-ax-large-test*")))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buf
|
||||||
|
;; Insert ~100KB of text
|
||||||
|
(dotimes (_ 1000)
|
||||||
|
(insert (make-string 100 ?x) "\n"))
|
||||||
|
(should (> (buffer-size) 100000))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(should (= (point) 1))
|
||||||
|
(goto-char (point-max))
|
||||||
|
(should (= (point) (point-max))))
|
||||||
|
(kill-buffer buf)))))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-multibyte-buffer ()
|
||||||
|
"Test that multibyte content is handled correctly."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(let ((ns-accessibility-enabled t))
|
||||||
|
(let ((buf (generate-new-buffer " *ns-ax-mb-test*")))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "Hello řeřicha 日本語 🎉\n")
|
||||||
|
(should (multibyte-string-p (buffer-string)))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(forward-word)
|
||||||
|
(should (> (point) 1)))
|
||||||
|
(kill-buffer buf)))))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-invisible-text ()
|
||||||
|
"Test that invisible text does not break buffer operations."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(let ((ns-accessibility-enabled t))
|
||||||
|
(let ((buf (generate-new-buffer " *ns-ax-invis-test*")))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "visible")
|
||||||
|
(let ((start (point)))
|
||||||
|
(insert "invisible")
|
||||||
|
(put-text-property start (point) 'invisible t))
|
||||||
|
(insert "visible again\n")
|
||||||
|
;; Buffer should contain all text
|
||||||
|
(should (string-match "invisible"
|
||||||
|
(buffer-substring-no-properties
|
||||||
|
(point-min) (point-max))))
|
||||||
|
;; But invisible text should be invisible
|
||||||
|
(goto-char (point-min))
|
||||||
|
(should (not (get-text-property 1 'invisible)))
|
||||||
|
(should (get-text-property 8 'invisible)))
|
||||||
|
(kill-buffer buf)))))
|
||||||
|
|
||||||
|
;;; --- Widget/button text property tests ---
|
||||||
|
;; Verify that text properties used by span scanning work correctly.
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-widget-property ()
|
||||||
|
"Test that `widget' text property can be set and read."
|
||||||
|
(let ((buf (generate-new-buffer " *ns-ax-widget-test*")))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "Click here")
|
||||||
|
(put-text-property 1 11 'widget '(test-widget))
|
||||||
|
(should (equal (get-text-property 1 'widget)
|
||||||
|
'(test-widget)))
|
||||||
|
(should (null (get-text-property 11 'widget))))
|
||||||
|
(kill-buffer buf))))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-button-property ()
|
||||||
|
"Test that `button' text property can be set and read."
|
||||||
|
(let ((buf (generate-new-buffer " *ns-ax-button-test*")))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buf
|
||||||
|
(insert "Press me")
|
||||||
|
(put-text-property 1 9 'button '(test-button))
|
||||||
|
(should (equal (get-text-property 1 'button)
|
||||||
|
'(test-button))))
|
||||||
|
(kill-buffer buf))))
|
||||||
|
|
||||||
|
;;; --- Window/frame safety tests ---
|
||||||
|
|
||||||
|
;; Note: this test only runs in interactive (GUI) sessions.
|
||||||
|
;; It is always skipped under 'make check' (batch mode).
|
||||||
|
(ert-deftest ns-accessibility-window-split-delete ()
|
||||||
|
"Test that window operations work with accessibility enabled."
|
||||||
|
(skip-unless (not noninteractive))
|
||||||
|
(save-window-excursion
|
||||||
|
(let ((ns-accessibility-enabled t))
|
||||||
|
(delete-other-windows)
|
||||||
|
(split-window)
|
||||||
|
(should (= (length (window-list)) 2))
|
||||||
|
(delete-other-windows)
|
||||||
|
(should (= (length (window-list)) 1)))))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-minibuffer ()
|
||||||
|
"Test that minibuffer is accessible."
|
||||||
|
(should (minibuffer-window))
|
||||||
|
(should (window-live-p (minibuffer-window))))
|
||||||
|
|
||||||
|
;;; --- Completion buffer tests ---
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-completions-buffer ()
|
||||||
|
"Test that *Completions* buffer can be created with completion content."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(let ((ns-accessibility-enabled t))
|
||||||
|
(let ((buf (get-buffer-create "*Completions*")))
|
||||||
|
(unwind-protect
|
||||||
|
(with-current-buffer buf
|
||||||
|
(display-completion-list '("alpha" "beta" "gamma"))
|
||||||
|
(should (string-match "alpha" (buffer-string)))
|
||||||
|
(should (string-match "gamma" (buffer-string))))
|
||||||
|
(kill-buffer buf)))))
|
||||||
|
|
||||||
|
;;; --- Batch-mode sanity tests ---
|
||||||
|
;; These tests verify basic variable/symbol sanity on NS builds.
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-batch-no-crash ()
|
||||||
|
"Test that accessibility variable access does not crash in batch mode."
|
||||||
|
(skip-unless (not ns-accessibility-tests-skip))
|
||||||
|
(should (boundp 'ns-accessibility-enabled))
|
||||||
|
;; In batch mode, should be nil (no window system)
|
||||||
|
(when noninteractive
|
||||||
|
(should (eq ns-accessibility-enabled nil))))
|
||||||
|
|
||||||
|
(ert-deftest ns-accessibility-symbols-consistent ()
|
||||||
|
"Test that DEFSYM symbols match their expected Lisp names.
|
||||||
|
The C code uses Qns_ax_widget for symbol `widget', etc.
|
||||||
|
This test ensures the symbol names are standard Emacs names,
|
||||||
|
not namespaced (e.g., not `ns-ax-widget')."
|
||||||
|
;; These should be standard symbols that widget.el and button.el use
|
||||||
|
(should (eq (intern "widget") 'widget))
|
||||||
|
(should (eq (intern "button") 'button))
|
||||||
|
(should (eq (intern "completion") 'completion))
|
||||||
|
;; Verify namespaced versions are NOT the ones used
|
||||||
|
;; (they would be wrong since no Lisp code sets them)
|
||||||
|
(should-not (eq (intern "widget") (intern "ns-ax-widget"))))
|
||||||
|
|
||||||
|
(provide 'nsterm-accessibility-tests)
|
||||||
|
|
||||||
|
;;; nsterm-accessibility-tests.el ends here
|
||||||
Reference in New Issue
Block a user