Merge commit '4fe912b' into merge-main-13-seg13-copy-reveal

This commit is contained in:
Joe Julian
2026-03-29 13:35:12 -07:00
17 changed files with 2221 additions and 340 deletions
+349
View File
@@ -11,6 +11,9 @@ import (
"strings"
"testing"
"gioui.org/io/key"
"gioui.org/unit"
"git.julianfamily.org/keepassgo/clipboard"
"git.julianfamily.org/keepassgo/session"
"git.julianfamily.org/keepassgo/vault"
@@ -41,6 +44,88 @@ func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) {
}
}
func TestUISearchBehaviorIsConsistentAcrossDesktopAndPhoneLayouts(t *testing.T) {
t.Parallel()
modes := []string{"desktop", "phone"}
for _, mode := range modes {
mode := mode
t.Run(mode, func(t *testing.T) {
t.Parallel()
u := newUIWithModel(mode, vault.Model{
Entries: []vault.Entry{
{ID: "entry-1", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Root", "Internet"}},
{ID: "entry-2", Title: "HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
},
Templates: []vault.Entry{
{ID: "tpl-1", Title: "Website Login", URL: "https://accounts.example.com", Path: []string{"Templates", "Web"}},
{ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}},
},
RecycleBin: []vault.Entry{
{ID: "deleted-1", Title: "Deleted Bellagio", URL: "https://bellagio.example.com", Path: []string{"Root", "Internet"}},
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.search.SetText("climate")
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"HVAC"}) {
t.Fatalf("entries filteredTitles() = %v, want [HVAC]", got)
}
u.showTemplatesSection()
u.currentPath = []string{"Templates", "Web"}
u.search.SetText("infra")
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"SSH Login"}) {
t.Fatalf("templates filteredTitles() = %v, want [SSH Login]", got)
}
if got := u.visiblePathContexts(); !slices.Equal(got, []string{"Templates / Infra"}) {
t.Fatalf("templates visiblePathContexts() = %v, want [Templates / Infra]", got)
}
u.showRecycleBinSection()
u.search.SetText("climate")
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"Deleted HVAC"}) {
t.Fatalf("recycle filteredTitles() = %v, want [Deleted HVAC]", got)
}
if got := u.visiblePathContexts(); !slices.Equal(got, []string{"Recycle Bin / Root / Home"}) {
t.Fatalf("recycle visiblePathContexts() = %v, want [Recycle Bin / Root / Home]", got)
}
})
}
}
func TestUIClearingSearchResetsToCurrentSectionListing(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Templates: []vault.Entry{
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
{ID: "tpl-2", Title: "Email Login", Path: []string{"Templates", "Web"}},
{ID: "tpl-3", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
},
})
u.showTemplatesSection()
u.currentPath = []string{"Templates", "Web"}
u.search.SetText("ssh")
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"SSH Login"}) {
t.Fatalf("filteredTitles() with search = %v, want [SSH Login]", got)
}
u.search.SetText("")
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"Email Login", "Website Login"}) {
t.Fatalf("filteredTitles() after clearing search = %v, want [Email Login Website Login]", got)
}
}
func TestUIChildGroupsComeFromVaultModel(t *testing.T) {
t.Parallel()
@@ -593,6 +678,98 @@ func TestUISavesDuplicatesDeletesAndRestoresEntriesFromTheEditor(t *testing.T) {
}
}
func TestUICreatesEntryWithAllSupportedEditorFields(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.entryID.SetText("bellagio")
u.entryTitle.SetText("Bellagio")
u.entryUsername.SetText("rustyryan")
u.entryPassword.SetText("token-1")
u.entryURL.SetText("https://bellagio.example.invalid")
u.entryNotes.SetText("Registrar account")
u.entryTags.SetText("dns, registrar")
u.entryPath.SetText("Root / Internet")
u.entryFields.SetText("Environment=prod\nAccount ID=12345")
if err := u.saveEntryAction(); err != nil {
t.Fatalf("saveEntryAction() create error = %v", err)
}
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"Bellagio"}) {
t.Fatalf("filteredTitles() = %v, want [Bellagio]", got)
}
item, ok := u.selectedEntry()
if !ok {
t.Fatal("selectedEntry() ok = false, want created entry")
}
if item.Title != "Bellagio" || item.Username != "rustyryan" || item.Password != "token-1" || item.URL != "https://bellagio.example.invalid" {
t.Fatalf("selectedEntry() = %#v, want created Bellagio credentials", item)
}
if item.Notes != "Registrar account" {
t.Fatalf("selectedEntry().Notes = %q, want %q", item.Notes, "Registrar account")
}
if !slices.Equal(item.Tags, []string{"dns", "registrar"}) {
t.Fatalf("selectedEntry().Tags = %v, want [dns registrar]", item.Tags)
}
if item.Fields["Environment"] != "prod" || item.Fields["Account ID"] != "12345" {
t.Fatalf("selectedEntry().Fields = %#v, want parsed custom fields", item.Fields)
}
}
func TestUIEditingEntryPathMovesEntryBetweenGroups(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "vault-console",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-1",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.state.SelectedEntryID = "vault-console"
u.loadSelectedEntryIntoEditor()
u.entryPath.SetText("Root / Infrastructure")
if err := u.saveEntryAction(); err != nil {
t.Fatalf("saveEntryAction() move error = %v", err)
}
u.currentPath = []string{"Root", "Internet"}
u.filter()
if got := u.filteredTitles(); len(got) != 0 {
t.Fatalf("filteredTitles() in old path = %v, want empty after move", got)
}
u.currentPath = []string{"Root", "Infrastructure"}
u.filter()
if got := u.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
t.Fatalf("filteredTitles() in new path = %v, want [Vault Console]", got)
}
item, ok := u.selectedEntry()
if !ok {
t.Fatal("selectedEntry() ok = false, want moved entry")
}
if !slices.Equal(item.Path, []string{"Root", "Infrastructure"}) {
t.Fatalf("selectedEntry().Path = %v, want [Root Infrastructure]", item.Path)
}
}
func TestUITemplateAndAttachmentActionsWorkThroughEditor(t *testing.T) {
t.Parallel()
@@ -758,6 +935,178 @@ func TestUIKeyboardShortcutActionsDispatchExpectedCommands(t *testing.T) {
}
}
func TestUIKeyboardNavigationMovesAcrossBreadcrumbsListAndDetail(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "bellagio",
Title: "Bellagio",
Username: "rustyryan",
Path: []string{"Root", "Internet"},
},
{
ID: "vault-console",
Title: "Vault Console",
Username: "dannyocean",
Path: []string{"Root", "Internet"},
},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
if got := u.keyboardFocus; got != focusSearch {
t.Fatalf("keyboardFocus = %q, want %q", got, focusSearch)
}
u.handleKeyPress(key.NameTab, 0)
if got := u.keyboardFocus; got != breadcrumbFocusID(0) {
t.Fatalf("keyboardFocus after Tab = %q, want %q", got, breadcrumbFocusID(0))
}
u.handleKeyPress(key.NameTab, 0)
if got := u.keyboardFocus; got != listFocusID(0) {
t.Fatalf("keyboardFocus after second Tab = %q, want %q", got, listFocusID(0))
}
if got := u.state.SelectedEntryID; got != "bellagio" {
t.Fatalf("SelectedEntryID after list focus = %q, want %q", got, "bellagio")
}
u.handleKeyPress(key.NameDownArrow, 0)
if got := u.keyboardFocus; got != listFocusID(1) {
t.Fatalf("keyboardFocus after Down = %q, want %q", got, listFocusID(1))
}
if got := u.state.SelectedEntryID; got != "vault-console" {
t.Fatalf("SelectedEntryID after Down = %q, want %q", got, "vault-console")
}
u.handleKeyPress(key.NameTab, 0)
if got := u.keyboardFocus; got != detailFocusID(detailFieldTitle) {
t.Fatalf("keyboardFocus after detail Tab = %q, want %q", got, detailFocusID(detailFieldTitle))
}
}
func TestUIKeyboardNavigationActivatesBreadcrumbs(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "vault-console",
Title: "Vault Console",
Path: []string{"Root", "Internet"},
},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.keyboardFocus = breadcrumbFocusID(0)
u.handleKeyPress(key.NameRightArrow, 0)
if got := u.keyboardFocus; got != breadcrumbFocusID(1) {
t.Fatalf("keyboardFocus after Right = %q, want %q", got, breadcrumbFocusID(1))
}
u.handleKeyPress(key.NameReturn, 0)
if got := u.currentPath; !slices.Equal(got, []string{"Root"}) {
t.Fatalf("currentPath after breadcrumb activation = %v, want [Root]", got)
}
}
func TestUIKeyboardShortcutsMoveFocusForSearchAndNewEntry(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "vault-console",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-1",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.state.SelectedEntryID = "vault-console"
u.loadSelectedEntryIntoEditor()
u.keyboardFocus = listFocusID(0)
u.handleKeyPress("F", key.ModShortcut)
if got := u.keyboardFocus; got != focusSearch {
t.Fatalf("keyboardFocus after shortcut search = %q, want %q", got, focusSearch)
}
u.handleKeyPress("N", key.ModShortcut)
if got := u.state.SelectedEntryID; got != "" {
t.Fatalf("SelectedEntryID after shortcut new-entry = %q, want empty", got)
}
if got := u.keyboardFocus; got != detailFocusID(detailFieldTitle) {
t.Fatalf("keyboardFocus after shortcut new-entry = %q, want %q", got, detailFocusID(detailFieldTitle))
}
}
func TestUIAccessibilityLabelsDescribeFocusableControls(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "vault-console",
Title: "Vault Console",
Path: []string{"Root", "Internet"},
},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
if got := u.accessibilityLabel(focusSearch); got != "Search vault" {
t.Fatalf("accessibilityLabel(search) = %q, want %q", got, "Search vault")
}
if got := u.accessibilityLabel(breadcrumbFocusID(1)); got != "Navigate to Root" {
t.Fatalf("accessibilityLabel(breadcrumb) = %q, want %q", got, "Navigate to Root")
}
if got := u.accessibilityLabel(listFocusID(0)); got != "Select entry Vault Console" {
t.Fatalf("accessibilityLabel(list) = %q, want %q", got, "Select entry Vault Console")
}
if got := u.accessibilityLabel(detailFocusID(detailFieldPassword)); got != "Edit Password" {
t.Fatalf("accessibilityLabel(detail password) = %q, want %q", got, "Edit Password")
}
}
func TestFieldFocusAppearanceScalesForHighDPI(t *testing.T) {
t.Parallel()
lo := fieldFocusAppearance(unit.Metric{PxPerDp: 1, PxPerSp: 1}, true)
hi := fieldFocusAppearance(unit.Metric{PxPerDp: 2.5, PxPerSp: 2.5}, true)
unfocused := fieldFocusAppearance(unit.Metric{PxPerDp: 1, PxPerSp: 1}, false)
if got := lo.MinHeight; got != 44 {
t.Fatalf("fieldFocusAppearance(low).MinHeight = %d, want 44", got)
}
if got := hi.MinHeight; got != 110 {
t.Fatalf("fieldFocusAppearance(high).MinHeight = %d, want 110", got)
}
if got := lo.OutlineWidth; got < 2 {
t.Fatalf("fieldFocusAppearance(low).OutlineWidth = %d, want >= 2", got)
}
if hi.OutlineWidth <= lo.OutlineWidth {
t.Fatalf("fieldFocusAppearance(high).OutlineWidth = %d, want > %d", hi.OutlineWidth, lo.OutlineWidth)
}
if lo.OutlineColor == unfocused.OutlineColor {
t.Fatalf("fieldFocusAppearance().OutlineColor focused = %#v, want distinct from unfocused %#v", lo.OutlineColor, unfocused.OutlineColor)
}
}
func TestUIActionErrorsAndStatusMessagesAreCapturedForDisplay(t *testing.T) {
t.Parallel()