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