package component import ( "context" "errors" "sync" "testing" "time" "github.com/bnema/dumber/internal/application/dto" "github.com/bnema/dumber/internal/domain/entity" "github.com/bnema/puregotk/v4/gdk" "github.com/bnema/puregotk/v4/glib" "github.com/bnema/puregotk/v4/gtk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type fakeFavoritesSidebarUC struct { favorites []*entity.Favorite tags []*entity.Tag addInputs []dto.FavoriteCreateInput updateInputs []dto.FavoriteUpdateInput deletedIDs []entity.FavoriteID shortcuts []struct { id entity.FavoriteID key *int } tagged []struct { favID entity.FavoriteID tagID entity.TagID } untagged []struct { favID entity.FavoriteID tagID entity.TagID } createdTags []struct { name string color string } err error errOnSetShortcut error errOnTag map[entity.TagID]error errOnUntag map[entity.TagID]error failUntagOnCall int getAllCalled chan struct{} } func (f *fakeFavoritesSidebarUC) GetAll(context.Context) ([]*entity.Favorite, error) { if f.getAllCalled != nil { select { case f.getAllCalled <- struct{}{}: default: } } return f.favorites, f.err } func (f *fakeFavoritesSidebarUC) GetAllTags(context.Context) ([]*entity.Tag, error) { return f.tags, f.err } func (f *fakeFavoritesSidebarUC) AddTag(_ context.Context, name, color string) (*entity.Tag, error) { f.createdTags = append(f.createdTags, struct { name string color string }{name: name, color: color}) if f.err != nil { return nil, f.err } return &entity.Tag{ID: entity.TagID(len(f.tags) + 1), Name: name, Color: color}, nil } func (f *fakeFavoritesSidebarUC) AddFavorite(_ context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) { f.addInputs = append(f.addInputs, input) if f.err != nil { return nil, f.err } return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title}, nil } func (f *fakeFavoritesSidebarUC) UpdateFavorite(_ context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) { f.updateInputs = append(f.updateInputs, input) if f.err != nil { return nil, f.err } return &entity.Favorite{ID: input.ID, Title: input.Title}, nil } func (f *fakeFavoritesSidebarUC) DeleteFavorite(_ context.Context, id entity.FavoriteID) error { f.deletedIDs = append(f.deletedIDs, id) return f.err } func (f *fakeFavoritesSidebarUC) SetShortcut(_ context.Context, id entity.FavoriteID, key *int) error { f.shortcuts = append(f.shortcuts, struct { id entity.FavoriteID key *int }{id: id, key: key}) if f.errOnSetShortcut != nil { return f.errOnSetShortcut } return f.err } func (f *fakeFavoritesSidebarUC) TagFavorite(_ context.Context, favID entity.FavoriteID, tagID entity.TagID) error { f.tagged = append(f.tagged, struct { favID entity.FavoriteID tagID entity.TagID }{favID: favID, tagID: tagID}) if err := f.errOnTag[tagID]; err != nil { return err } return f.err } func (f *fakeFavoritesSidebarUC) UntagFavorite(_ context.Context, favID entity.FavoriteID, tagID entity.TagID) error { f.untagged = append(f.untagged, struct { favID entity.FavoriteID tagID entity.TagID }{favID: favID, tagID: tagID}) if err := f.errOnUntag[tagID]; err != nil { return err } if f.failUntagOnCall > 0 && len(f.untagged) == f.failUntagOnCall { return errors.New("untag failed") } return f.err } func (f *fakeFavoritesSidebarUC) UpdateFavoriteTags(ctx context.Context, favID entity.FavoriteID, wanted []entity.TagID) error { wantedSet := make(map[entity.TagID]struct{}, len(wanted)) for _, id := range wanted { wantedSet[id] = struct{}{} } var current []entity.Tag for _, favorite := range f.favorites { if favorite != nil && favorite.ID == favID { current = favorite.Tags break } } currentSet := make(map[entity.TagID]struct{}, len(current)) for _, tag := range current { currentSet[tag.ID] = struct{}{} if _, keep := wantedSet[tag.ID]; !keep { if err := f.UntagFavorite(ctx, favID, tag.ID); err != nil { return err } } } for _, id := range wanted { if _, exists := currentSet[id]; exists { continue } if err := f.TagFavorite(ctx, favID, id); err != nil { return err } } return nil } func newFavoritesSidebarHarness(favorites []*entity.Favorite, tags []*entity.Tag) *FavoritesSidebar { fs := &FavoritesSidebar{ favoritesUC: &fakeFavoritesSidebarUC{favorites: favorites, tags: tags}, selectedTagIDs: make(map[entity.TagID]struct{}), ctx: context.Background(), idleScheduler: func(cb glib.SourceFunc) { cb(0) }, } return fs } func TestFavoritesSidebarInitialLoadRenderingModelBehavior(t *testing.T) { fav := &entity.Favorite{ID: 1, URL: "https://go.dev", Title: "Go", Position: 2} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, []*entity.Tag{{ID: 10, Name: "dev"}}) fs.loadGen = 1 applied := fs.applyLoadedData(fs.favoritesUC.(*fakeFavoritesSidebarUC).favorites, fs.favoritesUC.(*fakeFavoritesSidebarUC).tags, 1, nil) require.True(t, applied) assert.Len(t, fs.allFavorites, 1) assert.Len(t, fs.allTags, 1) require.Len(t, fs.displayRows, 1) assert.Equal(t, entity.FavoriteID(1), fs.displayRows[0].FavoriteID) assert.Equal(t, "https://go.dev", fs.displayRows[0].URL) } func TestFavoritesSidebarSearchFilteringByTitleURLAndTag(t *testing.T) { devTag := entity.Tag{ID: 1, Name: "Dev"} newsTag := entity.Tag{ID: 2, Name: "News"} fs := newFavoritesSidebarHarness([]*entity.Favorite{ {ID: 1, URL: "https://go.dev/doc", Title: "Golang Docs", Tags: []entity.Tag{devTag}, Position: 1}, {ID: 2, URL: "https://example.com/news", Title: "Daily", Tags: []entity.Tag{newsTag}, Position: 2}, }, nil) fs.allFavorites = fs.favoritesUC.(*fakeFavoritesSidebarUC).favorites fs.currentQuery = "golang" fs.rebuildDisplayRowsLocked() require.Len(t, fs.displayRows, 1) assert.Equal(t, entity.FavoriteID(1), fs.displayRows[0].FavoriteID) fs.currentQuery = "example.com" fs.rebuildDisplayRowsLocked() require.Len(t, fs.displayRows, 1) assert.Equal(t, entity.FavoriteID(2), fs.displayRows[0].FavoriteID) fs.currentQuery = "dev" fs.rebuildDisplayRowsLocked() require.Len(t, fs.displayRows, 1) assert.Equal(t, entity.FavoriteID(1), fs.displayRows[0].FavoriteID) } func TestFavoritesSidebarTagFilteringMatchAny(t *testing.T) { fs := newFavoritesSidebarHarness([]*entity.Favorite{ {ID: 1, URL: "https://a.test", Title: "A", Tags: []entity.Tag{{ID: 1, Name: "one"}}}, {ID: 2, URL: "https://b.test", Title: "B", Tags: []entity.Tag{{ID: 2, Name: "two"}}}, {ID: 3, URL: "https://c.test", Title: "C", Tags: []entity.Tag{{ID: 3, Name: "three"}}}, }, nil) fs.allFavorites = fs.favoritesUC.(*fakeFavoritesSidebarUC).favorites fs.selectedTagIDs[1] = struct{}{} fs.selectedTagIDs[2] = struct{}{} fs.rebuildDisplayRowsLocked() require.Len(t, fs.displayRows, 2) assert.Equal(t, entity.FavoriteID(1), fs.displayRows[0].FavoriteID) assert.Equal(t, entity.FavoriteID(2), fs.displayRows[1].FavoriteID) } func TestFavoritesSidebarActivationCallbacks(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.ctx = context.Background() fs.idleScheduler = func(cb glib.SourceFunc) { cb(0) } var mu sync.Mutex called := []string{} fs.onNavigate = func(_ context.Context, url string) error { mu.Lock() defer mu.Unlock() called = append(called, "nav:"+url) return nil } fs.onNavigateKeepOpen = func(_ context.Context, url string) error { mu.Lock() defer mu.Unlock() called = append(called, "keep:"+url) return nil } fs.onOpenInNewPane = func(_ context.Context, url string) error { mu.Lock() defer mu.Unlock() called = append(called, "pane:"+url) return nil } fs.navigateToURL("https://a.test") fs.navigateWithoutClosing("https://b.test") fs.navigateToNewPane("https://c.test") assert.Equal(t, []string{"nav:https://a.test", "keep:https://b.test", "pane:https://c.test"}, called) } func TestFavoritesSidebarRequestReloadIfVisibleReloadsData(t *testing.T) { called := make(chan struct{}, 1) uc := &fakeFavoritesSidebarUC{ favorites: []*entity.Favorite{{ID: 1, URL: "https://go.dev", Title: "Go"}}, tags: []*entity.Tag{{ID: 10, Name: "dev"}}, getAllCalled: called, } fs := newFavoritesSidebarHarness(nil, nil) fs.favoritesUC = uc fs.visible = true fs.RequestReloadIfVisible("test") require.Eventually(t, func() bool { select { case <-called: return true default: return false } }, time.Second, 10*time.Millisecond) require.Eventually(t, func() bool { fs.mu.RLock() defer fs.mu.RUnlock() return len(fs.allFavorites) == 1 && len(fs.allTags) == 1 }, time.Second, 10*time.Millisecond) } func TestFavoritesSidebarStaleLoadRejected(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.loadGen = 2 applied := fs.applyLoadedData([]*entity.Favorite{{ID: 1, URL: "https://stale.test"}}, nil, 1, nil) assert.False(t, applied) assert.Empty(t, fs.allFavorites) assert.Empty(t, fs.displayRows) } func TestFavoritesSidebarDestroyedCallbacksNoOp(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.idleScheduler = func(cb glib.SourceFunc) { cb(0) } called := false fs.onNavigate = func(context.Context, string) error { called = true return nil } fs.destroyed = true fs.navigateToURL("https://example.test") fs.toggleTag(1) fs.onSearchChanged() assert.False(t, called) assert.Empty(t, fs.selectedTagIDs) } func TestFavoritesSidebarNavigationErrorSetsNotice(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.idleScheduler = func(cb glib.SourceFunc) { cb(0) } fs.onNavigate = func(context.Context, string) error { return errors.New("navigation failed") } fs.navigateToURL("https://example.test") assert.Equal(t, "navigation failed", fs.notice) } func TestFavoritesSidebarRenderingModelTagCallbackRetentionAndListRows(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.allTags = []*entity.Tag{{ID: 1, Name: "dev"}, {ID: 2, Name: "docs"}} fs.displayRows = buildFavoriteSidebarDisplayRows([]*entity.Favorite{ {ID: 1, URL: "https://go.dev", Title: "Go"}, {ID: 2, URL: "", Title: "Missing URL"}, }) assert.Len(t, fs.allTags, 2) require.Len(t, fs.displayRows, 2) assert.True(t, fs.displayRows[0].Selectable) assert.False(t, fs.displayRows[1].Selectable) assert.Equal(t, []string{"saved"}, favoriteSidebarNoticeRows("saved", "", true)) assert.Equal(t, []string{"No favorites"}, favoriteSidebarNoticeRows("", "", false)) } func TestFavoritesSidebarSlashFocusBehavior(t *testing.T) { assert.True(t, shouldFocusSearchForSlash(false)) assert.False(t, shouldFocusSearchForSlash(true)) } func TestFavoritesSidebarHandleEnterKeyDispatchesModifiers(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.idleScheduler = func(cb glib.SourceFunc) { cb(0) } fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, URL: "https://go.dev", Selectable: true}} var called []string fs.onNavigate = func(_ context.Context, url string) error { called = append(called, "nav:"+url) return nil } fs.onNavigateKeepOpen = func(_ context.Context, url string) error { called = append(called, "keep:"+url) return nil } fs.onOpenInNewPane = func(_ context.Context, url string) error { called = append(called, "pane:"+url) return nil } assert.True(t, fs.handleEnterKey(0)) assert.True(t, fs.handleEnterKey(gdk.ControlMaskValue)) assert.True(t, fs.handleEnterKey(gdk.ShiftMaskValue)) assert.Equal(t, []string{"nav:https://go.dev", "keep:https://go.dev", "pane:https://go.dev"}, called) } func TestFavoritesSidebarFocusZoneCyclingSkipsAbsentZones(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.searchEntry = nil fs.tagBox = nil fs.listBox = nil assert.Empty(t, fs.availableFocusZones()) fs.formBox = nil fs.mode = favoritesSidebarModeAdd fs.confirmDelete = true assert.Equal(t, []favoritesSidebarFocusZone{favoritesSidebarFocusConfirm}, fs.availableFocusZones()) } func TestFavoritesSidebarSingleKeyCommandsStartManagementOutsideTextEditContext(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.searchEntry = nil fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}} assert.True(t, fs.handleSingleKeyCommand(uint(gdk.KEY_a), 0)) assert.Equal(t, favoritesSidebarModeAdd, fs.mode) fs.mode = favoritesSidebarModeNone assert.True(t, fs.handleSingleKeyCommand(uint(gdk.KEY_e), 0)) assert.Equal(t, favoritesSidebarModeEdit, fs.mode) } func TestFavoritesSidebarAddAndEditDTOs(t *testing.T) { uc := &fakeFavoritesSidebarUC{} fs := newFavoritesSidebarHarness(nil, nil) fs.favoritesUC = uc fs.formURL = "https://add.test" fs.formTitle = "Added" fs.formTagIDs = map[entity.TagID]struct{}{1: {}, 2: {}} fs.formShortcut = "3" fs.mode = favoritesSidebarModeAdd assert.True(t, fs.submitForm()) require.Len(t, uc.addInputs, 1) assert.ElementsMatch(t, []entity.TagID{1, 2}, uc.addInputs[0].Tags) require.Len(t, uc.shortcuts, 1) assert.Equal(t, 3, *uc.shortcuts[0].key) fs.mode = favoritesSidebarModeEdit fs.editingID = 7 fs.formTitle = "Edited" fs.formShortcut = "4" assert.True(t, fs.submitForm()) require.Len(t, uc.updateInputs, 1) assert.Equal(t, entity.FavoriteID(7), uc.updateInputs[0].ID) assert.Equal(t, "Edited", uc.updateInputs[0].Title) require.NotNil(t, uc.updateInputs[0].ShortcutKey) assert.Equal(t, 4, *uc.updateInputs[0].ShortcutKey) fs.mode = favoritesSidebarModeEdit fs.editingID = 7 fs.formTitle = "Edited" fs.formShortcut = "" assert.True(t, fs.submitForm()) require.Len(t, uc.updateInputs, 2) assert.True(t, uc.updateInputs[1].ShortcutKeySet) assert.Nil(t, uc.updateInputs[1].ShortcutKey) } func TestFavoritesSidebarInvalidShortcutPreservesFormState(t *testing.T) { uc := &fakeFavoritesSidebarUC{} fs := newFavoritesSidebarHarness(nil, nil) fs.favoritesUC = uc fs.formURL = "https://add.test" fs.formTitle = "Added" fs.formShortcut = "10" fs.mode = favoritesSidebarModeAdd assert.True(t, fs.submitForm()) assert.Empty(t, uc.addInputs) assert.Equal(t, favoritesSidebarModeAdd, fs.mode) assert.Contains(t, fs.notice, "invalid shortcut") assert.Equal(t, "10", fs.formShortcut) } func TestFormTagCandidatesMatchesNamesAndKeepsSelectedTagsVisible(t *testing.T) { dev := &entity.Tag{ID: 10, Name: "dev"} news := &entity.Tag{ID: 11, Name: "news"} reading := &entity.Tag{ID: 12, Name: "reading"} selected := map[entity.TagID]struct{}{dev.ID: {}} assert.Equal(t, []*entity.Tag{dev}, formTagCandidates([]*entity.Tag{dev, news, reading}, selected, "")) assert.Equal(t, []*entity.Tag{reading}, formTagCandidates([]*entity.Tag{dev, news, reading}, selected, "read")) } func TestFavoritesSidebarReloadsAfterPartiallyApplyingTagChanges(t *testing.T) { first := entity.Tag{ID: 10, Name: "first"} second := entity.Tag{ID: 11, Name: "second"} fav := &entity.Favorite{ID: 5, URL: "https://go.dev", Tags: []entity.Tag{first, second}} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) uc.failUntagOnCall = 2 fs.allFavorites = []*entity.Favorite{fav} fs.loadGen = 4 assert.False(t, fs.updateFavoriteTags(fav.ID, nil)) assert.Len(t, uc.untagged, 2) assert.Equal(t, uint64(5), fs.loadGen, "a partial tag mutation must refresh stale favorites") assert.Equal(t, "untag failed", fs.notice) } func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { dev := entity.Tag{ID: 10, Name: "dev"} news := entity.Tag{ID: 11, Name: "news"} fav := &entity.Favorite{ID: 5, URL: "https://go.dev", Tags: []entity.Tag{dev}} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, []*entity.Tag{&dev, &news}) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) fs.allFavorites = []*entity.Favorite{fav} fs.formTagIDs = tagIDSet(fav.Tags) fs.toggleFormTag(news.ID) assert.ElementsMatch(t, []entity.TagID{dev.ID, news.ID}, fs.formTagIDsSnapshot()) assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) require.Len(t, uc.tagged, 1) assert.Equal(t, news.ID, uc.tagged[0].tagID) fav.Tags = []entity.Tag{dev, news} fs.toggleFormTag(dev.ID) assert.True(t, fs.updateFavoriteTags(fav.ID, fs.formTagIDsSnapshot())) require.Len(t, uc.untagged, 1) assert.Equal(t, dev.ID, uc.untagged[0].tagID) assert.Len(t, uc.tagged, 1, "the synchronized persisted tags must not be added again") } func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) { fav := &entity.Favorite{ID: 1, URL: "https://go.dev"} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) if !gtk.InitCheck() { t.Skip("GTK native display prerequisite unavailable (gtk.InitCheck returned false)") } listBox := gtk.NewListBox() require.NotNil(t, listBox) row := gtk.NewListBoxRow() require.NotNil(t, row) row.SetSelectable(true) listBox.Append(&row.Widget) fs.listBox = listBox fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} fs.focusZone = favoritesSidebarFocusSearch assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) assert.Equal(t, favoritesSidebarFocusList, fs.focusZone) assert.Equal(t, row, fs.listBox.GetSelectedRow()) assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) } func TestFavoritesSidebarDirectionalFocusConsumesArrowsOnTagControls(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.focusZone = favoritesSidebarFocusTags assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone) assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone) } func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) { fav := &entity.Favorite{ID: 5, URL: "https://go.dev"} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} fs.confirmDelete = true fs.confirmDeleteID = fav.ID assert.True(t, fs.confirmDeleteActive()) assert.True(t, fs.confirmDeleteFavorite()) assert.Equal(t, []entity.FavoriteID{5}, uc.deletedIDs) } func TestFavoritesSidebarCtrlEnterSubmitsFormPlainEnterDoesNot(t *testing.T) { uc := &fakeFavoritesSidebarUC{} fs := newFavoritesSidebarHarness(nil, nil) fs.favoritesUC = uc fs.formURL = "https://add.test" fs.formTitle = "Added" fs.mode = favoritesSidebarModeAdd fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, URL: "https://existing.test", Selectable: true}} fs.onNavigate = func(context.Context, string) error { t.Fatal("plain Enter in form mode must not activate selected row") return nil } assert.False(t, fs.handleReturnKey(0)) assert.Empty(t, uc.addInputs) fs.mode = favoritesSidebarModeAdd assert.True(t, fs.handleReturnKey(gdk.ControlMaskValue)) require.Len(t, uc.addInputs, 1) } func TestFavoritesSidebarEditSubmitIgnoresTagsByDesign(t *testing.T) { uc := &fakeFavoritesSidebarUC{} fs := newFavoritesSidebarHarness(nil, nil) fs.favoritesUC = uc fs.mode = favoritesSidebarModeEdit fs.editingID = 7 fs.formTitle = "Edited" assert.True(t, fs.submitForm()) require.Len(t, uc.updateInputs, 1) assert.Equal(t, entity.FavoriteID(7), uc.updateInputs[0].ID) assert.Empty(t, uc.tagged) assert.Empty(t, uc.untagged) } func TestFavoritesSidebarCreateTagTrimsNameAndUsesApplicationBoundary(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) assert.True(t, fs.createTag(" reading ")) require.Len(t, uc.createdTags, 1) assert.Equal(t, "reading", uc.createdTags[0].name) assert.Empty(t, uc.createdTags[0].color) assert.True(t, fs.createTag(" \t ")) assert.Len(t, uc.createdTags, 1) assert.Equal(t, "Tag name is required", fs.notice) } func TestFavoritesSidebarTagNameSubmissionCreatesTag(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) assert.True(t, fs.submitTagName(" work ")) require.Len(t, uc.createdTags, 1) assert.Equal(t, "work", uc.createdTags[0].name) } func TestFavoritesSidebarTagBindingTogglesExistingTag(t *testing.T) { dev := entity.Tag{ID: 10, Name: "dev"} news := entity.Tag{ID: 11, Name: "news"} fav := &entity.Favorite{ID: 5, URL: "https://go.dev", Tags: []entity.Tag{dev}} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, []*entity.Tag{&dev, &news}) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) fs.allFavorites = []*entity.Favorite{fav} fs.toggleFavoriteTag(fav.ID, dev.ID) require.Len(t, uc.untagged, 1) assert.Equal(t, dev.ID, uc.untagged[0].tagID) fs.toggleFavoriteTag(fav.ID, news.ID) require.Len(t, uc.tagged, 1) assert.Equal(t, news.ID, uc.tagged[0].tagID) } func TestFavoritesSidebarExplicitTextInputFocusControlsKeyboardRouting(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) input := >k.Widget{} fs.setTextInputFocused(input) assert.True(t, fs.inTextEditContext()) assert.True(t, shouldDeferToTextInput(fs.inTextEditContext(), uint(gdk.KEY_e))) assert.False(t, fs.handleSingleKeyCommand(uint(gdk.KEY_e), 0)) fs.mode = favoritesSidebarModeAdd assert.True(t, fs.cancelManagement()) assert.Equal(t, favoritesSidebarModeNone, fs.mode) fs.clearTextInputFocus(input) assert.False(t, fs.inTextEditContext()) assert.False(t, shouldDeferToTextInput(fs.inTextEditContext(), uint(gdk.KEY_e))) } func TestFavoritesSidebarTextInputDefersSidebarCommands(t *testing.T) { for _, key := range []uint{ uint(gdk.KEY_a), uint(gdk.KEY_e), uint(gdk.KEY_s), uint(gdk.KEY_r), uint(gdk.KEY_c), uint(gdk.KEY_t), uint(gdk.KEY_plus), uint(gdk.KEY_Up), uint(gdk.KEY_Return), } { assert.Truef(t, shouldDeferToTextInput(true, key), "key %d must reach the text input", key) } assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Tab))) assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_ISO_Left_Tab))) assert.False(t, shouldDeferToTextInput(true, uint(gdk.KEY_Escape))) assert.False(t, shouldDeferToTextInput(false, uint(gdk.KEY_e))) } func TestFavoritesSidebarPlusAndTDoNotHijackTextInput(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) assert.False(t, fs.handleSingleKeyCommand(uint(gdk.KEY_t), 0)) assert.False(t, fs.handleSingleKeyCommand(uint(gdk.KEY_plus), 0)) assert.Equal(t, favoritesSidebarModeNone, fs.mode) } func TestFavoritesSidebarSingleKeyCommandsIgnoreModifiedKeys(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) fs.searchEntry = nil assert.False(t, fs.handleSingleKeyCommand(uint(gdk.KEY_a), gdk.ControlMaskValue)) assert.Equal(t, favoritesSidebarModeNone, fs.mode) } func TestFavoritesSidebarConfirmDeleteRequiresConfirmationState(t *testing.T) { fav := &entity.Favorite{ID: 5, URL: "https://go.dev"} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} assert.True(t, fs.confirmDeleteFavorite()) assert.Empty(t, uc.deletedIDs) assert.True(t, fs.confirmDelete) } func TestFavoritesSidebarConfirmDeleteRepromptsWhenSelectionChanges(t *testing.T) { first := &entity.Favorite{ID: 5, URL: "https://first.test"} second := &entity.Favorite{ID: 6, URL: "https://second.test"} fs := newFavoritesSidebarHarness([]*entity.Favorite{first, second}, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) fs.displayRows = []favoriteSidebarDisplayRow{ {FavoriteID: second.ID, Favorite: second, URL: second.URL, Selectable: true}, {FavoriteID: first.ID, Favorite: first, URL: first.URL, Selectable: true}, } fs.confirmDelete = true fs.confirmDeleteID = first.ID assert.True(t, fs.confirmDeleteFavorite()) assert.Empty(t, uc.deletedIDs) assert.True(t, fs.confirmDelete) assert.Equal(t, second.ID, fs.confirmDeleteID) } func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) { uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")} fs := newFavoritesSidebarHarness(nil, nil) fs.favoritesUC = uc loaded := make(chan glib.SourceFunc, 1) fs.idleScheduler = func(cb glib.SourceFunc) { loaded <- cb } fs.formURL = "https://add.test" fs.formTitle = "Added" fs.formShortcut = "3" fs.mode = favoritesSidebarModeAdd assert.True(t, fs.submitForm()) require.Len(t, uc.addInputs, 1) select { case cb := <-loaded: cb(0) case <-time.After(time.Second): t.Fatal("timed out waiting for favorites reload") } assert.Equal(t, favoritesSidebarModeNone, fs.mode) assert.Contains(t, fs.notice, "failed to set shortcut") } func TestFavoritesSidebarShortcutAndDeleteFlows(t *testing.T) { dev := entity.Tag{ID: 10, Name: "dev"} fav := &entity.Favorite{ID: 5, URL: "https://go.dev", Tags: []entity.Tag{dev}} fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, []*entity.Tag{&dev}) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) fs.allTags = uc.tags fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} fs.mode = favoritesSidebarModeShortcut assert.True(t, fs.handleSingleKeyCommand(uint(gdk.KEY_9), 0)) assert.True(t, fs.handleSingleKeyCommand(uint(gdk.KEY_BackSpace), 0)) require.GreaterOrEqual(t, len(uc.shortcuts), 2) assert.Equal(t, 9, *uc.shortcuts[len(uc.shortcuts)-2].key) assert.Nil(t, uc.shortcuts[len(uc.shortcuts)-1].key) fs.mode = favoritesSidebarModeNone assert.True(t, fs.handleSingleKeyCommand(uint(gdk.KEY_Delete), 0)) assert.True(t, fs.confirmDelete) assert.True(t, fs.handleSingleKeyCommand(uint(gdk.KEY_Delete), 0)) assert.Equal(t, []entity.FavoriteID{5}, uc.deletedIDs) } func TestFavoritesSidebarErrorsPreserveStateAndShowNotice(t *testing.T) { fs := newFavoritesSidebarHarness(nil, nil) uc := fs.favoritesUC.(*fakeFavoritesSidebarUC) uc.err = errors.New("add failed") fs.mode = favoritesSidebarModeAdd assert.True(t, fs.submitForm()) assert.Equal(t, favoritesSidebarModeAdd, fs.mode) assert.Equal(t, "add failed", fs.notice) }