package mujica import ( "bytes" "image/color" "os" "path/filepath" "runtime" "strings" "testing" "time" "github.com/egoist/mygo/ui" "mujicaui/icons" "mujicaui/theme" ) func tabsSample() []Tab { return []Tab{ {Label: "General", Panel: func() {}}, {Label: "Fonts", Disabled: true}, {Label: "Colors"}, {Label: "Advanced"}, } } func TestTabsClickAndPanel(t *testing.T) { sel, changes := 0, 0 tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) ts := tabsSample() ts[0].Panel = func() { ui.Text(c, "General page") } ts[2].Panel = func() { ui.Text(c, "Colors page") } if Tabs(c, &sel, ts, TabsOptions{Label: "Settings"}).Changed() { changes++ } }, 500, 200) if !tt.HasText("General page") || tt.HasText("Colors page") { t.Fatalf("panel of tab 0 not shown: %q", tt.Texts()) } if err := tt.Click("Colors"); err != nil { t.Fatal(err) } if sel != 2 || changes != 1 || !tt.HasText("Colors page") { t.Fatalf("click Colors: sel %d changes %d texts %q", sel, changes, tt.Texts()) } _ = tt.Click("Fonts") if sel != 2 || changes != 1 { t.Fatalf("disabled tab chosen: sel %d changes %d", sel, changes) } } func TestTabsKeyboardSkipsDisabled(t *testing.T) { for _, vertical := range []bool{false, true} { sel := 0 tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) Tabs(c, &sel, tabsSample(), TabsOptions{Vertical: vertical}) }, 500, 300) tt.Key(0, ui.KeyTab) if !tt.Focused("General") { t.Fatalf("vertical=%v: Tab did not focus the chosen tab", vertical) } next := ui.KeyRight if vertical { next = ui.KeyDown } tt.Key(0, next) if sel != 2 || !tt.Focused("Colors") { t.Fatalf("vertical=%v: arrow chose %d, want 2 skipping disabled", vertical, sel) } tt.Key(0, ui.KeyEnd) if sel != 3 { t.Fatalf("vertical=%v: End chose %d", vertical, sel) } tt.Key(0, next) if sel != 0 { t.Fatalf("vertical=%v: wrap chose %d", vertical, sel) } } } func TestTabsOverflowScrolls(t *testing.T) { sel := 0 var tabs []Tab for _, l := range []string{"Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel"} { tabs = append(tabs, Tab{Label: l}) } var w float32 tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) ui.Column(c).Width(240).Children(func() { w = Tabs(c, &sel, tabs, TabsOptions{}).Bounds().W }) }, 400, 200) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyEnd) tt.Frame() r, ok := tt.Find("Hotel") if sel != 7 || !ok || r.X+r.W > 240 { t.Fatalf("last tab not scrolled into view: sel %d rect %+v", sel, r) } if w > 240 { t.Fatalf("tabs widened their parent to %v", w) } } func TestTabsInvalid(t *testing.T) { for name, tc := range map[string]struct { sel int tabs []Tab }{ "empty": {0, nil}, "range": {4, tabsSample()}, "disabled": {1, tabsSample()}, "label": {0, []Tab{{Icon: icons.Must("star")}}}, } { t.Run(name, func(t *testing.T) { defer func() { if recover() == nil { t.Fatal("no panic") } }() ui.Render(func(c *ui.Context) { Use(c, Settings{}) Tabs(c, &tc.sel, tc.tabs, TabsOptions{}) }, 300, 100, 1) }) } } func tabsView(c *ui.Context) { sel, vsel := 0, 2 ui.Column(c).Padding(16).Gap(16).Children(func() { Tabs(c, &sel, []Tab{ {Label: "Overview", Icon: icons.Must("house"), Panel: func() { ui.Text(c, "The chosen tab's panel.") }}, {Label: "Members", Icon: icons.Must("user")}, {Label: "Archive", Disabled: true}, {Label: "Settings", Icon: icons.Must("settings")}, }, TabsOptions{}) Tabs(c, &vsel, []Tab{ {Label: "Profile"}, {Label: "Security", Disabled: true}, {Label: "Billing", Panel: func() { ui.Text(c, "Vertical layout.") }}, }, TabsOptions{Vertical: true}) }) } func TestTabsSnapshot(t *testing.T) { snapshot(t, "tabs", 520, 300, tabsView) for _, d := range []theme.Density{theme.Compact, theme.Comfortable} { snapshot(t, "tabs-"+strings.ToLower(d.String()), 520, 340, func(c *ui.Context) { Use(c, Settings{Mode: tabsMode(c), Density: d}) tabsView(c) }) } } // tabsStateSnapshot compares tabsView after act in both themes with -.png. func tabsStateSnapshot(t *testing.T, name string, act func(tt *ui.Tester)) { t.Helper() for _, m := range []Mode{Light, Dark} { tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{Mode: m}) tabsView(c) }, 520, 300) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) act(tt) tt.Frame() got := encode(t, tt.Image()) path := filepath.Join("testdata", "snapshots", runtime.GOOS, name+"-"+strings.ToLower(m.String())+".png") if *update { if err := os.WriteFile(path, got, 0o644); err != nil { t.Fatal(err) } continue } want, err := os.ReadFile(path) if err != nil { t.Fatalf("%v (run go test -update)", err) } if !bytes.Equal(got, want) { if err := os.WriteFile(path+".got.png", got, 0o644); err != nil { t.Fatal(err) } t.Errorf("%s differs; see %s.got.png", path, path) } } } func TestTabsStateSnapshots(t *testing.T) { center := func(tt *ui.Tester, s string) (float32, float32) { r, ok := tt.Find(s) if !ok { t.Fatalf("no %q", s) } return r.X + r.W/2, r.Y + r.H/2 } tabsStateSnapshot(t, "tabs-hover", func(tt *ui.Tester) { tt.Move(center(tt, "Members")) }) tabsStateSnapshot(t, "tabs-pressed", func(tt *ui.Tester) { tt.Press(center(tt, "Settings")) }) tabsStateSnapshot(t, "tabs-focus", func(tt *ui.Tester) { tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyRight) }) } // TestTabsStateColorsEase checks hover fades in over StateDuration, not at once. func TestTabsStateColorsEase(t *testing.T) { sel := 0 tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{Mode: Light}) Tabs(c, &sel, []Tab{{Label: "One"}, {Label: "Two"}}, TabsOptions{}) }, 300, 120) r, _ := tt.Find("Two") x, y := int(r.X-8), int(r.Y+r.H/2) hover := tabsRGBA(theme.Light().SurfaceHover) tt.Move(r.X+r.W/2, r.Y+r.H/2) if got := tt.Image().RGBAAt(x, y); got == hover { t.Fatal("hover color applied at once") } time.Sleep(theme.StateDuration + 40*time.Millisecond) tt.Frame() if got := tt.Image().RGBAAt(x, y); got != hover { t.Fatalf("hover color %v after the transition, want %v", got, hover) } } func tabsRGBA(c ui.Color) color.RGBA { return color.RGBA{c.R, c.G, c.B, c.A} } // tabsMode recovers the mode snapshot applied, to add a density. func tabsMode(c *ui.Context) Mode { if Tokens(c).Dark { return Dark } return Light }