Improve group deletion and status banner UX

This commit is contained in:
Joe Julian
2026-03-29 16:17:22 -07:00
parent fe3fa854bb
commit 1c4fb59960
4 changed files with 236 additions and 10 deletions
+96
View File
@@ -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()