gitstack

grindlemire/go-tui code browser

3.9 KB Go 136 lines 2026-03-15 · 73fe47e raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package tui

import "testing"

func TestDispatchTable_FocusRequired(t *testing.T) {
	type tc struct {
		focusedID      string // which mock to focus ("" = none)
		pressRune      rune
		expectAppQuit  bool
		expectInserted bool
	}

	tests := map[string]tc{
		"unfocused input: app handler fires, input skipped": {
			focusedID:      "",
			pressRune:      'q',
			expectAppQuit:  true,
			expectInserted: false,
		},
		"focused input: focus-gated handler takes priority over broadcast": {
			focusedID:      "input",
			pressRune:      'q',
			expectAppQuit:  false, // broadcast handler skipped (focus-gated stop handler has priority)
			expectInserted: true,  // focus-gated handler fires exclusively
		},
		"focused input: non-matching app key still works": {
			focusedID:      "input",
			pressRune:      0, // will use KeyEscape instead
			expectAppQuit:  true,
			expectInserted: false,
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			appQuit := false
			charInserted := false

			fm := newFocusManager()
			inputMock := newMockFocusable("input", true)
			fm.Register(inputMock)

			if tt.focusedID == "input" {
				fm.SetFocus(inputMock)
			}

			// Build entries manually to test dispatch logic
			table := &dispatchTable{}

			// App-level broadcast binding: 'q' quits (no FocusRequired)
			table.entries = append(table.entries, dispatchEntry{
				pattern:  KeyPattern{Rune: 'q'},
				handler:  func(ke KeyEvent) { appQuit = true },
				stop:     false,
				position: 0,
			})

			// App-level broadcast binding: Escape quits (no FocusRequired)
			table.entries = append(table.entries, dispatchEntry{
				pattern:  KeyPattern{Key: KeyEscape, ExcludeMods: ModCtrl | ModAlt | ModShift},
				handler:  func(ke KeyEvent) { appQuit = true },
				stop:     false,
				position: 0,
			})

			// Input focus-gated binding: any rune inserts (FocusRequired)
			table.entries = append(table.entries, dispatchEntry{
				pattern:    KeyPattern{AnyRune: true, FocusRequired: true},
				handler:    func(ke KeyEvent) { charInserted = true },
				stop:       true,
				position:   1,
				focusCheck: inputMock.IsFocused,
			})

			var ke KeyEvent
			if tt.pressRune != 0 {
				ke = KeyEvent{Key: KeyRune, Rune: tt.pressRune}
			} else {
				ke = KeyEvent{Key: KeyEscape}
			}

			table.dispatch(ke)

			if appQuit != tt.expectAppQuit {
				t.Errorf("appQuit = %v, want %v", appQuit, tt.expectAppQuit)
			}
			if charInserted != tt.expectInserted {
				t.Errorf("charInserted = %v, want %v", charInserted, tt.expectInserted)
			}
		})
	}
}

func TestDispatchTable_ValidateFocusRequired(t *testing.T) {
	type tc struct {
		entries   []dispatchEntry
		expectErr bool
	}

	tests := map[string]tc{
		"two focus-gated stop handlers do not conflict": {
			entries: []dispatchEntry{
				{pattern: KeyPattern{AnyRune: true, FocusRequired: true}, stop: true, position: 0, focusCheck: newMockFocusable("a", true).IsFocused},
				{pattern: KeyPattern{AnyRune: true, FocusRequired: true}, stop: true, position: 1, focusCheck: newMockFocusable("b", true).IsFocused},
			},
			expectErr: false,
		},
		"focus-gated and broadcast stop handlers do not conflict": {
			entries: []dispatchEntry{
				{pattern: KeyPattern{AnyRune: true, FocusRequired: true}, stop: true, position: 0, focusCheck: newMockFocusable("a", true).IsFocused},
				{pattern: KeyPattern{AnyRune: true}, stop: true, position: 1},
			},
			expectErr: false,
		},
		"two broadcast stop handlers still conflict": {
			entries: []dispatchEntry{
				{pattern: KeyPattern{AnyRune: true}, stop: true, position: 0},
				{pattern: KeyPattern{AnyRune: true}, stop: true, position: 1},
			},
			expectErr: true,
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			table := &dispatchTable{entries: tt.entries}
			err := table.validate()
			if tt.expectErr && err == nil {
				t.Error("expected validation error, got nil")
			}
			if !tt.expectErr && err != nil {
				t.Errorf("unexpected validation error: %v", err)
			}
		})
	}
}