WIP feedback 2
No files matched your search
@@ -0,0 +1,119 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
type overlayProbe struct {
|
||||
loading bool
|
||||
saves, outside int
|
||||
cancels int
|
||||
cancel, label string
|
||||
}
|
||||
|
||||
func overlayTester(p *overlayProbe) *ui.Tester {
|
||||
return ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
ui.Column(c).Gap(12).Children(func() {
|
||||
r := LoadingOverlay(c, p.loading, LoadingOverlayOptions{Cancel: p.cancel, Label: p.label}, func() {
|
||||
ui.Column(c).Padding(24).Children(func() {
|
||||
if Button(c, "Save", ButtonOptions{}).Clicked() {
|
||||
p.saves++
|
||||
}
|
||||
})
|
||||
})
|
||||
if r.Canceled {
|
||||
p.cancels++
|
||||
}
|
||||
if Button(c, "Outside", ButtonOptions{}).Clicked() {
|
||||
p.outside++
|
||||
}
|
||||
})
|
||||
}, 500, 400)
|
||||
}
|
||||
|
||||
func TestLoadingOverlayBlocksOnlyItsRegion(t *testing.T) {
|
||||
p := &overlayProbe{}
|
||||
tt := overlayTester(p)
|
||||
_ = tt.Click("Save")
|
||||
p.loading = true
|
||||
tt.Frame()
|
||||
r, _ := tt.Find("Save")
|
||||
tt.ClickAt(r.X+r.W/2, r.Y+r.H/2)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if err := tt.Click("Outside"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.saves != 1 || p.outside != 1 {
|
||||
t.Fatalf("saves %d outside %d", p.saves, p.outside)
|
||||
}
|
||||
if !tt.HasText("Loading…") {
|
||||
t.Fatalf("texts %q", tt.Texts())
|
||||
}
|
||||
if a := tt.Announcements(); !slices.Contains(a, "Loading…") {
|
||||
t.Fatalf("announcements %q", a)
|
||||
}
|
||||
p.loading = false
|
||||
tt.Frame()
|
||||
_ = tt.Click("Save")
|
||||
if p.saves != 2 || tt.HasText("Loading…") {
|
||||
t.Fatalf("after loading saves %d", p.saves)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadingOverlayCancelAndFocus(t *testing.T) {
|
||||
p := &overlayProbe{cancel: "Stop", label: "Importing"}
|
||||
tt := overlayTester(p)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
if !tt.Focused("Save") {
|
||||
t.Fatal("Save not focused")
|
||||
}
|
||||
p.loading = true
|
||||
tt.Frame()
|
||||
if !tt.Focused("Stop") {
|
||||
t.Fatal("focus left the region instead of moving to cancel")
|
||||
}
|
||||
tt.Key(0, ui.KeyEscape)
|
||||
if p.cancels != 1 {
|
||||
t.Fatalf("Escape cancels %d", p.cancels)
|
||||
}
|
||||
if err := tt.Click("Stop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.cancels != 2 || p.saves != 0 {
|
||||
t.Fatalf("cancels %d saves %d", p.cancels, p.saves)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadingOverlayFocusWithoutCancel(t *testing.T) {
|
||||
p := &overlayProbe{}
|
||||
tt := overlayTester(p)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
p.loading = true
|
||||
tt.Frame()
|
||||
tt.Frame()
|
||||
if !tt.Focused("Loading…") {
|
||||
t.Fatal("focus not kept in the region")
|
||||
}
|
||||
tt.Key(0, ui.KeyEscape)
|
||||
if p.cancels != 0 {
|
||||
t.Fatal("Escape canceled without a cancel action")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadingOverlaySnapshot(t *testing.T) {
|
||||
snapshot(t, "loading_overlay", 420, 220, func(c *ui.Context) {
|
||||
ui.Column(c).Padding(16).Children(func() {
|
||||
LoadingOverlay(c, true, LoadingOverlayOptions{Cancel: "Cancel"}, func() {
|
||||
ui.Column(c).Padding(16).Gap(8).Background(Tokens(c).Surface).Radius(4).Children(func() {
|
||||
ui.Text(c, "Quarterly report")
|
||||
ui.Text(c, "Revenue, churn and pipeline for Q3.")
|
||||
Button(c, "Export", ButtonOptions{})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC
|
||||
u := Density(c).Unit()
|
||||
title := cmp.Or(opts.Title, msg(c, "notification_center.title"))
|
||||
var res NotificationCenterResult
|
||||
panel := ui.Column(c).FillWidth().Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
|
||||
panel := ui.Column(c).FillWidth().Shrink(1).MinHeight(0).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
|
||||
Role(ui.RoleGroup).Label(title).Clip()
|
||||
panel.Children(func() {
|
||||
ui.Row(c).AlignItems(ui.Center).Gap(u*2).Padding(u*3, u*3, u*3, u*4).Children(func() {
|
||||
@@ -98,7 +98,7 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC
|
||||
})
|
||||
return
|
||||
}
|
||||
list := ui.Scroll(c).Grow(1).Shrink(1).FillWidth().Role(ui.RoleList).FocusGroup(ui.Vertical | ui.Horizontal)
|
||||
list := ui.Scroll(c).Shrink(1).FillWidth().Role(ui.RoleList).FocusGroup(ui.Vertical | ui.Horizontal)
|
||||
list.Dividers(theme.BorderWidth, k.Border)
|
||||
list.Children(func() {
|
||||
for i := range *items {
|
||||
@@ -124,7 +124,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification,
|
||||
name = msg(c, "notification_center.unread_mark") + ": " + n.Title
|
||||
}
|
||||
row := ui.Row(c).Key(n.ID).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2).
|
||||
Role(ui.RoleListItem).Label(name)
|
||||
Role(ui.RoleListItem)
|
||||
if row.Hovered() {
|
||||
row.Background(k.SurfaceHover)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func sampleNotifications() []Notification {
|
||||
return []Notification{
|
||||
{ID: "a", Title: "Build failed", Body: "main · 3 tests failing", Time: "2 min ago", Action: "View log"},
|
||||
{ID: "b", Title: "Invite accepted", Body: "Mira joined the workspace", Time: "1 h ago"},
|
||||
{ID: "c", Title: "Backup complete", Time: "Yesterday", Read: true},
|
||||
}
|
||||
}
|
||||
|
||||
func notificationTester(items *[]Notification, last *NotificationCenterResult) *ui.Tester {
|
||||
return ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
ui.Column(c).Height(560).Children(func() {
|
||||
r := NotificationCenter(c, items, NotificationCenterOptions{})
|
||||
r.Element = nil
|
||||
if r != (NotificationCenterResult{}) {
|
||||
*last = r
|
||||
}
|
||||
})
|
||||
}, 640, 600)
|
||||
}
|
||||
|
||||
func TestNotificationCenterReadAndAction(t *testing.T) {
|
||||
items := sampleNotifications()
|
||||
var last NotificationCenterResult
|
||||
tt := notificationTester(&items, &last)
|
||||
if !tt.HasText("2 unread") {
|
||||
t.Fatalf("texts %q", tt.Texts())
|
||||
}
|
||||
if err := tt.Click("Unread: Invite accepted"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !items[1].Read || last.Opened != "b" || !tt.HasText("1 unread") {
|
||||
t.Fatalf("open: %+v %+v", items[1], last)
|
||||
}
|
||||
if err := tt.Click("View log"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if last.Action != "a" || items[0].Read {
|
||||
t.Fatalf("action: %+v %+v", last, items[0])
|
||||
}
|
||||
if err := tt.Click("Mark unread"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if items[1].Read || last.Toggled != "b" {
|
||||
t.Fatalf("toggle: %+v %+v", items[1], last)
|
||||
}
|
||||
if err := tt.Click("Mark all read"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range items {
|
||||
if !n.Read {
|
||||
t.Fatalf("mark all: %+v", n)
|
||||
}
|
||||
}
|
||||
if !last.MarkedAllRead || tt.HasText("1 unread") || tt.HasText("2 unread") {
|
||||
t.Fatalf("mark all: %+v %q", last, tt.Texts())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationCenterRemoveAndClear(t *testing.T) {
|
||||
items := sampleNotifications()
|
||||
var last NotificationCenterResult
|
||||
tt := notificationTester(&items, &last)
|
||||
if err := tt.Click("Clear"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 2 || items[0].ID != "b" || last.Removed != "a" {
|
||||
t.Fatalf("remove: %+v %+v", items, last)
|
||||
}
|
||||
if err := tt.Click("Clear all"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 0 || !last.ClearedAll || !tt.HasText("You're all caught up") {
|
||||
t.Fatalf("clear all: %+v %q", items, tt.Texts())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationCenterKeyboard(t *testing.T) {
|
||||
items := sampleNotifications()
|
||||
var last NotificationCenterResult
|
||||
tt := notificationTester(&items, &last)
|
||||
tt.Key(0, ui.KeyTab) // Mark all read
|
||||
tt.Key(0, ui.KeyTab) // Clear all
|
||||
tt.Key(0, ui.KeyTab) // first notification
|
||||
if !tt.Focused("Unread: Build failed") {
|
||||
t.Fatal("Tab did not enter the list")
|
||||
}
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if !items[0].Read || last.Opened != "a" {
|
||||
t.Fatalf("Enter: %+v", items[0])
|
||||
}
|
||||
tt.Key(0, ui.KeyDelete)
|
||||
if len(items) != 2 || items[0].ID != "b" {
|
||||
t.Fatalf("Delete: %+v", items)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
if tt.Focused("Unread: Invite accepted") {
|
||||
t.Fatal("the list takes more than one Tab stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationCenterPanicsOnRepeatedID(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("no panic")
|
||||
}
|
||||
}()
|
||||
items := []Notification{{ID: "x"}, {ID: "x"}}
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
NotificationCenter(c, &items, NotificationCenterOptions{})
|
||||
}, 300, 200)
|
||||
}
|
||||
|
||||
func TestNotificationCenterSnapshot(t *testing.T) {
|
||||
items := sampleNotifications()
|
||||
snapshot(t, "notification_center", 520, 400, func(c *ui.Context) {
|
||||
ui.Column(c).Padding(16).Height(400).Children(func() {
|
||||
NotificationCenter(c, &items, NotificationCenterOptions{})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -37,25 +37,32 @@ func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element {
|
||||
still := c.Preferences().ReduceMotion
|
||||
row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center)
|
||||
row.Children(func() {
|
||||
// MyGo's spinner carries the indeterminate progress role.
|
||||
ui.Spinner(c).Key(still).Label(label).Size(size, size).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
turn := 0.0
|
||||
if !still {
|
||||
turn = float64(p.Now().UnixMilli()%1200) / 1200
|
||||
p.AnimationFrame()
|
||||
}
|
||||
cx, cy, rad := r.X+r.W/2, r.Y+r.H/2, r.W*0.32
|
||||
for i := range 4 {
|
||||
a := 2 * math.Pi * (turn + float64(i)/4)
|
||||
x := cx + rad*float32(math.Cos(a))
|
||||
y := cy + rad*float32(math.Sin(a))
|
||||
alpha := 1 - float32(i)*0.22
|
||||
p.FillPath(internal.Diamond(x, y, r.W*0.3), color.Alpha(alpha))
|
||||
}
|
||||
})
|
||||
// Keyed by motion so a flip repaints; MyGo's spinner carries the progress role.
|
||||
ui.Box(c).Key(still).Shrink(0).Children(func() { spinnerMark(c, label, size, color, still) })
|
||||
if opts.ShowLabel {
|
||||
ui.Text(c, label).TextColor(k.TextMuted).SingleLine()
|
||||
}
|
||||
})
|
||||
return row
|
||||
}
|
||||
|
||||
func spinnerMark(c *ui.Context, label string, size float32, color ui.Color, still bool) {
|
||||
ui.Spinner(c).Label(label).Size(size, size).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
turn := 0.0
|
||||
if !still {
|
||||
turn = float64(p.Now().UnixMilli()%1200) / 1200
|
||||
p.AnimationFrame()
|
||||
}
|
||||
cx, cy, rad := r.X+r.W/2, r.Y+r.H/2, r.W*0.32
|
||||
for i := range 4 {
|
||||
a := 2 * math.Pi * (turn + float64(i)/4)
|
||||
x := cx + rad*float32(math.Cos(a))
|
||||
y := cy + rad*float32(math.Sin(a))
|
||||
// Device-pixel centers keep cached shapes rendering alike.
|
||||
s := p.Scale()
|
||||
x, y = float32(math.Round(float64(x*s)))/s, float32(math.Round(float64(y*s)))/s
|
||||
alpha := 1 - float32(i)*0.22
|
||||
p.FillPath(internal.Diamond(x, y, r.W*0.3), color.Alpha(alpha))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package mujica
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -19,8 +20,8 @@ func TestSpinnerLabelAndSize(t *testing.T) {
|
||||
if !ok || r.W != 16 || r.H != 16 {
|
||||
t.Fatalf("default mark %v %v", r, ok)
|
||||
}
|
||||
if tt.HasText("加载中") {
|
||||
t.Fatal("hidden label painted")
|
||||
if n := len(slices.DeleteFunc(tt.Texts(), func(s string) bool { return s != "加载中" })); n != 1 {
|
||||
t.Fatalf("hidden label painted: %d", n)
|
||||
}
|
||||
mark, ok = tt.Find("Fetching mail")
|
||||
if !ok || mark.W != 32 {
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |