Improve group deletion and status banner UX
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/unit"
|
||||
@@ -774,6 +775,7 @@ func TestUIGroupManagementAndPathNavigationAreControllerDriven(t *testing.T) {
|
||||
|
||||
u.state.NavigateToPath([]string{"Root", "Budget"})
|
||||
u.filter()
|
||||
u.armDeleteCurrentGroupAction()
|
||||
if err := u.deleteCurrentGroupAction(); err != nil {
|
||||
t.Fatalf("deleteCurrentGroupAction() error = %v", err)
|
||||
}
|
||||
@@ -1667,6 +1669,28 @@ func TestUIBannerSurfacePrefersLoadingThenErrorThenStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIStatusBannerExpiresAfterTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Date(2026, time.March, 29, 12, 0, 0, 0, time.UTC)
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.now = func() time.Time { return now }
|
||||
u.state.StatusMessage = "synchronize vault complete"
|
||||
u.statusExpiresAt = now.Add(statusBannerDuration)
|
||||
|
||||
if got := u.bannerSurface(); got.Kind != bannerStatus || got.Message != "synchronize vault complete" {
|
||||
t.Fatalf("bannerSurface() before expiry = %#v, want visible status banner", got)
|
||||
}
|
||||
|
||||
now = now.Add(statusBannerDuration + time.Millisecond)
|
||||
if got := u.bannerSurface(); got.Kind != bannerNone {
|
||||
t.Fatalf("bannerSurface() after expiry = %#v, want no banner", got)
|
||||
}
|
||||
if got := u.state.StatusMessage; got != "" {
|
||||
t.Fatalf("state.StatusMessage after expiry = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIRunActionNormalizesRemoteSaveConflictsForDisplay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2144,6 +2168,78 @@ func TestUILocalLifecycleActionsUpdateVisibleStatusMessages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIGroupDeletionOnlyAllowsEmptyGroupsAndRequiresConfirmation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("non-empty group cannot be deleted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
||||
},
|
||||
Groups: [][]string{{"Root"}, {"Root", "Internet"}},
|
||||
})
|
||||
u.showEntriesSection()
|
||||
u.state.NavigateToPath([]string{"Root", "Internet"})
|
||||
u.filter()
|
||||
|
||||
if deletable, reason := u.currentGroupDeletionState(); deletable {
|
||||
t.Fatal("currentGroupDeletionState() deletable = true, want false for non-empty group")
|
||||
} else if !strings.Contains(reason, "contains entries") {
|
||||
t.Fatalf("currentGroupDeletionState() reason = %q, want contains entries guidance", reason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty group requires confirmation before deletion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Groups: [][]string{{"Root"}, {"Root", "Archive"}},
|
||||
})
|
||||
u.showEntriesSection()
|
||||
u.state.NavigateToPath([]string{"Root", "Archive"})
|
||||
u.filter()
|
||||
|
||||
if deletable, reason := u.currentGroupDeletionState(); !deletable {
|
||||
t.Fatalf("currentGroupDeletionState() = false, want true for empty group: %q", reason)
|
||||
}
|
||||
|
||||
u.armDeleteCurrentGroupAction()
|
||||
if !u.deleteGroupPendingConfirmation() {
|
||||
t.Fatal("deleteGroupPendingConfirmation() = false, want true after arming delete")
|
||||
}
|
||||
if got := u.state.StatusMessage; !strings.Contains(got, "Confirm deleting empty group") {
|
||||
t.Fatalf("StatusMessage after arming delete = %q, want confirmation guidance", got)
|
||||
}
|
||||
|
||||
if err := u.deleteCurrentGroupAction(); err != nil {
|
||||
t.Fatalf("deleteCurrentGroupAction() error = %v", err)
|
||||
}
|
||||
if u.deleteGroupPendingConfirmation() {
|
||||
t.Fatal("deleteGroupPendingConfirmation() = true, want false after deletion")
|
||||
}
|
||||
if got := u.currentPath; !slices.Equal(got, []string{"Root"}) {
|
||||
t.Fatalf("currentPath after delete = %v, want [Root]", got)
|
||||
}
|
||||
if got := u.childGroups(); len(got) != 0 {
|
||||
t.Fatalf("childGroups() after delete = %v, want empty", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUITemplateSectionEmptyStateStaysProductSpecific(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
u.showTemplatesSection()
|
||||
|
||||
got := u.listEmptyMessage()
|
||||
if got != "Templates are not available in this build." {
|
||||
t.Fatalf("listEmptyMessage() = %q, want templates unavailable copy", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUILocalLifecycleActionErrorsAreVisibleAndSpecific(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user