Files
MujicaUI/layout/status_bar_test.go

326 lines
9.4 KiB
Go

package layout
import (
"fmt"
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
func statusBarSample() StatusBarOptions {
return StatusBarOptions{
Left: []StatusItem{
{ID: "branch", Text: "main", Icon: icons.Must("folder")},
{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4},
{ID: "lint", Text: "2 warnings", Tone: StatusItemWarning, Secondary: true},
},
Right: []StatusItem{
{ID: "enc", Text: "UTF-8", Secondary: true},
{ID: "pos", Text: "Ln 12, Col 4", Secondary: true},
{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true},
},
}
}
func TestStatusBarActionClick(t *testing.T) {
var clicked []string
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
if r := StatusBar(c, statusBarSample()); r.Clicked != "" {
clicked = append(clicked, r.Clicked)
}
}, 800, 100)
if err := tt.Click("Notifications"); err != nil {
t.Fatal(err)
}
_ = tt.Click("main")
if len(clicked) != 1 || clicked[0] != "bell" {
t.Fatalf("clicked %v, want [bell]", clicked)
}
}
func TestStatusBarActionKeyboard(t *testing.T) {
n := 0
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
if StatusBar(c, statusBarSample()).Clicked == "bell" {
n++
}
}, 800, 100)
tt.Key(0, ui.KeyTab)
if !tt.Focused("Notifications") {
t.Fatal("Tab did not reach the action")
}
tt.Key(0, ui.KeyEnter)
tt.Key(0, ui.KeySpace)
if n != 2 {
t.Fatalf("keyboard activations %d, want 2", n)
}
}
func TestStatusBarCollapsesSecondary(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
StatusBar(c, statusBarSample())
}, 800, 100)
for _, s := range []string{"main", "Syncing", "2 warnings", "UTF-8", "Ln 12, Col 4"} {
if !tt.HasText(s) {
t.Fatalf("wide bar lost %q", s)
}
}
tt.SetSize(330, 100)
for _, s := range []string{"Ln 12, Col 4", "UTF-8", "2 warnings"} {
if tt.HasText(s) {
t.Fatalf("narrow bar still shows secondary %q; texts %q", s, tt.Texts())
}
}
if !tt.HasText("main") || !tt.HasText("Syncing") {
t.Fatalf("narrow bar hid primary items: %q", tt.Texts())
}
if !tt.HasText("3 more items") {
t.Fatalf("no hidden count named: %q", tt.Texts())
}
// Medium width drops only the last secondary items.
tt.SetSize(400, 100)
if !tt.HasText("2 warnings") || tt.HasText("Ln 12, Col 4") {
t.Fatalf("medium bar hid the wrong items: %q", tt.Texts())
}
// At every width the visible items stay inside the bar.
for w := 320; w <= 800; w += 10 {
tt.SetSize(w, 100)
bell, ok := tt.Find("Notifications")
if !ok || bell.X+bell.W > float32(w)-8+0.5 || bell.W < 28 {
t.Fatalf("width %d: action at %+v", w, bell)
}
}
tt.SetSize(800, 100)
if !tt.HasText("UTF-8") {
t.Fatal("secondary items did not come back when wide")
}
}
func TestStatusBarManyActionsKeepMinimumWidth(t *testing.T) {
var o StatusBarOptions
for i := range 16 {
o.Right = append(o.Right, StatusItem{ID: fmt.Sprint(i), Label: fmt.Sprintf("Action %d", i),
Icon: icons.Must("bell"), Action: true, Secondary: i >= 8})
}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
StatusBar(c, o)
}, 560, 100)
shown := 0
for i := range 16 {
r, ok := tt.Find(fmt.Sprintf("Action %d", i))
if !ok {
continue
}
shown++
if r.W < 28 || r.X+r.W > 560-8+0.5 {
t.Fatalf("Action %d at %+v: narrower than 28 or past the bar", i, r)
}
}
if shown == 16 {
t.Fatal("no secondary action folded")
}
if _, ok := tt.Find(fmt.Sprintf("%d more items", 16-shown)); !ok {
t.Fatalf("no button for the %d hidden actions", 16-shown)
}
}
func TestStatusBarHiddenActionsByKeyboard(t *testing.T) {
var clicked []string
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
if id := StatusBar(c, StatusBarOptions{
Left: []StatusItem{{ID: "main", Text: "A long primary status text that fills the bar"}},
Right: []StatusItem{{ID: "fmt", Text: "Format", Action: true, Secondary: true}},
}).Clicked; id != "" {
clicked = append(clicked, id)
}
}, 300, 300)
if tt.HasText("Format") {
t.Fatal("secondary action not folded")
}
tt.Key(0, ui.KeyTab)
if !tt.Focused("1 more items") {
t.Fatal("Tab did not reach the hidden-items button")
}
tt.Key(0, ui.KeyEnter)
if !tt.HasText("Format") {
t.Fatal("Enter did not open the hidden items")
}
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnter)
if len(clicked) != 1 || clicked[0] != "fmt" {
t.Fatalf("clicked %v, want [fmt]", clicked)
}
if tt.HasText("Format") {
t.Fatal("running a hidden action did not close the list")
}
if !tt.Focused("1 more items") {
t.Fatal("focus did not return to the hidden-items button")
}
tt.Key(0, ui.KeyEnter)
tt.Key(0, ui.KeyEscape)
if tt.HasText("Format") || !tt.Focused("1 more items") {
t.Fatal("Escape did not close the list back to its button")
}
}
func TestStatusBarIconSize(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
StatusBar(c, StatusBarOptions{Left: []StatusItem{{ID: "i", Label: "Online", Icon: icons.Must("wifi-off")}}})
}, 400, 100)
// Padding of one unit each side around a 16 DIP icon.
if r, _ := tt.Find("Online"); r.W != 24 {
t.Fatalf("icon item width %v, want 24", r.W)
}
}
func TestStatusBarTextScale(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
StatusBar(c, StatusBarOptions{Left: []StatusItem{{ID: "r", Text: "Ready"}}})
}, 400, 200)
plain, _ := tt.Find("Ready")
tt.SetPreferences(ui.Preferences{TextScale: 2})
tt.Frame()
big, _ := tt.Find("Ready")
bar, _ := tt.Find("Status bar")
if big.H < 1.8*plain.H {
t.Fatalf("text height %v at 200%%, was %v", big.H, plain.H)
}
if bar.H < big.H {
t.Fatalf("bar %v shorter than its text %v", bar.H, big.H)
}
}
func TestStatusBarRejectsBadItems(t *testing.T) {
for _, o := range []StatusBarOptions{
{Left: []StatusItem{{Text: "no id"}}},
{Left: []StatusItem{{ID: "a", Text: "a"}}, Right: []StatusItem{{ID: "a", Text: "b"}}},
{Left: []StatusItem{{ID: "p", Text: "p", ShowProgress: true, Progress: 1.5}}},
{Left: []StatusItem{{ID: "n", Icon: icons.Must("bell")}}},
} {
func() {
defer func() {
if recover() == nil {
t.Errorf("%+v did not panic", o)
}
}()
ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
StatusBar(c, o)
}, 400, 100)
}()
}
}
func TestStatusBarSnapshot(t *testing.T) {
snapshot(t, "status_bar", 560, 60, func(c *ui.Context) {
ui.Column(c).Fill().Justify(ui.End).Children(func() {
o := statusBarSample()
o.Left = append(o.Left, StatusItem{ID: "ok", Text: "Saved", Tone: StatusItemSuccess, Icon: icons.Must("check")})
StatusBar(c, o)
})
})
}
func TestStatusBarFoldsOnResizeAlone(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
StatusBar(c, statusBarSample())
}, 400, 100)
tt.SetSize(160, 100)
for _, s := range []string{"2 warnings", "UTF-8", "Ln 12, Col 4"} {
if tt.HasText(s) {
t.Fatalf("160 DIP bar still shows %q", s)
}
}
if !tt.HasText("3 more items") {
t.Fatalf("no hidden count: %q", tt.Texts())
}
}
func TestStatusBarKeepsFocusAcrossDensity(t *testing.T) {
density := theme.Default
n := 0
// At 155 DIP the secondary item fits at Default and folds into "+1" at Comfortable.
o := StatusBarOptions{
Left: []StatusItem{{ID: "main", Text: "main"}, {ID: "enc", Text: "UTF-8", Secondary: true}},
Right: []StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}},
}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{Density: density})
if StatusBar(c, o).Clicked == "bell" {
n++
}
}, 155, 100)
if !tt.HasText("UTF-8") {
t.Fatal("secondary item folded at Default density")
}
for range 4 {
if tt.Focused("Notifications") {
break
}
tt.Key(0, ui.KeyTab)
}
if !tt.Focused("Notifications") {
t.Fatal("Tab never reached Notifications")
}
density = theme.Comfortable
tt.Frame()
if tt.HasText("UTF-8") || !tt.HasText("1 more items") {
t.Fatalf("Comfortable did not fold the secondary item: %q", tt.Texts())
}
if !tt.Focused("Notifications") {
t.Fatal("density switch lost the focus")
}
tt.Key(0, ui.KeyEnter)
if n != 1 {
t.Fatalf("Enter after the switch activated %d times, want 1", n)
}
}
func TestStatusBarFocusMovesToMoreWhenFocusedItemFolds(t *testing.T) {
density := theme.Default
var clicked []string
o := StatusBarOptions{
Left: []StatusItem{{ID: "main", Text: "main"}, {ID: "fmt", Text: "Format", Action: true, Secondary: true}},
Right: []StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}},
}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{Density: density})
if id := StatusBar(c, o).Clicked; id != "" {
clicked = append(clicked, id)
}
}, 152, 300)
tt.Key(0, ui.KeyTab)
if !tt.Focused("Format") {
t.Fatal("Tab did not focus Format at Default density")
}
density = theme.Comfortable
tt.Frame()
if tt.HasText("Format") {
t.Fatal("Format did not fold at Comfortable density")
}
if !tt.Focused("1 more items") {
t.Fatal("focus did not move to the +N button that holds Format")
}
tt.Key(0, ui.KeyEnter)
if !tt.HasText("Format") {
t.Fatal("Enter on +N did not open the list with Format")
}
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnter)
if len(clicked) != 1 || clicked[0] != "fmt" {
t.Fatalf("clicked %v, want [fmt]", clicked)
}
}