termdash/container/focus_test.go

1519 lines
35 KiB
Go

// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container
import (
"fmt"
"image"
"strings"
"testing"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/private/event"
"github.com/mum4k/termdash/private/event/testevent"
"github.com/mum4k/termdash/private/faketerm"
"github.com/mum4k/termdash/terminal/terminalapi"
)
// pointCase is a test case for the pointCont function.
type pointCase struct {
desc string
point image.Point
wantNil bool
wantColor cell.Color // expected container identified by its border color
}
func TestPointCont(t *testing.T) {
tests := []struct {
desc string
termSize image.Point
container func(ft *faketerm.Terminal) (*Container, error)
cases []pointCase
}{
{
desc: "single container, no border",
termSize: image.Point{3, 3},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
BorderColor(cell.ColorBlue),
)
},
cases: []pointCase{
{
desc: "inside the container",
point: image.Point{1, 1},
wantColor: cell.ColorBlue,
},
{
desc: "top left corner",
point: image.Point{0, 0},
wantColor: cell.ColorBlue,
},
{
desc: "top right corner",
point: image.Point{2, 0},
wantColor: cell.ColorBlue,
},
{
desc: "bottom left corner",
point: image.Point{0, 2},
wantColor: cell.ColorBlue,
},
{
desc: "bottom right corner",
point: image.Point{2, 2},
wantColor: cell.ColorBlue,
},
{
desc: "outside of the container, too large",
point: image.Point{3, 3},
wantNil: true,
},
{
desc: "outside of the container, too small",
point: image.Point{-1, -1},
wantNil: true,
},
},
},
{
desc: "single container, border",
termSize: image.Point{3, 3},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
BorderColor(cell.ColorBlue),
)
},
cases: []pointCase{
{
desc: "inside the container",
point: image.Point{1, 1},
wantColor: cell.ColorBlue,
},
{
desc: "on the border",
point: image.Point{0, 1},
wantColor: cell.ColorBlue,
},
},
},
{
desc: "split containers, parent has no border",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
BorderColor(cell.ColorBlack),
SplitVertical(
Left(
SplitHorizontal(
Top(
BorderColor(cell.ColorGreen),
),
Bottom(
BorderColor(cell.ColorWhite),
),
),
),
Right(
BorderColor(cell.ColorRed),
),
),
)
},
cases: []pointCase{
{
desc: "right sub container, inside corner",
point: image.Point{5, 5},
wantColor: cell.ColorRed,
},
{
desc: "right sub container, outside corner",
point: image.Point{9, 9},
wantColor: cell.ColorRed,
},
{
desc: "top left",
point: image.Point{0, 0},
wantColor: cell.ColorGreen,
},
{
desc: "bottom left",
point: image.Point{0, 9},
wantColor: cell.ColorWhite,
},
},
},
{
desc: "split containers, parent has border",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
BorderColor(cell.ColorBlack),
SplitVertical(
Left(
SplitHorizontal(
Top(
BorderColor(cell.ColorGreen),
),
Bottom(
BorderColor(cell.ColorWhite),
),
),
),
Right(
BorderColor(cell.ColorRed),
),
),
)
},
cases: []pointCase{
{
desc: "right sub container, inside corner",
point: image.Point{5, 5},
wantColor: cell.ColorRed,
},
{
desc: "top right corner focuses parent",
point: image.Point{9, 9},
wantColor: cell.ColorBlack,
},
{
desc: "right sub container, outside corner",
point: image.Point{8, 8},
wantColor: cell.ColorRed,
},
{
desc: "top left focuses parent",
point: image.Point{0, 0},
wantColor: cell.ColorBlack,
},
{
desc: "top left sub container",
point: image.Point{1, 1},
wantColor: cell.ColorGreen,
},
{
desc: "bottom left focuses parent",
point: image.Point{0, 9},
wantColor: cell.ColorBlack,
},
{
desc: "bottom left sub container",
point: image.Point{1, 8},
wantColor: cell.ColorWhite,
},
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
ft, err := faketerm.New(tc.termSize)
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
cont, err := tc.container(ft)
if err != nil {
t.Fatalf("tc.container => unexpected error: %v", err)
}
// Initial draw to determine sizes of containers.
if err := cont.Draw(); err != nil {
t.Fatalf("Draw => unexpected error: %v", err)
}
for _, pc := range tc.cases {
gotCont := pointCont(cont, pc.point)
if (gotCont == nil) != pc.wantNil {
t.Errorf("%s, pointCont%v => got %v, wantNil: %v", pc.desc, pc.point, gotCont, pc.wantNil)
}
if gotCont == nil {
continue
}
gotColor := gotCont.opts.inherited.borderColor
if gotColor != pc.wantColor {
t.Errorf("%s, pointCont%v => got container with border color %v, want %v", pc.desc, pc.point, gotColor, pc.wantColor)
}
}
})
}
}
// contLocIntro3 prints out an introduction explaining the used container
// locations on test failures.
func contLocIntro3() string {
var s strings.Builder
s.WriteString("Container locations refer to containers in the following tree, i.e. contLocA is the root container:\n")
s.WriteString(`
A
/ \
B C
`)
return s.String()
}
// contLocIntro5 prints out an introduction explaining the used container
// locations on test failures.
func contLocIntro5() string {
var s strings.Builder
s.WriteString("Container locations refer to containers in the following tree, i.e. contLocA is the root container:\n")
s.WriteString(`
A
/ \
B C
/ \
D E
`)
return s.String()
}
// contLoc is used in tests to indicate the desired location of a container.
type contLoc int
// String implements fmt.Stringer()
func (cl contLoc) String() string {
if n, ok := contLocNames[cl]; ok {
return n
}
return "contLocUnknown"
}
// contLocNames maps contLoc values to human readable names.
var contLocNames = map[contLoc]string{
contLocA: "contLocA",
contLocB: "contLocB",
contLocC: "contLocC",
contLocD: "contLocD",
contLocE: "contLocE",
}
const (
contLocUnknown contLoc = iota
contLocA
contLocB
contLocC
contLocD
contLocE
)
func TestFocusTrackerMouse(t *testing.T) {
t.Log(contLocIntro3())
ft, err := faketerm.New(image.Point{10, 10})
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
var (
insideB = image.Point{1, 1}
insideC = image.Point{6, 6}
)
tests := []struct {
desc string
// Can be either the mouse event or a time.Duration to pause for.
events []*terminalapi.Mouse
wantFocused contLoc
wantProcessed int
}{
{
desc: "initially the root is focused",
wantFocused: contLocA,
},
{
desc: "click and release moves focus to the left",
events: []*terminalapi.Mouse{
{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
{Position: image.Point{1, 1}, Button: mouse.ButtonRelease},
},
wantFocused: contLocB,
wantProcessed: 2,
},
{
desc: "click and release moves focus to the right",
events: []*terminalapi.Mouse{
{Position: image.Point{5, 5}, Button: mouse.ButtonLeft},
{Position: image.Point{6, 6}, Button: mouse.ButtonRelease},
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "click in the same container is a no-op",
events: []*terminalapi.Mouse{
{Position: insideC, Button: mouse.ButtonLeft},
{Position: insideC, Button: mouse.ButtonRelease},
{Position: insideC, Button: mouse.ButtonLeft},
{Position: insideC, Button: mouse.ButtonRelease},
},
wantFocused: contLocC,
wantProcessed: 4,
},
{
desc: "click in the same container and release never happens",
events: []*terminalapi.Mouse{
{Position: insideC, Button: mouse.ButtonLeft},
{Position: insideB, Button: mouse.ButtonLeft},
{Position: insideB, Button: mouse.ButtonRelease},
},
wantFocused: contLocB,
wantProcessed: 3,
},
{
desc: "click in the same container, release elsewhere",
events: []*terminalapi.Mouse{
{Position: insideC, Button: mouse.ButtonLeft},
{Position: insideB, Button: mouse.ButtonRelease},
},
wantFocused: contLocA,
wantProcessed: 2,
},
{
desc: "other buttons are ignored",
events: []*terminalapi.Mouse{
{Position: insideB, Button: mouse.ButtonMiddle},
{Position: insideB, Button: mouse.ButtonRelease},
{Position: insideB, Button: mouse.ButtonRight},
{Position: insideB, Button: mouse.ButtonRelease},
{Position: insideB, Button: mouse.ButtonWheelUp},
{Position: insideB, Button: mouse.ButtonWheelDown},
},
wantFocused: contLocA,
wantProcessed: 6,
},
{
desc: "moving mouse with pressed button and then releasing moves focus",
events: []*terminalapi.Mouse{
{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
{Position: image.Point{1, 1}, Button: mouse.ButtonLeft},
{Position: image.Point{2, 2}, Button: mouse.ButtonRelease},
},
wantFocused: contLocB,
wantProcessed: 3,
},
{
desc: "click ignored if followed by another click of the same button elsewhere",
events: []*terminalapi.Mouse{
{Position: insideC, Button: mouse.ButtonLeft},
{Position: insideB, Button: mouse.ButtonLeft},
{Position: insideC, Button: mouse.ButtonRelease},
},
wantFocused: contLocA,
wantProcessed: 3,
},
{
desc: "click ignored if followed by another click of a different button",
events: []*terminalapi.Mouse{
{Position: insideC, Button: mouse.ButtonLeft},
{Position: insideC, Button: mouse.ButtonMiddle},
{Position: insideC, Button: mouse.ButtonRelease},
},
wantFocused: contLocA,
wantProcessed: 3,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
root, err := New(
ft,
SplitVertical(
Left(),
Right(),
),
)
if err != nil {
t.Fatalf("New => unexpected error: %v", err)
}
eds := event.NewDistributionSystem()
root.Subscribe(eds)
// Initial draw to determine sizes of containers.
if err := root.Draw(); err != nil {
t.Fatalf("Draw => unexpected error: %v", err)
}
for _, ev := range tc.events {
eds.Event(ev)
}
if err := testevent.WaitFor(5*time.Second, func() error {
if got, want := eds.Processed(), tc.wantProcessed; got != want {
return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
}
return nil
}); err != nil {
t.Fatalf("testevent.WaitFor => %v", err)
}
var wantFocused *Container
switch wf := tc.wantFocused; wf {
case contLocA:
wantFocused = root
case contLocB:
wantFocused = root.first
case contLocC:
wantFocused = root.second
default:
t.Fatalf("unsupported wantFocused value => %v", wf)
}
if !root.focusTracker.isActive(wantFocused) {
t.Errorf("isActive(%v) => false, want true, status: contLocA(%v):%v, contLocB(%v):%v, contLocC(%v):%v",
tc.wantFocused,
contLocA, root.focusTracker.isActive(root),
contLocB, root.focusTracker.isActive(root.first),
contLocC, root.focusTracker.isActive(root.second),
)
}
})
}
}
// contDir represents a direction in which we want to change container focus.
type contDir int
// String implements fmt.Stringer()
func (cd contDir) String() string {
if n, ok := contDirNames[cd]; ok {
return n
}
return "contDirUnknown"
}
// contDirNames maps contDir values to human readable names.
var contDirNames = map[contDir]string{
contDirNext: "contDirNext",
contDirPrevious: "contDirPrevious",
}
const (
contDirUnknown contDir = iota
contDirNext
contDirPrevious
)
// contSize determines the size of the container used in the test.
type contSize int
const (
contSize3 contSize = iota
contSize5
)
func TestFocusTrackerNextAndPrevious(t *testing.T) {
ft, err := faketerm.New(image.Point{10, 10})
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
const (
keyNext keyboard.Key = keyboard.KeyTab
keyPrevious keyboard.Key = '~'
)
tests := []struct {
desc string
contSize contSize
container func(ft *faketerm.Terminal) (*Container, error)
events []*terminalapi.Keyboard
wantFocused contLoc
wantProcessed int
}{
{
desc: "initially the root is focused by default",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
wantFocused: contLocA,
},
{
desc: "focus root explicitly",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Focused(),
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
wantFocused: contLocA,
},
{
desc: "focus can be set to a container other than root",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(Focused()),
Right(),
),
KeyFocusNext(keyNext),
)
},
wantFocused: contLocB,
},
{
desc: "option Focused used on multiple containers, the last one takes effect",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(Focused()),
Right(Focused()),
),
KeyFocusNext(keyNext),
)
},
wantFocused: contLocC,
},
{
desc: "keyNext does nothing when only root exists",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
},
wantFocused: contLocA,
wantProcessed: 1,
},
{
desc: "keyNext focuses the first container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
},
wantFocused: contLocB,
wantProcessed: 1,
},
{
desc: "two keyNext presses focuses the second container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
{Key: keyNext},
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "three keyNext presses focuses the first container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
{Key: keyNext},
{Key: keyNext},
},
wantFocused: contLocB,
wantProcessed: 3,
},
{
desc: "four keyNext presses focuses the second container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
{Key: keyNext},
{Key: keyNext},
{Key: keyNext},
},
wantFocused: contLocC,
wantProcessed: 4,
},
{
desc: "five keyNext presses focuses the first container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
{Key: keyNext},
{Key: keyNext},
{Key: keyNext},
{Key: keyNext},
},
wantFocused: contLocB,
wantProcessed: 5,
},
{
desc: "keyPrevious does nothing when only root exists",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
},
wantFocused: contLocA,
wantProcessed: 1,
},
{
desc: "keyPrevious focuses the last container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
},
wantFocused: contLocC,
wantProcessed: 1,
},
{
desc: "two keyPrevious presses focuses the first container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
{Key: keyPrevious},
},
wantFocused: contLocB,
wantProcessed: 2,
},
{
desc: "three keyPrevious presses focuses the second container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
{Key: keyPrevious},
{Key: keyPrevious},
},
wantFocused: contLocC,
wantProcessed: 3,
},
{
desc: "four keyPrevious presses focuses the first container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
{Key: keyPrevious},
{Key: keyPrevious},
{Key: keyPrevious},
},
wantFocused: contLocB,
wantProcessed: 4,
},
{
desc: "five keyPrevious presses focuses the second container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
{Key: keyPrevious},
{Key: keyPrevious},
{Key: keyPrevious},
{Key: keyPrevious},
},
wantFocused: contLocC,
wantProcessed: 5,
},
{
desc: "first container requests to be skipped on key based focus changes, using next",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(
KeyFocusSkip(),
),
Right(),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
},
wantFocused: contLocC,
wantProcessed: 1,
},
{
desc: "last container requests to be skipped on key based focus changes, using next",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(
KeyFocusSkip(),
),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
{Key: keyNext},
},
wantFocused: contLocB,
wantProcessed: 2,
},
{
desc: "all containers request to be skipped on key based focus changes, using next",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(
KeyFocusSkip(),
),
Right(
KeyFocusSkip(),
),
),
KeyFocusNext(keyNext),
)
},
events: []*terminalapi.Keyboard{
{Key: keyNext},
},
wantFocused: contLocA,
wantProcessed: 1,
},
{
desc: "first container requests to be skipped on key based focus changes, using previous",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(
KeyFocusSkip(),
),
Right(),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
{Key: keyPrevious},
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "last container requests to be skipped on key based focus changes, using previous",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(
KeyFocusSkip(),
),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
},
wantFocused: contLocB,
wantProcessed: 1,
},
{
desc: "all containers request to be skipped on key based focus changes, using previous",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(
KeyFocusSkip(),
),
Right(
KeyFocusSkip(),
),
),
KeyFocusPrevious(keyPrevious),
)
},
events: []*terminalapi.Keyboard{
{Key: keyPrevious},
},
wantFocused: contLocA,
wantProcessed: 1,
},
{
desc: "containers don't belong to focus group by default",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
SplitVertical(
Left(),
Right(),
),
KeyFocusGroupsNext('n', 0),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
},
wantFocused: contLocA,
wantProcessed: 1,
},
{
desc: "moves to the next container in focus group, pressing KeysFocusGroupNext once focuses the first container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusGroups(1),
),
Right(
KeyFocusGroups(1),
),
),
KeyFocusGroupsNext('n', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
},
wantFocused: contLocB,
wantProcessed: 1,
},
{
desc: "moves to the next container in focus group, pressing KeysFocusGroupNext twice focuses the second container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusGroups(1),
),
Right(
KeyFocusGroups(1),
),
),
KeyFocusGroupsNext('n', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
{Key: 'n'},
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "moves to the next container in focus group, pressing KeysFocusGroupNext three times focuses the first container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(2),
SplitVertical(
Left(
KeyFocusGroups(2),
),
Right(
KeyFocusGroups(2),
),
),
KeyFocusGroupsNext('n', 2),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
{Key: 'n'},
{Key: 'n'},
},
wantFocused: contLocB,
wantProcessed: 3,
},
{
desc: "moves to the previous container in focus group, pressing KeysFocusGroupPrevious once focuses the second container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusGroups(1),
),
Right(
KeyFocusGroups(1),
),
),
KeyFocusGroupsPrevious('p', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'p'},
},
wantFocused: contLocC,
wantProcessed: 1,
},
{
desc: "moves to the previous container in focus group, pressing KeysFocusGroupPrevious twice focuses the first container",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusGroups(1),
),
Right(
KeyFocusGroups(1),
),
),
KeyFocusGroupsPrevious('p', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'p'},
{Key: 'p'},
},
wantFocused: contLocB,
wantProcessed: 2,
},
{
desc: "moves to the previous container in focus group, pressing KeysFocusGroupPrevious three times focuses the second container again",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusGroups(1),
),
Right(
KeyFocusGroups(1),
),
),
KeyFocusGroupsPrevious('p', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'p'},
{Key: 'p'},
{Key: 'p'},
},
wantFocused: contLocC,
wantProcessed: 3,
},
{
desc: "configuring container with KeyFocusSkip has no effect on a closed focus group",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusSkip(),
KeyFocusGroups(1),
),
Right(
KeyFocusSkip(),
KeyFocusGroups(1),
),
),
KeyFocusGroupsNext('n', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
},
wantFocused: contLocB,
wantProcessed: 1,
},
{
desc: "a focus group can have multiple keys configured for next",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusSkip(),
KeyFocusGroups(1),
),
Right(
KeyFocusSkip(),
KeyFocusGroups(1),
),
),
KeyFocusGroupsNext('n', 1),
KeyFocusGroupsNext(keyboard.KeyArrowRight, 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
{Key: keyboard.KeyArrowRight},
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "a focus group can have multiple keys configured for previous",
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
KeyFocusGroups(1),
SplitVertical(
Left(
KeyFocusGroups(1),
),
Right(
KeyFocusGroups(1),
),
),
KeyFocusGroupsPrevious('n', 1),
KeyFocusGroupsPrevious(keyboard.KeyArrowRight, 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'},
{Key: keyboard.KeyArrowRight},
},
wantFocused: contLocB,
wantProcessed: 2,
},
{
desc: "a container can be in multiple focus groups, rotates within group while on next",
contSize: contSize5,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
KeyFocusGroups(1),
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
SplitVertical(
Left( // contLocD
KeyFocusGroups(1),
),
Right( // contLocE
KeyFocusGroups(1, 2),
),
),
),
Right( // contLocC
KeyFocusGroups(1, 2),
),
),
KeyFocusGroupsNext('n', 1),
KeyFocusGroupsNext(keyboard.KeyArrowRight, 2),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocD
{Key: 'n'}, // focuses contLocE
{Key: keyboard.KeyArrowRight}, // focuses contLocC
{Key: keyboard.KeyArrowRight}, // rotates focus to contLocE
},
wantFocused: contLocE,
wantProcessed: 4,
},
{
desc: "a container can be in multiple focus groups, rotates within group while on previous",
contSize: contSize5,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
KeyFocusGroups(1),
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
SplitVertical(
Left( // contLocD
KeyFocusGroups(1),
),
Right( // contLocE
KeyFocusGroups(1, 2),
),
),
),
Right( // contLocC
KeyFocusGroups(1, 2),
),
),
KeyFocusGroupsPrevious('n', 1),
KeyFocusGroupsPrevious(keyboard.KeyArrowLeft, 2),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocC
{Key: keyboard.KeyArrowLeft}, // focuses contLocE
{Key: keyboard.KeyArrowLeft}, // rotates focus back to contLocC
},
wantFocused: contLocC,
wantProcessed: 3,
},
{
desc: "same key and group, first group takes priority, group 1 is first",
contSize: contSize5,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
KeyFocusGroups(1),
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
SplitVertical(
Left( // contLocD
KeyFocusGroups(1, 2),
),
Right( // contLocE
KeyFocusGroups(1),
),
),
),
Right( // contLocC
KeyFocusGroups(1, 2),
),
),
KeyFocusGroupsNext('n', 1),
KeyFocusGroupsNext('n', 2),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocD
{Key: 'n'}, // focuses contLocE
},
wantFocused: contLocE,
wantProcessed: 2,
},
{
desc: "same key and group, first group takes priority, group 2 is first",
contSize: contSize5,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
KeyFocusGroups(1),
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
SplitVertical(
Left( // contLocD
KeyFocusGroups(2, 1),
),
Right( // contLocE
KeyFocusGroups(1),
),
),
),
Right( // contLocC
KeyFocusGroups(1, 2),
),
),
KeyFocusGroupsNext('n', 1),
KeyFocusGroupsNext('n', 2),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocD
{Key: 'n'}, // focuses contLocC
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "KeyFocusGroups called multiple times, same key and group, first group takes priority, group 2 is first",
contSize: contSize5,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
KeyFocusGroups(1),
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
SplitVertical(
Left( // contLocD
KeyFocusGroups(2),
KeyFocusGroups(1),
),
Right( // contLocE
KeyFocusGroups(1),
),
),
),
Right( // contLocC
KeyFocusGroups(1, 2),
),
),
KeyFocusGroupsNext('n', 1),
KeyFocusGroupsNext('n', 2),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocD
{Key: 'n'}, // focuses contLocC
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "global KeyFocusNext moves focus out of a focus group",
contSize: contSize3,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
),
Right( // contLocC
),
),
KeyFocusNext('n'),
KeyFocusGroupsNext(keyboard.KeyArrowRight, 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocB in focus group 1
{Key: 'n'}, // focuses contLocC
},
wantFocused: contLocC,
wantProcessed: 2,
},
{
desc: "global KeyFocusPrevious moves focus out of a focus group",
contSize: contSize3,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
SplitVertical(
Left( // contLocB
),
Right( // contLocC
KeyFocusGroups(1),
),
),
KeyFocusPrevious('p'),
KeyFocusGroupsPrevious(keyboard.KeyArrowLeft, 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'p'}, // focuses contLocC in focus group 1
{Key: 'p'}, // focuses contLocB
},
wantFocused: contLocB,
wantProcessed: 2,
},
{
desc: "KeyFocusGroups with no arguments removes all groups",
contSize: contSize5,
container: func(ft *faketerm.Terminal) (*Container, error) {
return New( // contLocA
ft,
KeyFocusGroups(1),
SplitVertical(
Left( // contLocB
KeyFocusGroups(1),
SplitVertical(
Left( // contLocD
KeyFocusGroups(1),
),
Right( // contLocE
KeyFocusGroups(1),
KeyFocusGroups(),
),
),
),
Right( // contLocC
KeyFocusGroups(1),
),
),
KeyFocusGroupsNext('n', 1),
)
},
events: []*terminalapi.Keyboard{
{Key: 'n'}, // focuses contLocD
{Key: 'n'}, // focuses contLocC
},
wantFocused: contLocC,
wantProcessed: 2,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
root, err := tc.container(ft)
if err != nil {
t.Fatalf("tc.container => unexpected error: %v", err)
}
eds := event.NewDistributionSystem()
root.Subscribe(eds)
for _, ev := range tc.events {
eds.Event(ev)
}
if err := testevent.WaitFor(5*time.Second, func() error {
if got, want := eds.Processed(), tc.wantProcessed; got != want {
return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
}
return nil
}); err != nil {
t.Fatalf("testevent.WaitFor => %v", err)
}
var wantFocused *Container
switch wf := tc.wantFocused; wf {
case contLocA:
wantFocused = root
case contLocB:
wantFocused = root.first
case contLocC:
wantFocused = root.second
case contLocD:
wantFocused = root.first.first
case contLocE:
wantFocused = root.first.second
default:
t.Fatalf("unsupported wantFocused value => %v", wf)
}
switch tc.contSize {
case contSize3:
t.Log(contLocIntro3())
if !root.focusTracker.isActive(wantFocused) {
t.Errorf("isActive(%v) => false, want true, status: %v:%v, %v:%v, %v:%v",
tc.wantFocused,
contLocA, root.focusTracker.isActive(root),
contLocB, root.focusTracker.isActive(root.first),
contLocC, root.focusTracker.isActive(root.second),
)
}
case contSize5:
t.Log(contLocIntro5())
if !root.focusTracker.isActive(wantFocused) {
t.Errorf("isActive(%v) => false, want true, status: %v:%v, %v:%v, %v:%v, %v:%v, %v:%v",
tc.wantFocused,
contLocA, root.focusTracker.isActive(root),
contLocB, root.focusTracker.isActive(root.first),
contLocC, root.focusTracker.isActive(root.second),
contLocD, root.focusTracker.isActive(root.first.first),
contLocE, root.focusTracker.isActive(root.first.second),
)
}
default:
t.Errorf("unknown contSize: %v", tc.contSize)
}
})
}
}