1// Copyright 2019 Google Inc. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package button 16 17import ( 18 "fmt" 19 "image" 20 "testing" 21 22 "github.com/mum4k/termdash/mouse" 23 "github.com/mum4k/termdash/terminal/terminalapi" 24) 25 26// eventTestCase is one mouse event and the output expectation. 27type eventTestCase struct { 28 // area if specified, will be provided to UpdateArea *before* processing the event. 29 area *image.Rectangle 30 31 // event is the mouse event to send. 32 event *terminalapi.Mouse 33 34 // wantClick indicates whether we expect the FSM to recognize a mouse click. 35 wantClick bool 36 37 // wantState is the expected button state. 38 wantState State 39} 40 41func TestFSM(t *testing.T) { 42 tests := []struct { 43 desc string 44 button mouse.Button 45 area image.Rectangle 46 eventCases []*eventTestCase 47 }{ 48 { 49 desc: "tracks single left button click", 50 button: mouse.ButtonLeft, 51 area: image.Rect(0, 0, 1, 1), 52 eventCases: []*eventTestCase{ 53 { 54 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 55 wantClick: false, 56 wantState: Down, 57 }, 58 { 59 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 60 wantClick: true, 61 wantState: Up, 62 }, 63 }, 64 }, 65 { 66 desc: "updates area so the clicks falls outside", 67 button: mouse.ButtonLeft, 68 area: image.Rect(0, 0, 1, 1), 69 eventCases: []*eventTestCase{ 70 { 71 area: func() *image.Rectangle { 72 ar := image.Rect(1, 1, 2, 2) 73 return &ar 74 }(), 75 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 76 wantClick: false, 77 wantState: Up, 78 }, 79 { 80 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 81 wantClick: false, 82 wantState: Up, 83 }, 84 }, 85 }, 86 { 87 desc: "updates area before release, so the release falls outside", 88 button: mouse.ButtonLeft, 89 area: image.Rect(0, 0, 1, 1), 90 eventCases: []*eventTestCase{ 91 { 92 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 93 wantClick: false, 94 wantState: Down, 95 }, 96 { 97 area: func() *image.Rectangle { 98 ar := image.Rect(1, 1, 2, 2) 99 return &ar 100 }(), 101 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 102 wantClick: false, 103 wantState: Up, 104 }, 105 }, 106 }, 107 { 108 desc: "increased area makes the release count", 109 button: mouse.ButtonLeft, 110 area: image.Rect(0, 0, 1, 1), 111 eventCases: []*eventTestCase{ 112 { 113 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 114 wantClick: false, 115 wantState: Down, 116 }, 117 { 118 area: func() *image.Rectangle { 119 ar := image.Rect(0, 0, 2, 2) 120 return &ar 121 }(), 122 event: &terminalapi.Mouse{Position: image.Point{1, 1}, Button: mouse.ButtonRelease}, 123 wantClick: true, 124 wantState: Up, 125 }, 126 }, 127 }, 128 { 129 desc: "tracks single right button click", 130 button: mouse.ButtonRight, 131 area: image.Rect(0, 0, 1, 1), 132 eventCases: []*eventTestCase{ 133 { 134 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRight}, 135 wantClick: false, 136 wantState: Down, 137 }, 138 { 139 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 140 wantClick: true, 141 wantState: Up, 142 }, 143 }, 144 }, 145 { 146 desc: "ignores unrelated button in state wantPress", 147 button: mouse.ButtonLeft, 148 area: image.Rect(0, 0, 1, 1), 149 eventCases: []*eventTestCase{ 150 { 151 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRight}, 152 wantClick: false, 153 wantState: Up, 154 }, 155 { 156 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 157 wantClick: false, 158 wantState: Up, 159 }, 160 { 161 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 162 wantClick: false, 163 wantState: Down, 164 }, 165 { 166 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 167 wantClick: true, 168 wantState: Up, 169 }, 170 }, 171 }, 172 { 173 desc: "reverts to wantPress on unrelated button in state wantRelease", 174 button: mouse.ButtonLeft, 175 area: image.Rect(0, 0, 1, 1), 176 eventCases: []*eventTestCase{ 177 { 178 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 179 wantClick: false, 180 wantState: Down, 181 }, 182 { 183 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRight}, 184 wantClick: false, 185 wantState: Up, 186 }, 187 { 188 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 189 wantClick: false, 190 wantState: Up, 191 }, 192 }, 193 }, 194 { 195 desc: "reports button as down when the tracked button is pressed again in the area", 196 button: mouse.ButtonLeft, 197 area: image.Rect(0, 0, 1, 1), 198 eventCases: []*eventTestCase{ 199 { 200 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 201 wantClick: false, 202 wantState: Down, 203 }, 204 { 205 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 206 wantClick: false, 207 wantState: Down, 208 }, 209 { 210 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 211 wantClick: true, 212 wantState: Up, 213 }, 214 }, 215 }, 216 { 217 desc: "reports button as up when the tracked button is pressed again outside the area", 218 button: mouse.ButtonLeft, 219 area: image.Rect(0, 0, 1, 1), 220 eventCases: []*eventTestCase{ 221 { 222 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 223 wantClick: false, 224 wantState: Down, 225 }, 226 { 227 event: &terminalapi.Mouse{Position: image.Point{1, 1}, Button: mouse.ButtonLeft}, 228 wantClick: false, 229 wantState: Up, 230 }, 231 { 232 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 233 wantClick: false, 234 wantState: Down, 235 }, 236 { 237 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 238 wantClick: true, 239 wantState: Up, 240 }, 241 }, 242 }, 243 { 244 desc: "ignores clicks outside of area in state wantPress", 245 button: mouse.ButtonLeft, 246 area: image.Rect(0, 0, 1, 1), 247 eventCases: []*eventTestCase{ 248 { 249 event: &terminalapi.Mouse{Position: image.Point{1, 1}, Button: mouse.ButtonLeft}, 250 wantClick: false, 251 wantState: Up, 252 }, 253 { 254 event: &terminalapi.Mouse{Position: image.Point{1, 1}, Button: mouse.ButtonRelease}, 255 wantClick: false, 256 wantState: Up, 257 }, 258 { 259 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 260 wantClick: false, 261 wantState: Down, 262 }, 263 { 264 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 265 wantClick: true, 266 wantState: Up, 267 }, 268 }, 269 }, 270 { 271 desc: "release outside of area releases button too", 272 button: mouse.ButtonLeft, 273 area: image.Rect(0, 0, 1, 1), 274 eventCases: []*eventTestCase{ 275 { 276 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 277 wantClick: false, 278 wantState: Down, 279 }, 280 { 281 event: &terminalapi.Mouse{Position: image.Point{1, 1}, Button: mouse.ButtonRelease}, 282 wantClick: false, 283 wantState: Up, 284 }, 285 { 286 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, 287 wantClick: false, 288 wantState: Down, 289 }, 290 { 291 event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, 292 wantClick: true, 293 wantState: Up, 294 }, 295 }, 296 }, 297 } 298 299 for _, tc := range tests { 300 t.Run(fmt.Sprintf(tc.desc), func(t *testing.T) { 301 fsm := NewFSM(tc.button, tc.area) 302 for _, etc := range tc.eventCases { 303 if etc.area != nil { 304 fsm.UpdateArea(*etc.area) 305 } 306 307 gotClick, gotState := fsm.Event(etc.event) 308 t.Logf("Called fsm.Event(%v) => %v, %v", etc.event, gotClick, gotState) 309 if gotClick != etc.wantClick || gotState != etc.wantState { 310 t.Errorf("fsm.Event(%v) => %v, %v, want %v, %v", etc.event, gotClick, gotState, etc.wantClick, etc.wantState) 311 } 312 } 313 }) 314 } 315} 316