feat(cef): capture measured accessibility updates per webview
- commit
- 4e133f4bdf02213bd55d0922bcf3ba5153b4b7cf
- author
- brice · · 2026-07-29 08:57 UTC
- committer
- bnema · · 2026-08-14 02:42 UTC
- parent
- 482b51e
- signature
- unsigned
7 files · +163 -78
M internal/application/port/webview.go +7 -0
| @@ -437,3 +437,10 @@ type PageScrollable interface { | |||
| 437 | 437 | type PageScrollCanceler interface { | |
| 438 | 438 | CancelPageScroll(ctx context.Context) | |
| 439 | 439 | } | |
| 440 | + | ||
| 441 | + | // AccessibilityEnabler is an optional capability for adapters that can enable | |
| 442 | + | // browser accessibility tooling once per WebView. Engines that do not support | |
| 443 | + | // accessibility leave this unimplemented so callers can type-assert safely. | |
| 444 | + | type AccessibilityEnabler interface { | |
| 445 | + | EnableAccessibility() | |
| 446 | + | } | |
| @@ -437,3 +437,10 @@ type PageScrollable interface { | |||||
| 437 | type PageScrollCanceler interface { | 437 | type PageScrollCanceler interface { | ||
| 438 | CancelPageScroll(ctx context.Context) | 438 | CancelPageScroll(ctx context.Context) | ||
| 439 | } | 439 | } | ||
| 440 | + | ||||
| 441 | + | // AccessibilityEnabler is an optional capability for adapters that can enable | |||
| 442 | + | // browser accessibility tooling once per WebView. Engines that do not support | |||
| 443 | + | // accessibility leave this unimplemented so callers can type-assert safely. | |||
| 444 | + | type AccessibilityEnabler interface { | |||
| 445 | + | EnableAccessibility() | |||
| 446 | + | } | |||
M internal/domain/entity/config_types.go +16 -42
| @@ -44,10 +44,9 @@ type ActionBinding struct { | |||
| 44 | 44 | ||
| 45 | 45 | // PaneModeConfig holds pane mode shortcut configuration. | |
| 46 | 46 | type PaneModeConfig struct { | |
| 47 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 48 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 49 | - | SequenceTimeoutMilliseconds int `mapstructure:"sequence_timeout_ms" yaml:"sequence_timeout_ms" toml:"sequence_timeout_ms" json:"sequence_timeout_ms"` //nolint:lll // struct tags must stay on one line | |
| 50 | - | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 47 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 48 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 49 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 51 | 50 | } | |
| 52 | 51 | ||
| 53 | 52 | // keyBindingsFromActions converts an action binding map to a key→action map. | |
| @@ -79,11 +78,13 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string { | |||
| 79 | 78 | return keyBindingsFromActions(t.Actions) | |
| 80 | 79 | } | |
| 81 | 80 | ||
| 82 | - | // VimModeConfig holds vim scroll mode shortcut configuration. | |
| 81 | + | // VimModeConfig holds page scroll mode shortcut configuration. | |
| 83 | 82 | type VimModeConfig struct { | |
| 84 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 85 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 86 | - | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 83 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 84 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 85 | + | SequenceTimeoutMilliseconds int `mapstructure:"sequence_timeout_ms" yaml:"sequence_timeout_ms" toml:"sequence_timeout_ms" json:"sequence_timeout_ms"` //nolint:lll // struct tags must stay on one line | |
| 86 | + | PreloadAccessibility bool `mapstructure:"preload_accessibility" yaml:"preload_accessibility" toml:"preload_accessibility" json:"preload_accessibility"` //nolint:lll // struct tags must stay on one line | |
| 87 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 87 | 88 | } | |
| 88 | 89 | ||
| 89 | 90 | // GetKeyBindings returns a map from key string to action name. | |
| @@ -210,42 +211,15 @@ type BrowsingContextConfig struct { | |||
| 210 | 211 | // Deprecated: PopupBehaviorConfig is a compatibility alias for BrowsingContextConfig. | |
| 211 | 212 | type PopupBehaviorConfig = BrowsingContextConfig | |
| 212 | 213 | ||
| 213 | - | // ExternalLinkBehavior defines how externally opened URLs are placed in the workspace. | |
| 214 | - | type ExternalLinkBehavior string | |
| 215 | - | ||
| 216 | - | const ( | |
| 217 | - | ExternalLinkBehaviorWindowed ExternalLinkBehavior = "windowed" | |
| 218 | - | ExternalLinkBehaviorTabbed ExternalLinkBehavior = "tabbed" | |
| 219 | - | ExternalLinkBehaviorSplit ExternalLinkBehavior = "split" | |
| 220 | - | ExternalLinkBehaviorStacked ExternalLinkBehavior = "stacked" | |
| 221 | - | ) | |
| 222 | - | ||
| 223 | - | // ExternalLinkPlacement defines the direction used when splitting for an external URL. | |
| 224 | - | type ExternalLinkPlacement string | |
| 225 | - | ||
| 226 | - | const ( | |
| 227 | - | ExternalLinkPlacementRight ExternalLinkPlacement = "right" | |
| 228 | - | ExternalLinkPlacementLeft ExternalLinkPlacement = "left" | |
| 229 | - | ExternalLinkPlacementTop ExternalLinkPlacement = "top" | |
| 230 | - | ExternalLinkPlacementBottom ExternalLinkPlacement = "bottom" | |
| 231 | - | ) | |
| 232 | - | ||
| 233 | - | // ExternalLinksConfig controls how externally opened URLs are placed in the workspace. | |
| 234 | - | type ExternalLinksConfig struct { | |
| 235 | - | Behavior ExternalLinkBehavior `mapstructure:"behavior" yaml:"behavior" toml:"behavior" json:"behavior"` | |
| 236 | - | Placement ExternalLinkPlacement `mapstructure:"placement" yaml:"placement" toml:"placement" json:"placement"` | |
| 237 | - | } | |
| 238 | - | ||
| 239 | 214 | // WorkspaceConfig holds all workspace layout and behavior settings. | |
| 240 | 215 | type WorkspaceConfig struct { | |
| 241 | - | NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"` | |
| 242 | - | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` | |
| 243 | - | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` | |
| 244 | - | VimMode VimModeConfig `mapstructure:"vim_mode" yaml:"vim_mode" toml:"vim_mode" json:"vim_mode"` | |
| 245 | - | ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"` | |
| 246 | - | Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"` | |
| 247 | - | FloatingPane FloatingPaneConfig `mapstructure:"floating_pane" yaml:"floating_pane" toml:"floating_pane" json:"floating_pane"` | |
| 248 | - | ExternalLinks ExternalLinksConfig `mapstructure:"external_links" yaml:"external_links" toml:"external_links" json:"external_links"` | |
| 216 | + | NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"` | |
| 217 | + | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` | |
| 218 | + | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` | |
| 219 | + | VimMode VimModeConfig `mapstructure:"vim_mode" yaml:"vim_mode" toml:"vim_mode" json:"vim_mode"` | |
| 220 | + | ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"` | |
| 221 | + | Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"` | |
| 222 | + | FloatingPane FloatingPaneConfig `mapstructure:"floating_pane" yaml:"floating_pane" toml:"floating_pane" json:"floating_pane"` | |
| 249 | 223 | ||
| 250 | 224 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | |
| 251 | 225 | HideTabBarWhenSingleTab bool `mapstructure:"hide_tab_bar_when_single_tab" yaml:"hide_tab_bar_when_single_tab" toml:"hide_tab_bar_when_single_tab" json:"hide_tab_bar_when_single_tab"` //nolint:lll // struct tags must stay on one line | |
| @@ -44,10 +44,9 @@ type ActionBinding struct { | |||||
| 44 | 44 | ||||
| 45 | // PaneModeConfig holds pane mode shortcut configuration. | 45 | // PaneModeConfig holds pane mode shortcut configuration. | ||
| 46 | type PaneModeConfig struct { | 46 | type PaneModeConfig struct { | ||
| 47 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | 47 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line |
| 48 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | 48 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` |
| 49 | - | SequenceTimeoutMilliseconds int `mapstructure:"sequence_timeout_ms" yaml:"sequence_timeout_ms" toml:"sequence_timeout_ms" json:"sequence_timeout_ms"` //nolint:lll // struct tags must stay on one line | 49 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` |
| 50 | - | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |||
| 51 | } | 50 | } | ||
| 52 | 51 | ||||
| 53 | // keyBindingsFromActions converts an action binding map to a key→action map. | 52 | // keyBindingsFromActions converts an action binding map to a key→action map. | ||
| @@ -79,11 +78,13 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string { | |||||
| 79 | return keyBindingsFromActions(t.Actions) | 78 | return keyBindingsFromActions(t.Actions) | ||
| 80 | } | 79 | } | ||
| 81 | 80 | ||||
| 82 | - | // VimModeConfig holds vim scroll mode shortcut configuration. | 81 | + | // VimModeConfig holds page scroll mode shortcut configuration. |
| 83 | type VimModeConfig struct { | 82 | type VimModeConfig struct { | ||
| 84 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | 83 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line |
| 85 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | 84 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` |
| 86 | - | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | 85 | + | SequenceTimeoutMilliseconds int `mapstructure:"sequence_timeout_ms" yaml:"sequence_timeout_ms" toml:"sequence_timeout_ms" json:"sequence_timeout_ms"` //nolint:lll // struct tags must stay on one line |
| 86 | + | PreloadAccessibility bool `mapstructure:"preload_accessibility" yaml:"preload_accessibility" toml:"preload_accessibility" json:"preload_accessibility"` //nolint:lll // struct tags must stay on one line | |||
| 87 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |||
| 87 | } | 88 | } | ||
| 88 | 89 | ||||
| 89 | // GetKeyBindings returns a map from key string to action name. | 90 | // GetKeyBindings returns a map from key string to action name. | ||
| @@ -210,42 +211,15 @@ type BrowsingContextConfig struct { | |||||
| 210 | // Deprecated: PopupBehaviorConfig is a compatibility alias for BrowsingContextConfig. | 211 | // Deprecated: PopupBehaviorConfig is a compatibility alias for BrowsingContextConfig. | ||
| 211 | type PopupBehaviorConfig = BrowsingContextConfig | 212 | type PopupBehaviorConfig = BrowsingContextConfig | ||
| 212 | 213 | ||||
| 213 | - | // ExternalLinkBehavior defines how externally opened URLs are placed in the workspace. | |||
| 214 | - | type ExternalLinkBehavior string | |||
| 215 | - | ||||
| 216 | - | const ( | |||
| 217 | - | ExternalLinkBehaviorWindowed ExternalLinkBehavior = "windowed" | |||
| 218 | - | ExternalLinkBehaviorTabbed ExternalLinkBehavior = "tabbed" | |||
| 219 | - | ExternalLinkBehaviorSplit ExternalLinkBehavior = "split" | |||
| 220 | - | ExternalLinkBehaviorStacked ExternalLinkBehavior = "stacked" | |||
| 221 | - | ) | |||
| 222 | - | ||||
| 223 | - | // ExternalLinkPlacement defines the direction used when splitting for an external URL. | |||
| 224 | - | type ExternalLinkPlacement string | |||
| 225 | - | ||||
| 226 | - | const ( | |||
| 227 | - | ExternalLinkPlacementRight ExternalLinkPlacement = "right" | |||
| 228 | - | ExternalLinkPlacementLeft ExternalLinkPlacement = "left" | |||
| 229 | - | ExternalLinkPlacementTop ExternalLinkPlacement = "top" | |||
| 230 | - | ExternalLinkPlacementBottom ExternalLinkPlacement = "bottom" | |||
| 231 | - | ) | |||
| 232 | - | ||||
| 233 | - | // ExternalLinksConfig controls how externally opened URLs are placed in the workspace. | |||
| 234 | - | type ExternalLinksConfig struct { | |||
| 235 | - | Behavior ExternalLinkBehavior `mapstructure:"behavior" yaml:"behavior" toml:"behavior" json:"behavior"` | |||
| 236 | - | Placement ExternalLinkPlacement `mapstructure:"placement" yaml:"placement" toml:"placement" json:"placement"` | |||
| 237 | - | } | |||
| 238 | - | ||||
| 239 | // WorkspaceConfig holds all workspace layout and behavior settings. | 214 | // WorkspaceConfig holds all workspace layout and behavior settings. | ||
| 240 | type WorkspaceConfig struct { | 215 | type WorkspaceConfig struct { | ||
| 241 | - | NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"` | 216 | + | NewPaneURL string `mapstructure:"new_pane_url" yaml:"new_pane_url" toml:"new_pane_url" json:"new_pane_url"` |
| 242 | - | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` | 217 | + | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` |
| 243 | - | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` | 218 | + | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` |
| 244 | - | VimMode VimModeConfig `mapstructure:"vim_mode" yaml:"vim_mode" toml:"vim_mode" json:"vim_mode"` | 219 | + | VimMode VimModeConfig `mapstructure:"vim_mode" yaml:"vim_mode" toml:"vim_mode" json:"vim_mode"` |
| 245 | - | ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"` | 220 | + | ResizeMode ResizeModeConfig `mapstructure:"resize_mode" yaml:"resize_mode" toml:"resize_mode" json:"resize_mode"` |
| 246 | - | Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"` | 221 | + | Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"` |
| 247 | - | FloatingPane FloatingPaneConfig `mapstructure:"floating_pane" yaml:"floating_pane" toml:"floating_pane" json:"floating_pane"` | 222 | + | FloatingPane FloatingPaneConfig `mapstructure:"floating_pane" yaml:"floating_pane" toml:"floating_pane" json:"floating_pane"` |
| 248 | - | ExternalLinks ExternalLinksConfig `mapstructure:"external_links" yaml:"external_links" toml:"external_links" json:"external_links"` | |||
| 249 | 223 | ||||
| 250 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | 224 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | ||
| 251 | HideTabBarWhenSingleTab bool `mapstructure:"hide_tab_bar_when_single_tab" yaml:"hide_tab_bar_when_single_tab" toml:"hide_tab_bar_when_single_tab" json:"hide_tab_bar_when_single_tab"` //nolint:lll // struct tags must stay on one line | 225 | HideTabBarWhenSingleTab bool `mapstructure:"hide_tab_bar_when_single_tab" yaml:"hide_tab_bar_when_single_tab" toml:"hide_tab_bar_when_single_tab" json:"hide_tab_bar_when_single_tab"` //nolint:lll // struct tags must stay on one line | ||
A internal/infrastructure/cef/accessibility_enable_test.go +103 -0
| @@ -0,0 +1,103 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | "github.com/stretchr/testify/assert" | |
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/dumber/internal/application/port" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | type accessibilityStateHost struct { | |
| 15 | + | purecef.BrowserHost | |
| 16 | + | ||
| 17 | + | mu sync.Mutex | |
| 18 | + | states []purecef.State | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (h *accessibilityStateHost) SetAccessibilityState(state purecef.State) { | |
| 22 | + | h.mu.Lock() | |
| 23 | + | defer h.mu.Unlock() | |
| 24 | + | h.states = append(h.states, state) | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func (h *accessibilityStateHost) recordedStates() []purecef.State { | |
| 28 | + | h.mu.Lock() | |
| 29 | + | defer h.mu.Unlock() | |
| 30 | + | out := make([]purecef.State, len(h.states)) | |
| 31 | + | copy(out, h.states) | |
| 32 | + | return out | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func TestWebView_EnableAccessibility_NoOpWhenDestroyedOrHostNil(t *testing.T) { | |
| 36 | + | t.Parallel() | |
| 37 | + | ||
| 38 | + | destroyed := &WebView{} | |
| 39 | + | destroyed.destroyed.Store(true) | |
| 40 | + | host := &accessibilityStateHost{} | |
| 41 | + | destroyed.host = host | |
| 42 | + | destroyed.EnableAccessibility() | |
| 43 | + | assert.Empty(t, host.recordedStates()) | |
| 44 | + | ||
| 45 | + | noHost := &WebView{} | |
| 46 | + | noHost.EnableAccessibility() | |
| 47 | + | assert.False(t, noHost.a11yEnabled.Load()) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestWebView_EnableAccessibility_IdempotentOncePerWebView(t *testing.T) { | |
| 51 | + | t.Parallel() | |
| 52 | + | ||
| 53 | + | host := &accessibilityStateHost{} | |
| 54 | + | wv := &WebView{host: host} | |
| 55 | + | ||
| 56 | + | wv.EnableAccessibility() | |
| 57 | + | wv.EnableAccessibility() | |
| 58 | + | wv.EnableAccessibility() | |
| 59 | + | ||
| 60 | + | states := host.recordedStates() | |
| 61 | + | require.Len(t, states, 1) | |
| 62 | + | assert.Equal(t, purecef.StateStateEnabled, states[0]) | |
| 63 | + | assert.True(t, wv.a11yEnabled.Load()) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func TestWebView_EnableAccessibility_IndependentPerWebView(t *testing.T) { | |
| 67 | + | t.Parallel() | |
| 68 | + | ||
| 69 | + | hostA := &accessibilityStateHost{} | |
| 70 | + | hostB := &accessibilityStateHost{} | |
| 71 | + | wvA := &WebView{host: hostA} | |
| 72 | + | wvB := &WebView{host: hostB} | |
| 73 | + | ||
| 74 | + | wvA.EnableAccessibility() | |
| 75 | + | wvA.EnableAccessibility() | |
| 76 | + | wvB.EnableAccessibility() | |
| 77 | + | ||
| 78 | + | require.Len(t, hostA.recordedStates(), 1) | |
| 79 | + | require.Len(t, hostB.recordedStates(), 1) | |
| 80 | + | assert.Equal(t, purecef.StateStateEnabled, hostA.recordedStates()[0]) | |
| 81 | + | assert.Equal(t, purecef.StateStateEnabled, hostB.recordedStates()[0]) | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func TestWebView_EnableAccessibility_RetriesWhenHostBecomesReady(t *testing.T) { | |
| 85 | + | t.Parallel() | |
| 86 | + | ||
| 87 | + | wv := &WebView{} | |
| 88 | + | wv.EnableAccessibility() | |
| 89 | + | assert.False(t, wv.a11yEnabled.Load()) | |
| 90 | + | ||
| 91 | + | host := &accessibilityStateHost{} | |
| 92 | + | wv.host = host | |
| 93 | + | wv.EnableAccessibility() | |
| 94 | + | ||
| 95 | + | require.Len(t, host.recordedStates(), 1) | |
| 96 | + | assert.Equal(t, purecef.StateStateEnabled, host.recordedStates()[0]) | |
| 97 | + | assert.True(t, wv.a11yEnabled.Load()) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func TestWebView_ImplementsAccessibilityEnabler(t *testing.T) { | |
| 101 | + | t.Parallel() | |
| 102 | + | var _ port.AccessibilityEnabler = (*WebView)(nil) | |
| 103 | + | } | |
| @@ -0,0 +1,103 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 8 | + | "github.com/stretchr/testify/assert" | |||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | type accessibilityStateHost struct { | |||
| 15 | + | purecef.BrowserHost | |||
| 16 | + | ||||
| 17 | + | mu sync.Mutex | |||
| 18 | + | states []purecef.State | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (h *accessibilityStateHost) SetAccessibilityState(state purecef.State) { | |||
| 22 | + | h.mu.Lock() | |||
| 23 | + | defer h.mu.Unlock() | |||
| 24 | + | h.states = append(h.states, state) | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func (h *accessibilityStateHost) recordedStates() []purecef.State { | |||
| 28 | + | h.mu.Lock() | |||
| 29 | + | defer h.mu.Unlock() | |||
| 30 | + | out := make([]purecef.State, len(h.states)) | |||
| 31 | + | copy(out, h.states) | |||
| 32 | + | return out | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func TestWebView_EnableAccessibility_NoOpWhenDestroyedOrHostNil(t *testing.T) { | |||
| 36 | + | t.Parallel() | |||
| 37 | + | ||||
| 38 | + | destroyed := &WebView{} | |||
| 39 | + | destroyed.destroyed.Store(true) | |||
| 40 | + | host := &accessibilityStateHost{} | |||
| 41 | + | destroyed.host = host | |||
| 42 | + | destroyed.EnableAccessibility() | |||
| 43 | + | assert.Empty(t, host.recordedStates()) | |||
| 44 | + | ||||
| 45 | + | noHost := &WebView{} | |||
| 46 | + | noHost.EnableAccessibility() | |||
| 47 | + | assert.False(t, noHost.a11yEnabled.Load()) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestWebView_EnableAccessibility_IdempotentOncePerWebView(t *testing.T) { | |||
| 51 | + | t.Parallel() | |||
| 52 | + | ||||
| 53 | + | host := &accessibilityStateHost{} | |||
| 54 | + | wv := &WebView{host: host} | |||
| 55 | + | ||||
| 56 | + | wv.EnableAccessibility() | |||
| 57 | + | wv.EnableAccessibility() | |||
| 58 | + | wv.EnableAccessibility() | |||
| 59 | + | ||||
| 60 | + | states := host.recordedStates() | |||
| 61 | + | require.Len(t, states, 1) | |||
| 62 | + | assert.Equal(t, purecef.StateStateEnabled, states[0]) | |||
| 63 | + | assert.True(t, wv.a11yEnabled.Load()) | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func TestWebView_EnableAccessibility_IndependentPerWebView(t *testing.T) { | |||
| 67 | + | t.Parallel() | |||
| 68 | + | ||||
| 69 | + | hostA := &accessibilityStateHost{} | |||
| 70 | + | hostB := &accessibilityStateHost{} | |||
| 71 | + | wvA := &WebView{host: hostA} | |||
| 72 | + | wvB := &WebView{host: hostB} | |||
| 73 | + | ||||
| 74 | + | wvA.EnableAccessibility() | |||
| 75 | + | wvA.EnableAccessibility() | |||
| 76 | + | wvB.EnableAccessibility() | |||
| 77 | + | ||||
| 78 | + | require.Len(t, hostA.recordedStates(), 1) | |||
| 79 | + | require.Len(t, hostB.recordedStates(), 1) | |||
| 80 | + | assert.Equal(t, purecef.StateStateEnabled, hostA.recordedStates()[0]) | |||
| 81 | + | assert.Equal(t, purecef.StateStateEnabled, hostB.recordedStates()[0]) | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | func TestWebView_EnableAccessibility_RetriesWhenHostBecomesReady(t *testing.T) { | |||
| 85 | + | t.Parallel() | |||
| 86 | + | ||||
| 87 | + | wv := &WebView{} | |||
| 88 | + | wv.EnableAccessibility() | |||
| 89 | + | assert.False(t, wv.a11yEnabled.Load()) | |||
| 90 | + | ||||
| 91 | + | host := &accessibilityStateHost{} | |||
| 92 | + | wv.host = host | |||
| 93 | + | wv.EnableAccessibility() | |||
| 94 | + | ||||
| 95 | + | require.Len(t, host.recordedStates(), 1) | |||
| 96 | + | assert.Equal(t, purecef.StateStateEnabled, host.recordedStates()[0]) | |||
| 97 | + | assert.True(t, wv.a11yEnabled.Load()) | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func TestWebView_ImplementsAccessibilityEnabler(t *testing.T) { | |||
| 101 | + | t.Parallel() | |||
| 102 | + | var _ port.AccessibilityEnabler = (*WebView)(nil) | |||
| 103 | + | } | |||
M internal/infrastructure/cef/webview.go +22 -0
| @@ -37,6 +37,7 @@ var ( | |||
| 37 | 37 | _ port.OAuthCallbackCapable = (*WebView)(nil) | |
| 38 | 38 | _ port.PageScrollable = (*WebView)(nil) | |
| 39 | 39 | _ port.PageScrollCanceler = (*WebView)(nil) | |
| 40 | + | _ port.AccessibilityEnabler = (*WebView)(nil) | |
| 40 | 41 | ) | |
| 41 | 42 | ||
| 42 | 43 | // errDestroyed is returned when an operation is attempted on a destroyed WebView. | |
| @@ -157,6 +158,7 @@ type WebView struct { | |||
| 157 | 158 | a11yWorker *accessibilityCaptureWorker | |
| 158 | 159 | a11yCapture *accessibilityCapture | |
| 159 | 160 | a11yStats accessibilityStats | |
| 161 | + | a11yEnabled atomic.Bool | |
| 160 | 162 | ||
| 161 | 163 | // beginFrameTick drives CEF external BeginFrame requests while the GTK | |
| 162 | 164 | // widget is visible. Access is guarded by mu. | |
| @@ -2541,6 +2543,26 @@ func (wv *WebView) CancelPageScroll(_ context.Context) { | |||
| 2541 | 2543 | q.commitMu.Unlock() | |
| 2542 | 2544 | } | |
| 2543 | 2545 | ||
| 2546 | + | // EnableAccessibility turns on CEF accessibility for this WebView exactly once. | |
| 2547 | + | // Calls before the browser host exists are no-ops so UI can safely retry. | |
| 2548 | + | func (wv *WebView) EnableAccessibility() { | |
| 2549 | + | if wv == nil || wv.destroyed.Load() || wv.a11yEnabled.Load() { | |
| 2550 | + | return | |
| 2551 | + | } | |
| 2552 | + | wv.mu.RLock() | |
| 2553 | + | host := wv.host | |
| 2554 | + | wv.mu.RUnlock() | |
| 2555 | + | if host == nil || !wv.a11yEnabled.CompareAndSwap(false, true) { | |
| 2556 | + | return | |
| 2557 | + | } | |
| 2558 | + | host.SetAccessibilityState(purecef.StateStateEnabled) | |
| 2559 | + | if wv.ctx != nil { | |
| 2560 | + | logging.FromContext(wv.ctx).Debug(). | |
| 2561 | + | Uint64("webview_id", uint64(wv.id)). | |
| 2562 | + | Msg("cef: accessibility enabled") | |
| 2563 | + | } | |
| 2564 | + | } | |
| 2565 | + | ||
| 2544 | 2566 | func (wv *WebView) resetPageScrollQueue() { | |
| 2545 | 2567 | q := &wv.pageScrollQueue | |
| 2546 | 2568 | q.mu.Lock() | |
| @@ -37,6 +37,7 @@ var ( | |||||
| 37 | _ port.OAuthCallbackCapable = (*WebView)(nil) | 37 | _ port.OAuthCallbackCapable = (*WebView)(nil) | ||
| 38 | _ port.PageScrollable = (*WebView)(nil) | 38 | _ port.PageScrollable = (*WebView)(nil) | ||
| 39 | _ port.PageScrollCanceler = (*WebView)(nil) | 39 | _ port.PageScrollCanceler = (*WebView)(nil) | ||
| 40 | + | _ port.AccessibilityEnabler = (*WebView)(nil) | |||
| 40 | ) | 41 | ) | ||
| 41 | 42 | ||||
| 42 | // errDestroyed is returned when an operation is attempted on a destroyed WebView. | 43 | // errDestroyed is returned when an operation is attempted on a destroyed WebView. | ||
| @@ -157,6 +158,7 @@ type WebView struct { | |||||
| 157 | a11yWorker *accessibilityCaptureWorker | 158 | a11yWorker *accessibilityCaptureWorker | ||
| 158 | a11yCapture *accessibilityCapture | 159 | a11yCapture *accessibilityCapture | ||
| 159 | a11yStats accessibilityStats | 160 | a11yStats accessibilityStats | ||
| 161 | + | a11yEnabled atomic.Bool | |||
| 160 | 162 | ||||
| 161 | // beginFrameTick drives CEF external BeginFrame requests while the GTK | 163 | // beginFrameTick drives CEF external BeginFrame requests while the GTK | ||
| 162 | // widget is visible. Access is guarded by mu. | 164 | // widget is visible. Access is guarded by mu. | ||
| @@ -2541,6 +2543,26 @@ func (wv *WebView) CancelPageScroll(_ context.Context) { | |||||
| 2541 | q.commitMu.Unlock() | 2543 | q.commitMu.Unlock() | ||
| 2542 | } | 2544 | } | ||
| 2543 | 2545 | ||||
| 2546 | + | // EnableAccessibility turns on CEF accessibility for this WebView exactly once. | |||
| 2547 | + | // Calls before the browser host exists are no-ops so UI can safely retry. | |||
| 2548 | + | func (wv *WebView) EnableAccessibility() { | |||
| 2549 | + | if wv == nil || wv.destroyed.Load() || wv.a11yEnabled.Load() { | |||
| 2550 | + | return | |||
| 2551 | + | } | |||
| 2552 | + | wv.mu.RLock() | |||
| 2553 | + | host := wv.host | |||
| 2554 | + | wv.mu.RUnlock() | |||
| 2555 | + | if host == nil || !wv.a11yEnabled.CompareAndSwap(false, true) { | |||
| 2556 | + | return | |||
| 2557 | + | } | |||
| 2558 | + | host.SetAccessibilityState(purecef.StateStateEnabled) | |||
| 2559 | + | if wv.ctx != nil { | |||
| 2560 | + | logging.FromContext(wv.ctx).Debug(). | |||
| 2561 | + | Uint64("webview_id", uint64(wv.id)). | |||
| 2562 | + | Msg("cef: accessibility enabled") | |||
| 2563 | + | } | |||
| 2564 | + | } | |||
| 2565 | + | ||||
| 2544 | func (wv *WebView) resetPageScrollQueue() { | 2566 | func (wv *WebView) resetPageScrollQueue() { | ||
| 2545 | q := &wv.pageScrollQueue | 2567 | q := &wv.pageScrollQueue | ||
| 2546 | q.mu.Lock() | 2568 | q.mu.Lock() | ||
M internal/infrastructure/config/loader.go +5 -17
| @@ -175,6 +175,10 @@ func (m *Manager) transformLegacyConfig() { | |||
| 175 | 175 | transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig) | |
| 176 | 176 | } | |
| 177 | 177 | ||
| 178 | + | // Merge newly added default actions into in-memory config so existing persisted | |
| 179 | + | // action maps keep working before the user writes a migrated config to disk. | |
| 180 | + | configMigrator.mergeMissingDefaultActions(rawConfig) | |
| 181 | + | ||
| 178 | 182 | // Apply transformed config back to viper | |
| 179 | 183 | for key, value := range rawConfig { | |
| 180 | 184 | m.viper.Set(key, value) | |
| @@ -374,7 +378,6 @@ func ensureDatabasePath(config *Config) error { | |||
| 374 | 378 | } | |
| 375 | 379 | ||
| 376 | 380 | func normalizeConfig(config *Config) { | |
| 377 | - | normalizeExternalLinks(config) | |
| 378 | 381 | normalizeAppearance(config) | |
| 379 | 382 | normalizeMedia(config) | |
| 380 | 383 | normalizeEngineConfig(config) | |
| @@ -387,20 +390,6 @@ func normalizeBrowsingContexts(config *Config) { | |||
| 387 | 390 | config.Workspace.Popups = config.Workspace.BrowsingContexts | |
| 388 | 391 | } | |
| 389 | 392 | ||
| 390 | - | func normalizeExternalLinks(config *Config) { | |
| 391 | - | switch config.Workspace.ExternalLinks.Behavior { | |
| 392 | - | case ExternalLinkBehaviorWindowed, ExternalLinkBehaviorTabbed, ExternalLinkBehaviorSplit, ExternalLinkBehaviorStacked: | |
| 393 | - | default: | |
| 394 | - | config.Workspace.ExternalLinks.Behavior = defaultExternalLinkBehavior | |
| 395 | - | } | |
| 396 | - | ||
| 397 | - | switch config.Workspace.ExternalLinks.Placement { | |
| 398 | - | case ExternalLinkPlacementRight, ExternalLinkPlacementLeft, ExternalLinkPlacementTop, ExternalLinkPlacementBottom: | |
| 399 | - | default: | |
| 400 | - | config.Workspace.ExternalLinks.Placement = defaultExternalLinkPlacement | |
| 401 | - | } | |
| 402 | - | } | |
| 403 | - | ||
| 404 | 393 | func normalizeEngineConfig(config *Config) { | |
| 405 | 394 | // Normalize GSK renderer (engine.webkit) | |
| 406 | 395 | switch strings.ToLower(string(config.Engine.WebKit.GSKRenderer)) { | |
| @@ -709,6 +698,7 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |||
| 709 | 698 | m.viper.SetDefault("workspace.vim_mode.activation_shortcut", defaults.Workspace.VimMode.ActivationShortcut) | |
| 710 | 699 | m.viper.SetDefault("workspace.vim_mode.timeout_ms", defaults.Workspace.VimMode.TimeoutMilliseconds) | |
| 711 | 700 | m.viper.SetDefault("workspace.vim_mode.sequence_timeout_ms", defaults.Workspace.VimMode.SequenceTimeoutMilliseconds) | |
| 701 | + | m.viper.SetDefault("workspace.vim_mode.preload_accessibility", defaults.Workspace.VimMode.PreloadAccessibility) | |
| 712 | 702 | m.viper.SetDefault("workspace.vim_mode.actions", defaults.Workspace.VimMode.Actions) | |
| 713 | 703 | m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut) | |
| 714 | 704 | m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds) | |
| @@ -719,8 +709,6 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |||
| 719 | 709 | m.viper.SetDefault("workspace.floating_pane.width_pct", defaults.Workspace.FloatingPane.WidthPct) | |
| 720 | 710 | m.viper.SetDefault("workspace.floating_pane.height_pct", defaults.Workspace.FloatingPane.HeightPct) | |
| 721 | 711 | m.viper.SetDefault("workspace.floating_pane.profiles", defaults.Workspace.FloatingPane.Profiles) | |
| 722 | - | m.viper.SetDefault("workspace.external_links.behavior", string(defaults.Workspace.ExternalLinks.Behavior)) | |
| 723 | - | m.viper.SetDefault("workspace.external_links.placement", string(defaults.Workspace.ExternalLinks.Placement)) | |
| 724 | 712 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | |
| 725 | 713 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | |
| 726 | 714 | m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove) | |
| @@ -175,6 +175,10 @@ func (m *Manager) transformLegacyConfig() { | |||||
| 175 | transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig) | 175 | transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig) | ||
| 176 | } | 176 | } | ||
| 177 | 177 | ||||
| 178 | + | // Merge newly added default actions into in-memory config so existing persisted | |||
| 179 | + | // action maps keep working before the user writes a migrated config to disk. | |||
| 180 | + | configMigrator.mergeMissingDefaultActions(rawConfig) | |||
| 181 | + | ||||
| 178 | // Apply transformed config back to viper | 182 | // Apply transformed config back to viper | ||
| 179 | for key, value := range rawConfig { | 183 | for key, value := range rawConfig { | ||
| 180 | m.viper.Set(key, value) | 184 | m.viper.Set(key, value) | ||
| @@ -374,7 +378,6 @@ func ensureDatabasePath(config *Config) error { | |||||
| 374 | } | 378 | } | ||
| 375 | 379 | ||||
| 376 | func normalizeConfig(config *Config) { | 380 | func normalizeConfig(config *Config) { | ||
| 377 | - | normalizeExternalLinks(config) | |||
| 378 | normalizeAppearance(config) | 381 | normalizeAppearance(config) | ||
| 379 | normalizeMedia(config) | 382 | normalizeMedia(config) | ||
| 380 | normalizeEngineConfig(config) | 383 | normalizeEngineConfig(config) | ||
| @@ -387,20 +390,6 @@ func normalizeBrowsingContexts(config *Config) { | |||||
| 387 | config.Workspace.Popups = config.Workspace.BrowsingContexts | 390 | config.Workspace.Popups = config.Workspace.BrowsingContexts | ||
| 388 | } | 391 | } | ||
| 389 | 392 | ||||
| 390 | - | func normalizeExternalLinks(config *Config) { | |||
| 391 | - | switch config.Workspace.ExternalLinks.Behavior { | |||
| 392 | - | case ExternalLinkBehaviorWindowed, ExternalLinkBehaviorTabbed, ExternalLinkBehaviorSplit, ExternalLinkBehaviorStacked: | |||
| 393 | - | default: | |||
| 394 | - | config.Workspace.ExternalLinks.Behavior = defaultExternalLinkBehavior | |||
| 395 | - | } | |||
| 396 | - | ||||
| 397 | - | switch config.Workspace.ExternalLinks.Placement { | |||
| 398 | - | case ExternalLinkPlacementRight, ExternalLinkPlacementLeft, ExternalLinkPlacementTop, ExternalLinkPlacementBottom: | |||
| 399 | - | default: | |||
| 400 | - | config.Workspace.ExternalLinks.Placement = defaultExternalLinkPlacement | |||
| 401 | - | } | |||
| 402 | - | } | |||
| 403 | - | ||||
| 404 | func normalizeEngineConfig(config *Config) { | 393 | func normalizeEngineConfig(config *Config) { | ||
| 405 | // Normalize GSK renderer (engine.webkit) | 394 | // Normalize GSK renderer (engine.webkit) | ||
| 406 | switch strings.ToLower(string(config.Engine.WebKit.GSKRenderer)) { | 395 | switch strings.ToLower(string(config.Engine.WebKit.GSKRenderer)) { | ||
| @@ -709,6 +698,7 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |||||
| 709 | m.viper.SetDefault("workspace.vim_mode.activation_shortcut", defaults.Workspace.VimMode.ActivationShortcut) | 698 | m.viper.SetDefault("workspace.vim_mode.activation_shortcut", defaults.Workspace.VimMode.ActivationShortcut) | ||
| 710 | m.viper.SetDefault("workspace.vim_mode.timeout_ms", defaults.Workspace.VimMode.TimeoutMilliseconds) | 699 | m.viper.SetDefault("workspace.vim_mode.timeout_ms", defaults.Workspace.VimMode.TimeoutMilliseconds) | ||
| 711 | m.viper.SetDefault("workspace.vim_mode.sequence_timeout_ms", defaults.Workspace.VimMode.SequenceTimeoutMilliseconds) | 700 | m.viper.SetDefault("workspace.vim_mode.sequence_timeout_ms", defaults.Workspace.VimMode.SequenceTimeoutMilliseconds) | ||
| 701 | + | m.viper.SetDefault("workspace.vim_mode.preload_accessibility", defaults.Workspace.VimMode.PreloadAccessibility) | |||
| 712 | m.viper.SetDefault("workspace.vim_mode.actions", defaults.Workspace.VimMode.Actions) | 702 | m.viper.SetDefault("workspace.vim_mode.actions", defaults.Workspace.VimMode.Actions) | ||
| 713 | m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut) | 703 | m.viper.SetDefault("workspace.resize_mode.activation_shortcut", defaults.Workspace.ResizeMode.ActivationShortcut) | ||
| 714 | m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds) | 704 | m.viper.SetDefault("workspace.resize_mode.timeout_ms", defaults.Workspace.ResizeMode.TimeoutMilliseconds) | ||
| @@ -719,8 +709,6 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |||||
| 719 | m.viper.SetDefault("workspace.floating_pane.width_pct", defaults.Workspace.FloatingPane.WidthPct) | 709 | m.viper.SetDefault("workspace.floating_pane.width_pct", defaults.Workspace.FloatingPane.WidthPct) | ||
| 720 | m.viper.SetDefault("workspace.floating_pane.height_pct", defaults.Workspace.FloatingPane.HeightPct) | 710 | m.viper.SetDefault("workspace.floating_pane.height_pct", defaults.Workspace.FloatingPane.HeightPct) | ||
| 721 | m.viper.SetDefault("workspace.floating_pane.profiles", defaults.Workspace.FloatingPane.Profiles) | 711 | m.viper.SetDefault("workspace.floating_pane.profiles", defaults.Workspace.FloatingPane.Profiles) | ||
| 722 | - | m.viper.SetDefault("workspace.external_links.behavior", string(defaults.Workspace.ExternalLinks.Behavior)) | |||
| 723 | - | m.viper.SetDefault("workspace.external_links.placement", string(defaults.Workspace.ExternalLinks.Placement)) | |||
| 724 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | 712 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | ||
| 725 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | 713 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | ||
| 726 | m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove) | 714 | m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove) | ||
M internal/infrastructure/config/schema_provider.go +8 -18
| @@ -477,7 +477,7 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||
| 477 | 477 | Key: "workspace.vim_mode.activation_shortcut", | |
| 478 | 478 | Type: "string", | |
| 479 | 479 | Default: defaults.Workspace.VimMode.ActivationShortcut, | |
| 480 | - | Description: "Shortcut to enter vim scroll mode", | |
| 480 | + | Description: "Shortcut to enter page scroll mode", | |
| 481 | 481 | Section: SectionWorkspace, | |
| 482 | 482 | }, | |
| 483 | 483 | { | |
| @@ -496,6 +496,13 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||
| 496 | 496 | Range: ">=0", | |
| 497 | 497 | Section: SectionWorkspace, | |
| 498 | 498 | }, | |
| 499 | + | { | |
| 500 | + | Key: "workspace.vim_mode.preload_accessibility", | |
| 501 | + | Type: "bool", | |
| 502 | + | Default: fmt.Sprintf("%t", defaults.Workspace.VimMode.PreloadAccessibility), | |
| 503 | + | Description: "Pre-enable browser accessibility when a WebView is shown (default false; enable on Page Mode enter otherwise)", | |
| 504 | + | Section: SectionWorkspace, | |
| 505 | + | }, | |
| 499 | 506 | { | |
| 500 | 507 | Key: "workspace.vim_mode.actions.<action>", | |
| 501 | 508 | Type: "object", | |
| @@ -572,23 +579,6 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||
| 572 | 579 | Description: "Named floating pane URL profiles with keys, url, and optional desc", | |
| 573 | 580 | Section: SectionWorkspace, | |
| 574 | 581 | }, | |
| 575 | - | // External links | |
| 576 | - | { | |
| 577 | - | Key: "workspace.external_links.behavior", | |
| 578 | - | Type: "string", | |
| 579 | - | Default: string(defaults.Workspace.ExternalLinks.Behavior), | |
| 580 | - | Description: "How URLs opened from external applications are placed", | |
| 581 | - | Values: []string{"windowed", "tabbed", "split", "stacked"}, | |
| 582 | - | Section: SectionWorkspace, | |
| 583 | - | }, | |
| 584 | - | { | |
| 585 | - | Key: "workspace.external_links.placement", | |
| 586 | - | Type: "string", | |
| 587 | - | Default: string(defaults.Workspace.ExternalLinks.Placement), | |
| 588 | - | Description: "Placement direction for split external links", | |
| 589 | - | Values: []string{"right", "left", "top", "bottom"}, | |
| 590 | - | Section: SectionWorkspace, | |
| 591 | - | }, | |
| 592 | 582 | // Browsing contexts | |
| 593 | 583 | { | |
| 594 | 584 | Key: "workspace.browsing_contexts.behavior", | |
| @@ -477,7 +477,7 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||||
| 477 | Key: "workspace.vim_mode.activation_shortcut", | 477 | Key: "workspace.vim_mode.activation_shortcut", | ||
| 478 | Type: "string", | 478 | Type: "string", | ||
| 479 | Default: defaults.Workspace.VimMode.ActivationShortcut, | 479 | Default: defaults.Workspace.VimMode.ActivationShortcut, | ||
| 480 | - | Description: "Shortcut to enter vim scroll mode", | 480 | + | Description: "Shortcut to enter page scroll mode", |
| 481 | Section: SectionWorkspace, | 481 | Section: SectionWorkspace, | ||
| 482 | }, | 482 | }, | ||
| 483 | { | 483 | { | ||
| @@ -496,6 +496,13 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||||
| 496 | Range: ">=0", | 496 | Range: ">=0", | ||
| 497 | Section: SectionWorkspace, | 497 | Section: SectionWorkspace, | ||
| 498 | }, | 498 | }, | ||
| 499 | + | { | |||
| 500 | + | Key: "workspace.vim_mode.preload_accessibility", | |||
| 501 | + | Type: "bool", | |||
| 502 | + | Default: fmt.Sprintf("%t", defaults.Workspace.VimMode.PreloadAccessibility), | |||
| 503 | + | Description: "Pre-enable browser accessibility when a WebView is shown (default false; enable on Page Mode enter otherwise)", | |||
| 504 | + | Section: SectionWorkspace, | |||
| 505 | + | }, | |||
| 499 | { | 506 | { | ||
| 500 | Key: "workspace.vim_mode.actions.<action>", | 507 | Key: "workspace.vim_mode.actions.<action>", | ||
| 501 | Type: "object", | 508 | Type: "object", | ||
| @@ -572,23 +579,6 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||||
| 572 | Description: "Named floating pane URL profiles with keys, url, and optional desc", | 579 | Description: "Named floating pane URL profiles with keys, url, and optional desc", | ||
| 573 | Section: SectionWorkspace, | 580 | Section: SectionWorkspace, | ||
| 574 | }, | 581 | }, | ||
| 575 | - | // External links | |||
| 576 | - | { | |||
| 577 | - | Key: "workspace.external_links.behavior", | |||
| 578 | - | Type: "string", | |||
| 579 | - | Default: string(defaults.Workspace.ExternalLinks.Behavior), | |||
| 580 | - | Description: "How URLs opened from external applications are placed", | |||
| 581 | - | Values: []string{"windowed", "tabbed", "split", "stacked"}, | |||
| 582 | - | Section: SectionWorkspace, | |||
| 583 | - | }, | |||
| 584 | - | { | |||
| 585 | - | Key: "workspace.external_links.placement", | |||
| 586 | - | Type: "string", | |||
| 587 | - | Default: string(defaults.Workspace.ExternalLinks.Placement), | |||
| 588 | - | Description: "Placement direction for split external links", | |||
| 589 | - | Values: []string{"right", "left", "top", "bottom"}, | |||
| 590 | - | Section: SectionWorkspace, | |||
| 591 | - | }, | |||
| 592 | // Browsing contexts | 582 | // Browsing contexts | ||
| 593 | { | 583 | { | ||
| 594 | Key: "workspace.browsing_contexts.behavior", | 584 | Key: "workspace.browsing_contexts.behavior", | ||
M internal/infrastructure/config/schema_provider_test.go +2 -1
| @@ -34,7 +34,8 @@ func TestConfigurationReferenceCoversSchemaKeys(t *testing.T) { | |||
| 34 | 34 | // schemaKeysPendingProductDocs lists SchemaProvider keys intentionally omitted from | |
| 35 | 35 | // product docs until a dedicated docs task lands them. | |
| 36 | 36 | var schemaKeysPendingProductDocs = map[string]struct{}{ | |
| 37 | - | "workspace.page_mode.sequence_timeout_ms": {}, | |
| 37 | + | "workspace.page_mode.sequence_timeout_ms": {}, | |
| 38 | + | "workspace.page_mode.preload_accessibility": {}, | |
| 38 | 39 | } | |
| 39 | 40 | ||
| 40 | 41 | func configurationReferenceKeys(t *testing.T) map[string]struct{} { | |
| @@ -34,7 +34,8 @@ func TestConfigurationReferenceCoversSchemaKeys(t *testing.T) { | |||||
| 34 | // schemaKeysPendingProductDocs lists SchemaProvider keys intentionally omitted from | 34 | // schemaKeysPendingProductDocs lists SchemaProvider keys intentionally omitted from | ||
| 35 | // product docs until a dedicated docs task lands them. | 35 | // product docs until a dedicated docs task lands them. | ||
| 36 | var schemaKeysPendingProductDocs = map[string]struct{}{ | 36 | var schemaKeysPendingProductDocs = map[string]struct{}{ | ||
| 37 | - | "workspace.page_mode.sequence_timeout_ms": {}, | 37 | + | "workspace.page_mode.sequence_timeout_ms": {}, |
| 38 | + | "workspace.page_mode.preload_accessibility": {}, | |||
| 38 | } | 39 | } | ||
| 39 | 40 | ||||
| 40 | func configurationReferenceKeys(t *testing.T) map[string]struct{} { | 41 | func configurationReferenceKeys(t *testing.T) map[string]struct{} { | ||