refactor(ui): extract omnibox favorite toggle helpers
- commit
- c031fea0aad12fcf336aa2594274ee5fa8b0f85a
- author
- brice · · 2026-06-25 12:49 UTC
- parent
- 0b6a0cf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +242 -13
M internal/ui/component/omnibox.go +89 -13
| @@ -39,6 +39,19 @@ type favoriteRowIndicatorUpdate struct { | |||
| 39 | 39 | ShowStarSlot bool | |
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | + | type favoriteToggleTarget struct { | |
| 43 | + | url string | |
| 44 | + | title string | |
| 45 | + | favoriteID int64 | |
| 46 | + | wasFavorite bool | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | type favoriteToggleResultUpdate struct { | |
| 50 | + | Apply bool | |
| 51 | + | Index int | |
| 52 | + | IsFavorite bool | |
| 53 | + | } | |
| 54 | + | ||
| 42 | 55 | // ViewMode distinguishes history search from favorites display. | |
| 43 | 56 | type ViewMode string | |
| 44 | 57 | ||
| @@ -2385,6 +2398,63 @@ func shouldPreferTypedURLNavigation(entryText string) bool { | |||
| 2385 | 2398 | return url.LooksLikeURL(entryText) | |
| 2386 | 2399 | } | |
| 2387 | 2400 | ||
| 2401 | + | func resolveFavoriteToggleTarget( | |
| 2402 | + | mode ViewMode, | |
| 2403 | + | idx int, | |
| 2404 | + | suggestions []Suggestion, | |
| 2405 | + | favorites []Favorite, | |
| 2406 | + | ) (favoriteToggleTarget, bool) { | |
| 2407 | + | if mode == ViewModeHistory { | |
| 2408 | + | if idx < 0 || idx >= len(suggestions) { | |
| 2409 | + | return favoriteToggleTarget{}, false | |
| 2410 | + | } | |
| 2411 | + | s := suggestions[idx] | |
| 2412 | + | if s.URL == "" { | |
| 2413 | + | return favoriteToggleTarget{}, false | |
| 2414 | + | } | |
| 2415 | + | return favoriteToggleTarget{ | |
| 2416 | + | url: s.URL, | |
| 2417 | + | title: s.Title, | |
| 2418 | + | wasFavorite: s.IsFavorite, | |
| 2419 | + | }, true | |
| 2420 | + | } | |
| 2421 | + | ||
| 2422 | + | if idx < 0 || idx >= len(favorites) { | |
| 2423 | + | return favoriteToggleTarget{}, false | |
| 2424 | + | } | |
| 2425 | + | f := favorites[idx] | |
| 2426 | + | if f.ID == 0 { | |
| 2427 | + | return favoriteToggleTarget{}, false | |
| 2428 | + | } | |
| 2429 | + | return favoriteToggleTarget{ | |
| 2430 | + | url: f.URL, | |
| 2431 | + | title: f.Title, | |
| 2432 | + | favoriteID: f.ID, | |
| 2433 | + | }, true | |
| 2434 | + | } | |
| 2435 | + | ||
| 2436 | + | func resolveFavoriteToggleResultUpdate( | |
| 2437 | + | suggestions []Suggestion, | |
| 2438 | + | index int, | |
| 2439 | + | expectedURL string, | |
| 2440 | + | isFavorite bool, | |
| 2441 | + | ) favoriteToggleResultUpdate { | |
| 2442 | + | if index < 0 || index >= len(suggestions) { | |
| 2443 | + | return favoriteToggleResultUpdate{} | |
| 2444 | + | } | |
| 2445 | + | if suggestions[index].URL != expectedURL { | |
| 2446 | + | return favoriteToggleResultUpdate{} | |
| 2447 | + | } | |
| 2448 | + | return favoriteToggleResultUpdate{Apply: true, Index: index, IsFavorite: isFavorite} | |
| 2449 | + | } | |
| 2450 | + | ||
| 2451 | + | func favoriteToggleErrorMessage(wasFavorite bool) string { | |
| 2452 | + | if wasFavorite { | |
| 2453 | + | return "Failed to remove favorite" | |
| 2454 | + | } | |
| 2455 | + | return "Failed to add favorite" | |
| 2456 | + | } | |
| 2457 | + | ||
| 2388 | 2458 | // toggleFavorite adds or removes the selected item from favorites. | |
| 2389 | 2459 | // In History mode: adds the selected item to favorites | |
| 2390 | 2460 | // In Favorites mode: removes the selected item from favorites | |
| @@ -2415,19 +2485,20 @@ func (o *Omnibox) toggleFavorite() { | |||
| 2415 | 2485 | log.Debug().Msg("toggle favorite: empty URL") | |
| 2416 | 2486 | return | |
| 2417 | 2487 | } | |
| 2488 | + | target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites) | |
| 2489 | + | if !ok { | |
| 2490 | + | return | |
| 2491 | + | } | |
| 2418 | 2492 | ||
| 2419 | 2493 | go func() { | |
| 2420 | 2494 | ctx := o.ctx | |
| 2421 | 2495 | goLog := logging.FromContext(ctx) | |
| 2422 | 2496 | ||
| 2423 | - | result, err := o.favoritesUC.Toggle(ctx, s.URL, s.Title) | |
| 2497 | + | result, err := o.favoritesUC.Toggle(ctx, target.url, target.title) | |
| 2424 | 2498 | if err != nil { | |
| 2425 | - | goLog.Error().Err(err).Str("url", s.URL).Msg("failed to toggle favorite") | |
| 2499 | + | goLog.Error().Err(err).Str("url", target.url).Msg("failed to toggle favorite") | |
| 2426 | 2500 | if o.onToast != nil { | |
| 2427 | - | msg := "Failed to add favorite" | |
| 2428 | - | if s.IsFavorite { | |
| 2429 | - | msg = "Failed to remove favorite" | |
| 2430 | - | } | |
| 2501 | + | msg := favoriteToggleErrorMessage(target.wasFavorite) | |
| 2431 | 2502 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 2432 | 2503 | o.onToast(ctx, msg, ToastError) | |
| 2433 | 2504 | return false | |
| @@ -2439,14 +2510,15 @@ func (o *Omnibox) toggleFavorite() { | |||
| 2439 | 2510 | ||
| 2440 | 2511 | // Update suggestion state | |
| 2441 | 2512 | o.mu.Lock() | |
| 2442 | - | if idx < len(o.suggestions) && o.suggestions[idx].URL == s.URL { | |
| 2443 | - | o.suggestions[idx].IsFavorite = result.Added | |
| 2513 | + | update := resolveFavoriteToggleResultUpdate(o.suggestions, idx, target.url, result.Added) | |
| 2514 | + | if update.Apply { | |
| 2515 | + | o.suggestions[update.Index].IsFavorite = update.IsFavorite | |
| 2444 | 2516 | } | |
| 2445 | 2517 | o.mu.Unlock() | |
| 2446 | 2518 | ||
| 2447 | 2519 | // Update row CSS and show toast on GTK main thread | |
| 2448 | 2520 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 2449 | - | o.updateRowFavoriteIndicator(idx, s.URL, result.Added) | |
| 2521 | + | o.updateRowFavoriteIndicator(idx, target.url, result.Added) | |
| 2450 | 2522 | if o.onToast != nil { | |
| 2451 | 2523 | o.onToast(ctx, result.Message, ToastSuccess) | |
| 2452 | 2524 | } | |
| @@ -2466,17 +2538,21 @@ func (o *Omnibox) toggleFavorite() { | |||
| 2466 | 2538 | log.Debug().Msg("remove favorite: invalid ID") | |
| 2467 | 2539 | return | |
| 2468 | 2540 | } | |
| 2541 | + | target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites) | |
| 2542 | + | if !ok { | |
| 2543 | + | return | |
| 2544 | + | } | |
| 2469 | 2545 | entryText := o.entry.GetText() | |
| 2470 | 2546 | ||
| 2471 | 2547 | go func() { | |
| 2472 | 2548 | ctx := o.ctx | |
| 2473 | 2549 | log := logging.FromContext(ctx) | |
| 2474 | 2550 | ||
| 2475 | - | log.Debug().Int64("id", f.ID).Str("url", f.URL).Msg("removing from favorites") | |
| 2551 | + | log.Debug().Int64("id", target.favoriteID).Str("url", target.url).Msg("removing from favorites") | |
| 2476 | 2552 | ||
| 2477 | - | err := o.favoritesUC.Remove(ctx, entity.FavoriteID(f.ID)) | |
| 2553 | + | err := o.favoritesUC.Remove(ctx, entity.FavoriteID(target.favoriteID)) | |
| 2478 | 2554 | if err != nil { | |
| 2479 | - | log.Error().Err(err).Int64("id", f.ID).Msg("failed to remove favorite") | |
| 2555 | + | log.Error().Err(err).Int64("id", target.favoriteID).Msg("failed to remove favorite") | |
| 2480 | 2556 | // Show error toast | |
| 2481 | 2557 | if o.onToast != nil { | |
| 2482 | 2558 | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| @@ -2488,7 +2564,7 @@ func (o *Omnibox) toggleFavorite() { | |||
| 2488 | 2564 | return | |
| 2489 | 2565 | } | |
| 2490 | 2566 | ||
| 2491 | - | log.Info().Int64("id", f.ID).Str("url", f.URL).Msg("favorite removed from omnibox") | |
| 2567 | + | log.Info().Int64("id", target.favoriteID).Str("url", target.url).Msg("favorite removed from omnibox") | |
| 2492 | 2568 | ||
| 2493 | 2569 | // Refresh favorites list and show toast | |
| 2494 | 2570 | o.mu.RLock() | |
| @@ -39,6 +39,19 @@ type favoriteRowIndicatorUpdate struct { | |||||
| 39 | ShowStarSlot bool | 39 | ShowStarSlot bool | ||
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | + | type favoriteToggleTarget struct { | |||
| 43 | + | url string | |||
| 44 | + | title string | |||
| 45 | + | favoriteID int64 | |||
| 46 | + | wasFavorite bool | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | type favoriteToggleResultUpdate struct { | |||
| 50 | + | Apply bool | |||
| 51 | + | Index int | |||
| 52 | + | IsFavorite bool | |||
| 53 | + | } | |||
| 54 | + | ||||
| 42 | // ViewMode distinguishes history search from favorites display. | 55 | // ViewMode distinguishes history search from favorites display. | ||
| 43 | type ViewMode string | 56 | type ViewMode string | ||
| 44 | 57 | ||||
| @@ -2385,6 +2398,63 @@ func shouldPreferTypedURLNavigation(entryText string) bool { | |||||
| 2385 | return url.LooksLikeURL(entryText) | 2398 | return url.LooksLikeURL(entryText) | ||
| 2386 | } | 2399 | } | ||
| 2387 | 2400 | ||||
| 2401 | + | func resolveFavoriteToggleTarget( | |||
| 2402 | + | mode ViewMode, | |||
| 2403 | + | idx int, | |||
| 2404 | + | suggestions []Suggestion, | |||
| 2405 | + | favorites []Favorite, | |||
| 2406 | + | ) (favoriteToggleTarget, bool) { | |||
| 2407 | + | if mode == ViewModeHistory { | |||
| 2408 | + | if idx < 0 || idx >= len(suggestions) { | |||
| 2409 | + | return favoriteToggleTarget{}, false | |||
| 2410 | + | } | |||
| 2411 | + | s := suggestions[idx] | |||
| 2412 | + | if s.URL == "" { | |||
| 2413 | + | return favoriteToggleTarget{}, false | |||
| 2414 | + | } | |||
| 2415 | + | return favoriteToggleTarget{ | |||
| 2416 | + | url: s.URL, | |||
| 2417 | + | title: s.Title, | |||
| 2418 | + | wasFavorite: s.IsFavorite, | |||
| 2419 | + | }, true | |||
| 2420 | + | } | |||
| 2421 | + | ||||
| 2422 | + | if idx < 0 || idx >= len(favorites) { | |||
| 2423 | + | return favoriteToggleTarget{}, false | |||
| 2424 | + | } | |||
| 2425 | + | f := favorites[idx] | |||
| 2426 | + | if f.ID == 0 { | |||
| 2427 | + | return favoriteToggleTarget{}, false | |||
| 2428 | + | } | |||
| 2429 | + | return favoriteToggleTarget{ | |||
| 2430 | + | url: f.URL, | |||
| 2431 | + | title: f.Title, | |||
| 2432 | + | favoriteID: f.ID, | |||
| 2433 | + | }, true | |||
| 2434 | + | } | |||
| 2435 | + | ||||
| 2436 | + | func resolveFavoriteToggleResultUpdate( | |||
| 2437 | + | suggestions []Suggestion, | |||
| 2438 | + | index int, | |||
| 2439 | + | expectedURL string, | |||
| 2440 | + | isFavorite bool, | |||
| 2441 | + | ) favoriteToggleResultUpdate { | |||
| 2442 | + | if index < 0 || index >= len(suggestions) { | |||
| 2443 | + | return favoriteToggleResultUpdate{} | |||
| 2444 | + | } | |||
| 2445 | + | if suggestions[index].URL != expectedURL { | |||
| 2446 | + | return favoriteToggleResultUpdate{} | |||
| 2447 | + | } | |||
| 2448 | + | return favoriteToggleResultUpdate{Apply: true, Index: index, IsFavorite: isFavorite} | |||
| 2449 | + | } | |||
| 2450 | + | ||||
| 2451 | + | func favoriteToggleErrorMessage(wasFavorite bool) string { | |||
| 2452 | + | if wasFavorite { | |||
| 2453 | + | return "Failed to remove favorite" | |||
| 2454 | + | } | |||
| 2455 | + | return "Failed to add favorite" | |||
| 2456 | + | } | |||
| 2457 | + | ||||
| 2388 | // toggleFavorite adds or removes the selected item from favorites. | 2458 | // toggleFavorite adds or removes the selected item from favorites. | ||
| 2389 | // In History mode: adds the selected item to favorites | 2459 | // In History mode: adds the selected item to favorites | ||
| 2390 | // In Favorites mode: removes the selected item from favorites | 2460 | // In Favorites mode: removes the selected item from favorites | ||
| @@ -2415,19 +2485,20 @@ func (o *Omnibox) toggleFavorite() { | |||||
| 2415 | log.Debug().Msg("toggle favorite: empty URL") | 2485 | log.Debug().Msg("toggle favorite: empty URL") | ||
| 2416 | return | 2486 | return | ||
| 2417 | } | 2487 | } | ||
| 2488 | + | target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites) | |||
| 2489 | + | if !ok { | |||
| 2490 | + | return | |||
| 2491 | + | } | |||
| 2418 | 2492 | ||||
| 2419 | go func() { | 2493 | go func() { | ||
| 2420 | ctx := o.ctx | 2494 | ctx := o.ctx | ||
| 2421 | goLog := logging.FromContext(ctx) | 2495 | goLog := logging.FromContext(ctx) | ||
| 2422 | 2496 | ||||
| 2423 | - | result, err := o.favoritesUC.Toggle(ctx, s.URL, s.Title) | 2497 | + | result, err := o.favoritesUC.Toggle(ctx, target.url, target.title) |
| 2424 | if err != nil { | 2498 | if err != nil { | ||
| 2425 | - | goLog.Error().Err(err).Str("url", s.URL).Msg("failed to toggle favorite") | 2499 | + | goLog.Error().Err(err).Str("url", target.url).Msg("failed to toggle favorite") |
| 2426 | if o.onToast != nil { | 2500 | if o.onToast != nil { | ||
| 2427 | - | msg := "Failed to add favorite" | 2501 | + | msg := favoriteToggleErrorMessage(target.wasFavorite) |
| 2428 | - | if s.IsFavorite { | |||
| 2429 | - | msg = "Failed to remove favorite" | |||
| 2430 | - | } | |||
| 2431 | cb := glib.SourceFunc(func(_ uintptr) bool { | 2502 | cb := glib.SourceFunc(func(_ uintptr) bool { | ||
| 2432 | o.onToast(ctx, msg, ToastError) | 2503 | o.onToast(ctx, msg, ToastError) | ||
| 2433 | return false | 2504 | return false | ||
| @@ -2439,14 +2510,15 @@ func (o *Omnibox) toggleFavorite() { | |||||
| 2439 | 2510 | ||||
| 2440 | // Update suggestion state | 2511 | // Update suggestion state | ||
| 2441 | o.mu.Lock() | 2512 | o.mu.Lock() | ||
| 2442 | - | if idx < len(o.suggestions) && o.suggestions[idx].URL == s.URL { | 2513 | + | update := resolveFavoriteToggleResultUpdate(o.suggestions, idx, target.url, result.Added) |
| 2443 | - | o.suggestions[idx].IsFavorite = result.Added | 2514 | + | if update.Apply { |
| 2515 | + | o.suggestions[update.Index].IsFavorite = update.IsFavorite | |||
| 2444 | } | 2516 | } | ||
| 2445 | o.mu.Unlock() | 2517 | o.mu.Unlock() | ||
| 2446 | 2518 | ||||
| 2447 | // Update row CSS and show toast on GTK main thread | 2519 | // Update row CSS and show toast on GTK main thread | ||
| 2448 | cb := glib.SourceFunc(func(_ uintptr) bool { | 2520 | cb := glib.SourceFunc(func(_ uintptr) bool { | ||
| 2449 | - | o.updateRowFavoriteIndicator(idx, s.URL, result.Added) | 2521 | + | o.updateRowFavoriteIndicator(idx, target.url, result.Added) |
| 2450 | if o.onToast != nil { | 2522 | if o.onToast != nil { | ||
| 2451 | o.onToast(ctx, result.Message, ToastSuccess) | 2523 | o.onToast(ctx, result.Message, ToastSuccess) | ||
| 2452 | } | 2524 | } | ||
| @@ -2466,17 +2538,21 @@ func (o *Omnibox) toggleFavorite() { | |||||
| 2466 | log.Debug().Msg("remove favorite: invalid ID") | 2538 | log.Debug().Msg("remove favorite: invalid ID") | ||
| 2467 | return | 2539 | return | ||
| 2468 | } | 2540 | } | ||
| 2541 | + | target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites) | |||
| 2542 | + | if !ok { | |||
| 2543 | + | return | |||
| 2544 | + | } | |||
| 2469 | entryText := o.entry.GetText() | 2545 | entryText := o.entry.GetText() | ||
| 2470 | 2546 | ||||
| 2471 | go func() { | 2547 | go func() { | ||
| 2472 | ctx := o.ctx | 2548 | ctx := o.ctx | ||
| 2473 | log := logging.FromContext(ctx) | 2549 | log := logging.FromContext(ctx) | ||
| 2474 | 2550 | ||||
| 2475 | - | log.Debug().Int64("id", f.ID).Str("url", f.URL).Msg("removing from favorites") | 2551 | + | log.Debug().Int64("id", target.favoriteID).Str("url", target.url).Msg("removing from favorites") |
| 2476 | 2552 | ||||
| 2477 | - | err := o.favoritesUC.Remove(ctx, entity.FavoriteID(f.ID)) | 2553 | + | err := o.favoritesUC.Remove(ctx, entity.FavoriteID(target.favoriteID)) |
| 2478 | if err != nil { | 2554 | if err != nil { | ||
| 2479 | - | log.Error().Err(err).Int64("id", f.ID).Msg("failed to remove favorite") | 2555 | + | log.Error().Err(err).Int64("id", target.favoriteID).Msg("failed to remove favorite") |
| 2480 | // Show error toast | 2556 | // Show error toast | ||
| 2481 | if o.onToast != nil { | 2557 | if o.onToast != nil { | ||
| 2482 | cb := glib.SourceFunc(func(_ uintptr) bool { | 2558 | cb := glib.SourceFunc(func(_ uintptr) bool { | ||
| @@ -2488,7 +2564,7 @@ func (o *Omnibox) toggleFavorite() { | |||||
| 2488 | return | 2564 | return | ||
| 2489 | } | 2565 | } | ||
| 2490 | 2566 | ||||
| 2491 | - | log.Info().Int64("id", f.ID).Str("url", f.URL).Msg("favorite removed from omnibox") | 2567 | + | log.Info().Int64("id", target.favoriteID).Str("url", target.url).Msg("favorite removed from omnibox") |
| 2492 | 2568 | ||||
| 2493 | // Refresh favorites list and show toast | 2569 | // Refresh favorites list and show toast | ||
| 2494 | o.mu.RLock() | 2570 | o.mu.RLock() | ||
A internal/ui/component/omnibox_favorite_toggle_test.go +153 -0
| @@ -0,0 +1,153 @@ | |||
| 1 | + | package component | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/assert" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestResolveFavoriteToggleTarget(t *testing.T) { | |
| 10 | + | suggestions := []Suggestion{ | |
| 11 | + | {URL: "https://example.com", Title: "Example"}, | |
| 12 | + | {URL: "https://favorite.example.com", Title: "Favorite", IsFavorite: true}, | |
| 13 | + | } | |
| 14 | + | favorites := []Favorite{ | |
| 15 | + | {ID: 42, URL: "https://saved.example.com", Title: "Saved"}, | |
| 16 | + | {ID: 0, URL: "https://invalid.example.com", Title: "Invalid"}, | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | tests := []struct { | |
| 20 | + | name string | |
| 21 | + | mode ViewMode | |
| 22 | + | index int | |
| 23 | + | suggestions []Suggestion | |
| 24 | + | favorites []Favorite | |
| 25 | + | wantTarget favoriteToggleTarget | |
| 26 | + | wantValidTarget bool | |
| 27 | + | }{ | |
| 28 | + | { | |
| 29 | + | name: "history add target uses selected suggestion URL and title", | |
| 30 | + | mode: ViewModeHistory, | |
| 31 | + | index: 0, | |
| 32 | + | suggestions: suggestions, | |
| 33 | + | favorites: favorites, | |
| 34 | + | wantTarget: favoriteToggleTarget{ | |
| 35 | + | url: "https://example.com", | |
| 36 | + | title: "Example", | |
| 37 | + | }, | |
| 38 | + | wantValidTarget: true, | |
| 39 | + | }, | |
| 40 | + | { | |
| 41 | + | name: "history remove target preserves prior favorite state", | |
| 42 | + | mode: ViewModeHistory, | |
| 43 | + | index: 1, | |
| 44 | + | suggestions: suggestions, | |
| 45 | + | favorites: favorites, | |
| 46 | + | wantTarget: favoriteToggleTarget{ | |
| 47 | + | url: "https://favorite.example.com", | |
| 48 | + | title: "Favorite", | |
| 49 | + | wasFavorite: true, | |
| 50 | + | }, | |
| 51 | + | wantValidTarget: true, | |
| 52 | + | }, | |
| 53 | + | { | |
| 54 | + | name: "history invalid index has no target", | |
| 55 | + | mode: ViewModeHistory, | |
| 56 | + | index: 2, | |
| 57 | + | suggestions: suggestions, | |
| 58 | + | favorites: favorites, | |
| 59 | + | wantValidTarget: false, | |
| 60 | + | }, | |
| 61 | + | { | |
| 62 | + | name: "history empty URL has no target", | |
| 63 | + | mode: ViewModeHistory, | |
| 64 | + | index: 0, | |
| 65 | + | suggestions: []Suggestion{{Title: "Missing URL"}}, | |
| 66 | + | favorites: favorites, | |
| 67 | + | wantValidTarget: false, | |
| 68 | + | }, | |
| 69 | + | { | |
| 70 | + | name: "favorites remove target uses favorite ID", | |
| 71 | + | mode: ViewModeFavorites, | |
| 72 | + | index: 0, | |
| 73 | + | suggestions: suggestions, | |
| 74 | + | favorites: favorites, | |
| 75 | + | wantTarget: favoriteToggleTarget{ | |
| 76 | + | url: "https://saved.example.com", | |
| 77 | + | title: "Saved", | |
| 78 | + | favoriteID: 42, | |
| 79 | + | }, | |
| 80 | + | wantValidTarget: true, | |
| 81 | + | }, | |
| 82 | + | { | |
| 83 | + | name: "favorites invalid index has no target", | |
| 84 | + | mode: ViewModeFavorites, | |
| 85 | + | index: -1, | |
| 86 | + | suggestions: suggestions, | |
| 87 | + | favorites: favorites, | |
| 88 | + | wantValidTarget: false, | |
| 89 | + | }, | |
| 90 | + | { | |
| 91 | + | name: "favorites zero ID has no target", | |
| 92 | + | mode: ViewModeFavorites, | |
| 93 | + | index: 1, | |
| 94 | + | suggestions: suggestions, | |
| 95 | + | favorites: favorites, | |
| 96 | + | wantValidTarget: false, | |
| 97 | + | }, | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | for _, tt := range tests { | |
| 101 | + | t.Run(tt.name, func(t *testing.T) { | |
| 102 | + | gotTarget, gotValidTarget := resolveFavoriteToggleTarget(tt.mode, tt.index, tt.suggestions, tt.favorites) | |
| 103 | + | assert.Equal(t, tt.wantValidTarget, gotValidTarget) | |
| 104 | + | assert.Equal(t, tt.wantTarget, gotTarget) | |
| 105 | + | }) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func TestResolveFavoriteToggleResultUpdate(t *testing.T) { | |
| 110 | + | suggestions := []Suggestion{{URL: "https://example.com"}} | |
| 111 | + | ||
| 112 | + | tests := []struct { | |
| 113 | + | name string | |
| 114 | + | index int | |
| 115 | + | expectedURL string | |
| 116 | + | added bool | |
| 117 | + | want favoriteToggleResultUpdate | |
| 118 | + | }{ | |
| 119 | + | { | |
| 120 | + | name: "matching URL updates selected suggestion favorite state", | |
| 121 | + | index: 0, | |
| 122 | + | expectedURL: "https://example.com", | |
| 123 | + | added: true, | |
| 124 | + | want: favoriteToggleResultUpdate{Apply: true, Index: 0, IsFavorite: true}, | |
| 125 | + | }, | |
| 126 | + | { | |
| 127 | + | name: "stale URL skips update", | |
| 128 | + | index: 0, | |
| 129 | + | expectedURL: "https://stale.example.com", | |
| 130 | + | added: true, | |
| 131 | + | want: favoriteToggleResultUpdate{}, | |
| 132 | + | }, | |
| 133 | + | { | |
| 134 | + | name: "out of range index skips update", | |
| 135 | + | index: 1, | |
| 136 | + | expectedURL: "https://example.com", | |
| 137 | + | added: true, | |
| 138 | + | want: favoriteToggleResultUpdate{}, | |
| 139 | + | }, | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | for _, tt := range tests { | |
| 143 | + | t.Run(tt.name, func(t *testing.T) { | |
| 144 | + | got := resolveFavoriteToggleResultUpdate(suggestions, tt.index, tt.expectedURL, tt.added) | |
| 145 | + | assert.Equal(t, tt.want, got) | |
| 146 | + | }) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func TestFavoriteToggleErrorMessage(t *testing.T) { | |
| 151 | + | assert.Equal(t, "Failed to add favorite", favoriteToggleErrorMessage(false)) | |
| 152 | + | assert.Equal(t, "Failed to remove favorite", favoriteToggleErrorMessage(true)) | |
| 153 | + | } | |
| @@ -0,0 +1,153 @@ | |||||
| 1 | + | package component | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/assert" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestResolveFavoriteToggleTarget(t *testing.T) { | |||
| 10 | + | suggestions := []Suggestion{ | |||
| 11 | + | {URL: "https://example.com", Title: "Example"}, | |||
| 12 | + | {URL: "https://favorite.example.com", Title: "Favorite", IsFavorite: true}, | |||
| 13 | + | } | |||
| 14 | + | favorites := []Favorite{ | |||
| 15 | + | {ID: 42, URL: "https://saved.example.com", Title: "Saved"}, | |||
| 16 | + | {ID: 0, URL: "https://invalid.example.com", Title: "Invalid"}, | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | tests := []struct { | |||
| 20 | + | name string | |||
| 21 | + | mode ViewMode | |||
| 22 | + | index int | |||
| 23 | + | suggestions []Suggestion | |||
| 24 | + | favorites []Favorite | |||
| 25 | + | wantTarget favoriteToggleTarget | |||
| 26 | + | wantValidTarget bool | |||
| 27 | + | }{ | |||
| 28 | + | { | |||
| 29 | + | name: "history add target uses selected suggestion URL and title", | |||
| 30 | + | mode: ViewModeHistory, | |||
| 31 | + | index: 0, | |||
| 32 | + | suggestions: suggestions, | |||
| 33 | + | favorites: favorites, | |||
| 34 | + | wantTarget: favoriteToggleTarget{ | |||
| 35 | + | url: "https://example.com", | |||
| 36 | + | title: "Example", | |||
| 37 | + | }, | |||
| 38 | + | wantValidTarget: true, | |||
| 39 | + | }, | |||
| 40 | + | { | |||
| 41 | + | name: "history remove target preserves prior favorite state", | |||
| 42 | + | mode: ViewModeHistory, | |||
| 43 | + | index: 1, | |||
| 44 | + | suggestions: suggestions, | |||
| 45 | + | favorites: favorites, | |||
| 46 | + | wantTarget: favoriteToggleTarget{ | |||
| 47 | + | url: "https://favorite.example.com", | |||
| 48 | + | title: "Favorite", | |||
| 49 | + | wasFavorite: true, | |||
| 50 | + | }, | |||
| 51 | + | wantValidTarget: true, | |||
| 52 | + | }, | |||
| 53 | + | { | |||
| 54 | + | name: "history invalid index has no target", | |||
| 55 | + | mode: ViewModeHistory, | |||
| 56 | + | index: 2, | |||
| 57 | + | suggestions: suggestions, | |||
| 58 | + | favorites: favorites, | |||
| 59 | + | wantValidTarget: false, | |||
| 60 | + | }, | |||
| 61 | + | { | |||
| 62 | + | name: "history empty URL has no target", | |||
| 63 | + | mode: ViewModeHistory, | |||
| 64 | + | index: 0, | |||
| 65 | + | suggestions: []Suggestion{{Title: "Missing URL"}}, | |||
| 66 | + | favorites: favorites, | |||
| 67 | + | wantValidTarget: false, | |||
| 68 | + | }, | |||
| 69 | + | { | |||
| 70 | + | name: "favorites remove target uses favorite ID", | |||
| 71 | + | mode: ViewModeFavorites, | |||
| 72 | + | index: 0, | |||
| 73 | + | suggestions: suggestions, | |||
| 74 | + | favorites: favorites, | |||
| 75 | + | wantTarget: favoriteToggleTarget{ | |||
| 76 | + | url: "https://saved.example.com", | |||
| 77 | + | title: "Saved", | |||
| 78 | + | favoriteID: 42, | |||
| 79 | + | }, | |||
| 80 | + | wantValidTarget: true, | |||
| 81 | + | }, | |||
| 82 | + | { | |||
| 83 | + | name: "favorites invalid index has no target", | |||
| 84 | + | mode: ViewModeFavorites, | |||
| 85 | + | index: -1, | |||
| 86 | + | suggestions: suggestions, | |||
| 87 | + | favorites: favorites, | |||
| 88 | + | wantValidTarget: false, | |||
| 89 | + | }, | |||
| 90 | + | { | |||
| 91 | + | name: "favorites zero ID has no target", | |||
| 92 | + | mode: ViewModeFavorites, | |||
| 93 | + | index: 1, | |||
| 94 | + | suggestions: suggestions, | |||
| 95 | + | favorites: favorites, | |||
| 96 | + | wantValidTarget: false, | |||
| 97 | + | }, | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | for _, tt := range tests { | |||
| 101 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 102 | + | gotTarget, gotValidTarget := resolveFavoriteToggleTarget(tt.mode, tt.index, tt.suggestions, tt.favorites) | |||
| 103 | + | assert.Equal(t, tt.wantValidTarget, gotValidTarget) | |||
| 104 | + | assert.Equal(t, tt.wantTarget, gotTarget) | |||
| 105 | + | }) | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func TestResolveFavoriteToggleResultUpdate(t *testing.T) { | |||
| 110 | + | suggestions := []Suggestion{{URL: "https://example.com"}} | |||
| 111 | + | ||||
| 112 | + | tests := []struct { | |||
| 113 | + | name string | |||
| 114 | + | index int | |||
| 115 | + | expectedURL string | |||
| 116 | + | added bool | |||
| 117 | + | want favoriteToggleResultUpdate | |||
| 118 | + | }{ | |||
| 119 | + | { | |||
| 120 | + | name: "matching URL updates selected suggestion favorite state", | |||
| 121 | + | index: 0, | |||
| 122 | + | expectedURL: "https://example.com", | |||
| 123 | + | added: true, | |||
| 124 | + | want: favoriteToggleResultUpdate{Apply: true, Index: 0, IsFavorite: true}, | |||
| 125 | + | }, | |||
| 126 | + | { | |||
| 127 | + | name: "stale URL skips update", | |||
| 128 | + | index: 0, | |||
| 129 | + | expectedURL: "https://stale.example.com", | |||
| 130 | + | added: true, | |||
| 131 | + | want: favoriteToggleResultUpdate{}, | |||
| 132 | + | }, | |||
| 133 | + | { | |||
| 134 | + | name: "out of range index skips update", | |||
| 135 | + | index: 1, | |||
| 136 | + | expectedURL: "https://example.com", | |||
| 137 | + | added: true, | |||
| 138 | + | want: favoriteToggleResultUpdate{}, | |||
| 139 | + | }, | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | for _, tt := range tests { | |||
| 143 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 144 | + | got := resolveFavoriteToggleResultUpdate(suggestions, tt.index, tt.expectedURL, tt.added) | |||
| 145 | + | assert.Equal(t, tt.want, got) | |||
| 146 | + | }) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | func TestFavoriteToggleErrorMessage(t *testing.T) { | |||
| 151 | + | assert.Equal(t, "Failed to add favorite", favoriteToggleErrorMessage(false)) | |||
| 152 | + | assert.Equal(t, "Failed to remove favorite", favoriteToggleErrorMessage(true)) | |||
| 153 | + | } | |||