refactor(history): use application history port
- commit
- 9a9b787cfbcd2c9c0ae0ea31d81e27b0967339ee
- author
- brice · · 2026-06-13 06:25 UTC
- parent
- bf8765a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +65 -114
M internal/ui/browser_window.go +10 -6
| @@ -301,7 +301,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) { | |||
| 301 | 301 | // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given | |
| 302 | 302 | // browser window. Extracted from initHistorySidebar for testability. | |
| 303 | 303 | func (a *App) buildHistorySidebarConfig(ctx context.Context, bw *browserWindow) component.HistorySidebarConfig { | |
| 304 | - | var historyUC *usecase.SearchHistoryUseCase | |
| 304 | + | var historyUC port.HomepageHistory | |
| 305 | 305 | if a.deps != nil { | |
| 306 | 306 | historyUC = a.deps.HistoryUC | |
| 307 | 307 | } | |
| @@ -363,15 +363,19 @@ func (bw *browserWindow) showHistorySidebar(widthCfg ...window.SidebarWidthConfi | |||
| 363 | 363 | // applySidebarWidthConfig extracts the config-backed sidebar width and | |
| 364 | 364 | // applies it via the MainWindow.SetSidebarWidth path. It is called during | |
| 365 | 365 | // initialization and can be reused if config is reloaded at runtime. | |
| 366 | + | func historySidebarWidthConfig(widthPx int) window.SidebarWidthConfig { | |
| 367 | + | cfg := window.SidebarDefaultWidth() | |
| 368 | + | if widthPx > 0 { | |
| 369 | + | cfg.WidthPx = widthPx | |
| 370 | + | } | |
| 371 | + | return cfg | |
| 372 | + | } | |
| 373 | + | ||
| 366 | 374 | func (bw *browserWindow) applySidebarWidthConfig(a *App) { | |
| 367 | 375 | if bw == nil || bw.mainWindow == nil || a == nil || a.deps == nil || a.deps.Config == nil { | |
| 368 | 376 | return | |
| 369 | 377 | } | |
| 370 | - | widthCfg := window.SidebarDefaultWidth() | |
| 371 | - | if w := a.deps.Config.SidebarWidth; w > 0 { | |
| 372 | - | widthCfg.WidthPx = w | |
| 373 | - | } | |
| 374 | - | bw.mainWindow.SetSidebarWidth(widthCfg) | |
| 378 | + | bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.deps.Config.SidebarWidth)) | |
| 375 | 379 | } | |
| 376 | 380 | ||
| 377 | 381 | // hideHistorySidebar hides the sidebar. Callers should also restore focus | |
| @@ -301,7 +301,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) { | |||||
| 301 | // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given | 301 | // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given | ||
| 302 | // browser window. Extracted from initHistorySidebar for testability. | 302 | // browser window. Extracted from initHistorySidebar for testability. | ||
| 303 | func (a *App) buildHistorySidebarConfig(ctx context.Context, bw *browserWindow) component.HistorySidebarConfig { | 303 | func (a *App) buildHistorySidebarConfig(ctx context.Context, bw *browserWindow) component.HistorySidebarConfig { | ||
| 304 | - | var historyUC *usecase.SearchHistoryUseCase | 304 | + | var historyUC port.HomepageHistory |
| 305 | if a.deps != nil { | 305 | if a.deps != nil { | ||
| 306 | historyUC = a.deps.HistoryUC | 306 | historyUC = a.deps.HistoryUC | ||
| 307 | } | 307 | } | ||
| @@ -363,15 +363,19 @@ func (bw *browserWindow) showHistorySidebar(widthCfg ...window.SidebarWidthConfi | |||||
| 363 | // applySidebarWidthConfig extracts the config-backed sidebar width and | 363 | // applySidebarWidthConfig extracts the config-backed sidebar width and | ||
| 364 | // applies it via the MainWindow.SetSidebarWidth path. It is called during | 364 | // applies it via the MainWindow.SetSidebarWidth path. It is called during | ||
| 365 | // initialization and can be reused if config is reloaded at runtime. | 365 | // initialization and can be reused if config is reloaded at runtime. | ||
| 366 | + | func historySidebarWidthConfig(widthPx int) window.SidebarWidthConfig { | |||
| 367 | + | cfg := window.SidebarDefaultWidth() | |||
| 368 | + | if widthPx > 0 { | |||
| 369 | + | cfg.WidthPx = widthPx | |||
| 370 | + | } | |||
| 371 | + | return cfg | |||
| 372 | + | } | |||
| 373 | + | ||||
| 366 | func (bw *browserWindow) applySidebarWidthConfig(a *App) { | 374 | func (bw *browserWindow) applySidebarWidthConfig(a *App) { | ||
| 367 | if bw == nil || bw.mainWindow == nil || a == nil || a.deps == nil || a.deps.Config == nil { | 375 | if bw == nil || bw.mainWindow == nil || a == nil || a.deps == nil || a.deps.Config == nil { | ||
| 368 | return | 376 | return | ||
| 369 | } | 377 | } | ||
| 370 | - | widthCfg := window.SidebarDefaultWidth() | 378 | + | bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.deps.Config.SidebarWidth)) |
| 371 | - | if w := a.deps.Config.SidebarWidth; w > 0 { | |||
| 372 | - | widthCfg.WidthPx = w | |||
| 373 | - | } | |||
| 374 | - | bw.mainWindow.SetSidebarWidth(widthCfg) | |||
| 375 | } | 379 | } | ||
| 376 | 380 | ||||
| 377 | // hideHistorySidebar hides the sidebar. Callers should also restore focus | 381 | // hideHistorySidebar hides the sidebar. Callers should also restore focus | ||
M internal/ui/browser_window_test.go +5 -37
| @@ -1774,50 +1774,18 @@ func TestApp_HideAndRestoreFocusForBrowserWindow_NilBWIsSafe(t *testing.T) { | |||
| 1774 | 1774 | } | |
| 1775 | 1775 | ||
| 1776 | 1776 | // ============================================================================= | |
| 1777 | - | // applySidebarWidthConfig tests | |
| 1777 | + | // Sidebar width config tests | |
| 1778 | 1778 | // ============================================================================= | |
| 1779 | 1779 | ||
| 1780 | - | // TestBrowserWindow_ApplySidebarWidthConfig_ConfigValue verifies that | |
| 1781 | - | // applySidebarWidthConfig reads the SidebarWidth from deps.Config and | |
| 1782 | - | // passes it to SetSidebarWidth as the width config, overriding the default. | |
| 1783 | - | func TestBrowserWindow_ApplySidebarWidthConfig_ConfigValue(t *testing.T) { | |
| 1784 | - | mw := &window.MainWindow{} | |
| 1785 | - | bw := &browserWindow{mainWindow: mw} | |
| 1786 | - | ||
| 1787 | - | app := &App{ | |
| 1788 | - | deps: &Dependencies{ | |
| 1789 | - | Config: &config.Config{ | |
| 1790 | - | SidebarWidth: 350, | |
| 1791 | - | }, | |
| 1792 | - | }, | |
| 1793 | - | } | |
| 1794 | - | ||
| 1795 | - | bw.applySidebarWidthConfig(app) | |
| 1796 | - | ||
| 1797 | - | // SetSidebarWidth should have been called with WidthPx = 350. | |
| 1798 | - | cfg := mw.LastSidebarWidthCfg() | |
| 1780 | + | func TestHistorySidebarWidthConfig_ConfigValue(t *testing.T) { | |
| 1781 | + | cfg := historySidebarWidthConfig(350) | |
| 1799 | 1782 | assert.Equal(t, 350, cfg.WidthPx, "should apply config-backed width of 350px") | |
| 1800 | 1783 | assert.Equal(t, 280, cfg.MinPx, "should keep default min clamp") | |
| 1801 | 1784 | assert.Equal(t, 380, cfg.MaxPx, "should keep default max clamp") | |
| 1802 | 1785 | } | |
| 1803 | 1786 | ||
| 1804 | - | // TestBrowserWindow_ApplySidebarWidthConfig_DefaultValue verifies that when | |
| 1805 | - | // SidebarWidth is 0 (unset), applySidebarWidthConfig passes the default 320px. | |
| 1806 | - | func TestBrowserWindow_ApplySidebarWidthConfig_DefaultValue(t *testing.T) { | |
| 1807 | - | mw := &window.MainWindow{} | |
| 1808 | - | bw := &browserWindow{mainWindow: mw} | |
| 1809 | - | ||
| 1810 | - | app := &App{ | |
| 1811 | - | deps: &Dependencies{ | |
| 1812 | - | Config: &config.Config{ | |
| 1813 | - | SidebarWidth: 0, | |
| 1814 | - | }, | |
| 1815 | - | }, | |
| 1816 | - | } | |
| 1817 | - | ||
| 1818 | - | bw.applySidebarWidthConfig(app) | |
| 1819 | - | ||
| 1820 | - | cfg := mw.LastSidebarWidthCfg() | |
| 1787 | + | func TestHistorySidebarWidthConfig_DefaultValue(t *testing.T) { | |
| 1788 | + | cfg := historySidebarWidthConfig(0) | |
| 1821 | 1789 | assert.Equal(t, 320, cfg.WidthPx, "should use default width of 320px when config is 0") | |
| 1822 | 1790 | assert.Equal(t, 280, cfg.MinPx, "should keep default min clamp") | |
| 1823 | 1791 | assert.Equal(t, 380, cfg.MaxPx, "should keep default max clamp") | |
| @@ -1774,50 +1774,18 @@ func TestApp_HideAndRestoreFocusForBrowserWindow_NilBWIsSafe(t *testing.T) { | |||||
| 1774 | } | 1774 | } | ||
| 1775 | 1775 | ||||
| 1776 | // ============================================================================= | 1776 | // ============================================================================= | ||
| 1777 | - | // applySidebarWidthConfig tests | 1777 | + | // Sidebar width config tests |
| 1778 | // ============================================================================= | 1778 | // ============================================================================= | ||
| 1779 | 1779 | ||||
| 1780 | - | // TestBrowserWindow_ApplySidebarWidthConfig_ConfigValue verifies that | 1780 | + | func TestHistorySidebarWidthConfig_ConfigValue(t *testing.T) { |
| 1781 | - | // applySidebarWidthConfig reads the SidebarWidth from deps.Config and | 1781 | + | cfg := historySidebarWidthConfig(350) |
| 1782 | - | // passes it to SetSidebarWidth as the width config, overriding the default. | |||
| 1783 | - | func TestBrowserWindow_ApplySidebarWidthConfig_ConfigValue(t *testing.T) { | |||
| 1784 | - | mw := &window.MainWindow{} | |||
| 1785 | - | bw := &browserWindow{mainWindow: mw} | |||
| 1786 | - | ||||
| 1787 | - | app := &App{ | |||
| 1788 | - | deps: &Dependencies{ | |||
| 1789 | - | Config: &config.Config{ | |||
| 1790 | - | SidebarWidth: 350, | |||
| 1791 | - | }, | |||
| 1792 | - | }, | |||
| 1793 | - | } | |||
| 1794 | - | ||||
| 1795 | - | bw.applySidebarWidthConfig(app) | |||
| 1796 | - | ||||
| 1797 | - | // SetSidebarWidth should have been called with WidthPx = 350. | |||
| 1798 | - | cfg := mw.LastSidebarWidthCfg() | |||
| 1799 | assert.Equal(t, 350, cfg.WidthPx, "should apply config-backed width of 350px") | 1782 | assert.Equal(t, 350, cfg.WidthPx, "should apply config-backed width of 350px") | ||
| 1800 | assert.Equal(t, 280, cfg.MinPx, "should keep default min clamp") | 1783 | assert.Equal(t, 280, cfg.MinPx, "should keep default min clamp") | ||
| 1801 | assert.Equal(t, 380, cfg.MaxPx, "should keep default max clamp") | 1784 | assert.Equal(t, 380, cfg.MaxPx, "should keep default max clamp") | ||
| 1802 | } | 1785 | } | ||
| 1803 | 1786 | ||||
| 1804 | - | // TestBrowserWindow_ApplySidebarWidthConfig_DefaultValue verifies that when | 1787 | + | func TestHistorySidebarWidthConfig_DefaultValue(t *testing.T) { |
| 1805 | - | // SidebarWidth is 0 (unset), applySidebarWidthConfig passes the default 320px. | 1788 | + | cfg := historySidebarWidthConfig(0) |
| 1806 | - | func TestBrowserWindow_ApplySidebarWidthConfig_DefaultValue(t *testing.T) { | |||
| 1807 | - | mw := &window.MainWindow{} | |||
| 1808 | - | bw := &browserWindow{mainWindow: mw} | |||
| 1809 | - | ||||
| 1810 | - | app := &App{ | |||
| 1811 | - | deps: &Dependencies{ | |||
| 1812 | - | Config: &config.Config{ | |||
| 1813 | - | SidebarWidth: 0, | |||
| 1814 | - | }, | |||
| 1815 | - | }, | |||
| 1816 | - | } | |||
| 1817 | - | ||||
| 1818 | - | bw.applySidebarWidthConfig(app) | |||
| 1819 | - | ||||
| 1820 | - | cfg := mw.LastSidebarWidthCfg() | |||
| 1821 | assert.Equal(t, 320, cfg.WidthPx, "should use default width of 320px when config is 0") | 1789 | assert.Equal(t, 320, cfg.WidthPx, "should use default width of 320px when config is 0") | ||
| 1822 | assert.Equal(t, 280, cfg.MinPx, "should keep default min clamp") | 1790 | assert.Equal(t, 280, cfg.MinPx, "should keep default min clamp") | ||
| 1823 | assert.Equal(t, 380, cfg.MaxPx, "should keep default max clamp") | 1791 | assert.Equal(t, 380, cfg.MaxPx, "should keep default max clamp") | ||
M internal/ui/component/history_sidebar.go +5 -4
| @@ -10,7 +10,8 @@ import ( | |||
| 10 | 10 | "github.com/bnema/puregotk/v4/gtk" | |
| 11 | 11 | "github.com/bnema/puregotk/v4/pango" | |
| 12 | 12 | ||
| 13 | - | "github.com/bnema/dumber/internal/application/usecase" | |
| 13 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 14 | + | "github.com/bnema/dumber/internal/application/port" | |
| 14 | 15 | "github.com/bnema/dumber/internal/domain/entity" | |
| 15 | 16 | "github.com/bnema/dumber/internal/logging" | |
| 16 | 17 | "github.com/bnema/dumber/internal/ui/layout" | |
| @@ -53,7 +54,7 @@ type HistorySidebar struct { | |||
| 53 | 54 | listBox *gtk.ListBox | |
| 54 | 55 | ||
| 55 | 56 | // Dependencies | |
| 56 | - | historyUC *usecase.SearchHistoryUseCase | |
| 57 | + | historyUC port.HomepageHistory | |
| 57 | 58 | onURL func(ctx context.Context, url string) | |
| 58 | 59 | onOpenInNewPane func(ctx context.Context, url string) error | |
| 59 | 60 | onNavigateKeepOpen func(ctx context.Context, url string) | |
| @@ -102,7 +103,7 @@ type HistorySidebar struct { | |||
| 102 | 103 | // HistorySidebarConfig holds configuration for creating a HistorySidebar. | |
| 103 | 104 | type HistorySidebarConfig struct { | |
| 104 | 105 | // HistoryUC provides history query and delete operations. | |
| 105 | - | HistoryUC *usecase.SearchHistoryUseCase | |
| 106 | + | HistoryUC port.HomepageHistory | |
| 106 | 107 | ||
| 107 | 108 | // OnNavigate is called when the user activates a history entry. | |
| 108 | 109 | // The default Enter / click behavior keeps the sidebar open after navigating. | |
| @@ -765,7 +766,7 @@ func (hs *HistorySidebar) doFTSearch(query string, gen uint64) { | |||
| 765 | 766 | } | |
| 766 | 767 | ||
| 767 | 768 | go func() { | |
| 768 | - | out, err := uc.Search(hs.ctx, usecase.SearchInput{Query: query, Limit: sidebarSearchLimit}) | |
| 769 | + | out, err := uc.Search(hs.ctx, dto.HistorySearchInput{Query: query, Limit: sidebarSearchLimit}) | |
| 769 | 770 | var entries []*entity.HistoryEntry | |
| 770 | 771 | if out != nil { | |
| 771 | 772 | entries = make([]*entity.HistoryEntry, len(out.Matches)) | |
| @@ -10,7 +10,8 @@ import ( | |||||
| 10 | "github.com/bnema/puregotk/v4/gtk" | 10 | "github.com/bnema/puregotk/v4/gtk" | ||
| 11 | "github.com/bnema/puregotk/v4/pango" | 11 | "github.com/bnema/puregotk/v4/pango" | ||
| 12 | 12 | ||||
| 13 | - | "github.com/bnema/dumber/internal/application/usecase" | 13 | + | "github.com/bnema/dumber/internal/application/dto" |
| 14 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 14 | "github.com/bnema/dumber/internal/domain/entity" | 15 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 15 | "github.com/bnema/dumber/internal/logging" | 16 | "github.com/bnema/dumber/internal/logging" | ||
| 16 | "github.com/bnema/dumber/internal/ui/layout" | 17 | "github.com/bnema/dumber/internal/ui/layout" | ||
| @@ -53,7 +54,7 @@ type HistorySidebar struct { | |||||
| 53 | listBox *gtk.ListBox | 54 | listBox *gtk.ListBox | ||
| 54 | 55 | ||||
| 55 | // Dependencies | 56 | // Dependencies | ||
| 56 | - | historyUC *usecase.SearchHistoryUseCase | 57 | + | historyUC port.HomepageHistory |
| 57 | onURL func(ctx context.Context, url string) | 58 | onURL func(ctx context.Context, url string) | ||
| 58 | onOpenInNewPane func(ctx context.Context, url string) error | 59 | onOpenInNewPane func(ctx context.Context, url string) error | ||
| 59 | onNavigateKeepOpen func(ctx context.Context, url string) | 60 | onNavigateKeepOpen func(ctx context.Context, url string) | ||
| @@ -102,7 +103,7 @@ type HistorySidebar struct { | |||||
| 102 | // HistorySidebarConfig holds configuration for creating a HistorySidebar. | 103 | // HistorySidebarConfig holds configuration for creating a HistorySidebar. | ||
| 103 | type HistorySidebarConfig struct { | 104 | type HistorySidebarConfig struct { | ||
| 104 | // HistoryUC provides history query and delete operations. | 105 | // HistoryUC provides history query and delete operations. | ||
| 105 | - | HistoryUC *usecase.SearchHistoryUseCase | 106 | + | HistoryUC port.HomepageHistory |
| 106 | 107 | ||||
| 107 | // OnNavigate is called when the user activates a history entry. | 108 | // OnNavigate is called when the user activates a history entry. | ||
| 108 | // The default Enter / click behavior keeps the sidebar open after navigating. | 109 | // The default Enter / click behavior keeps the sidebar open after navigating. | ||
| @@ -765,7 +766,7 @@ func (hs *HistorySidebar) doFTSearch(query string, gen uint64) { | |||||
| 765 | } | 766 | } | ||
| 766 | 767 | ||||
| 767 | go func() { | 768 | go func() { | ||
| 768 | - | out, err := uc.Search(hs.ctx, usecase.SearchInput{Query: query, Limit: sidebarSearchLimit}) | 769 | + | out, err := uc.Search(hs.ctx, dto.HistorySearchInput{Query: query, Limit: sidebarSearchLimit}) |
| 769 | var entries []*entity.HistoryEntry | 770 | var entries []*entity.HistoryEntry | ||
| 770 | if out != nil { | 771 | if out != nil { | ||
| 771 | entries = make([]*entity.HistoryEntry, len(out.Matches)) | 772 | entries = make([]*entity.HistoryEntry, len(out.Matches)) | ||
M internal/ui/component/history_sidebar_search_test.go +43 -57
| @@ -7,11 +7,12 @@ import ( | |||
| 7 | 7 | "testing" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | - | "github.com/bnema/dumber/internal/application/usecase" | |
| 10 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 11 | + | appportmocks "github.com/bnema/dumber/internal/application/port/mocks" | |
| 11 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | - | "github.com/bnema/dumber/internal/domain/repository" | |
| 13 | 13 | "github.com/rs/zerolog" | |
| 14 | 14 | "github.com/stretchr/testify/assert" | |
| 15 | + | "github.com/stretchr/testify/mock" | |
| 15 | 16 | "github.com/stretchr/testify/require" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| @@ -119,51 +120,27 @@ func TestApplySearchResults_EmptyResultsApplied(t *testing.T) { | |||
| 119 | 120 | } | |
| 120 | 121 | ||
| 121 | 122 | // ============================================================================= | |
| 122 | - | // doFTSearch seam: controllable HistoryUC with fake repo | |
| 123 | + | // doFTSearch seam: controllable history port mock | |
| 123 | 124 | // ============================================================================= | |
| 124 | 125 | ||
| 125 | - | // fakeHistoryRepo implements repository.HistoryRepository minimally for | |
| 126 | - | // sidebar search tests. Only Search and GetRecent are required; unused | |
| 127 | - | // methods panic. | |
| 128 | - | type fakeHistoryRepo struct { | |
| 129 | - | repository.HistoryRepository | |
| 130 | - | searchFn func(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) | |
| 131 | - | getRecentFn func(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | |
| 132 | - | } | |
| 133 | - | ||
| 134 | - | func (f *fakeHistoryRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | |
| 135 | - | if f.searchFn != nil { | |
| 136 | - | return f.searchFn(ctx, query, limit) | |
| 137 | - | } | |
| 138 | - | return []entity.HistoryMatch{}, nil | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | func (f *fakeHistoryRepo) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) { | |
| 142 | - | if f.getRecentFn != nil { | |
| 143 | - | return f.getRecentFn(ctx, limit, offset) | |
| 144 | - | } | |
| 145 | - | // Permanently block if no handler set — safe for tests that want to | |
| 146 | - | // hold a fetch in flight while the test controls timing. | |
| 147 | - | <-make(chan struct{}) | |
| 148 | - | return nil, nil | |
| 126 | + | func newMockHomepageHistory(t *testing.T) *appportmocks.MockHomepageHistory { | |
| 127 | + | return appportmocks.NewMockHomepageHistory(t) | |
| 149 | 128 | } | |
| 150 | 129 | ||
| 151 | 130 | func TestDoFTSearch_WithFakeUC_StaleGenerationDropsResults(t *testing.T) { | |
| 152 | 131 | searchCalled := make(chan struct{}, 1) | |
| 153 | 132 | ||
| 154 | - | repo := &fakeHistoryRepo{ | |
| 155 | - | searchFn: func(_ context.Context, query string, _ int) ([]entity.HistoryMatch, error) { | |
| 156 | - | searchCalled <- struct{}{} | |
| 157 | - | return []entity.HistoryMatch{ | |
| 158 | - | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}}, | |
| 159 | - | }, nil | |
| 160 | - | }, | |
| 161 | - | } | |
| 162 | - | fakeUC := usecase.NewSearchHistoryUseCase(repo) | |
| 133 | + | history := newMockHomepageHistory(t) | |
| 134 | + | history.EXPECT().Search(mock.Anything, dto.HistorySearchInput{Query: "test", Limit: sidebarSearchLimit}).RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) { | |
| 135 | + | searchCalled <- struct{}{} | |
| 136 | + | return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{ | |
| 137 | + | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}}, | |
| 138 | + | }}, nil | |
| 139 | + | }) | |
| 163 | 140 | ||
| 164 | 141 | hs := newTestSidebarSearchHarness() | |
| 165 | 142 | hs.ctx = context.Background() | |
| 166 | - | hs.historyUC = fakeUC | |
| 143 | + | hs.historyUC = history | |
| 167 | 144 | hs.searchGen = 1 | |
| 168 | 145 | ||
| 169 | 146 | // Start search with gen=1 | |
| @@ -192,19 +169,17 @@ func TestDoFTSearch_WithFakeUC_StaleGenerationDropsResults(t *testing.T) { | |||
| 192 | 169 | func TestDoFTSearch_WithFakeUC_CurrentGenApplied(t *testing.T) { | |
| 193 | 170 | searchCalled := make(chan struct{}, 1) | |
| 194 | 171 | ||
| 195 | - | repo := &fakeHistoryRepo{ | |
| 196 | - | searchFn: func(_ context.Context, query string, _ int) ([]entity.HistoryMatch, error) { | |
| 197 | - | searchCalled <- struct{}{} | |
| 198 | - | return []entity.HistoryMatch{ | |
| 199 | - | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}}, | |
| 200 | - | }, nil | |
| 201 | - | }, | |
| 202 | - | } | |
| 203 | - | fakeUC := usecase.NewSearchHistoryUseCase(repo) | |
| 172 | + | history := newMockHomepageHistory(t) | |
| 173 | + | history.EXPECT().Search(mock.Anything, dto.HistorySearchInput{Query: "live", Limit: sidebarSearchLimit}).RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) { | |
| 174 | + | searchCalled <- struct{}{} | |
| 175 | + | return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{ | |
| 176 | + | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}}, | |
| 177 | + | }}, nil | |
| 178 | + | }) | |
| 204 | 179 | ||
| 205 | 180 | hs := newTestSidebarSearchHarness() | |
| 206 | 181 | hs.ctx = context.Background() | |
| 207 | - | hs.historyUC = fakeUC | |
| 182 | + | hs.historyUC = history | |
| 208 | 183 | hs.searchGen = 1 | |
| 209 | 184 | ||
| 210 | 185 | // Start the search and wait for the use case to be invoked. | |
| @@ -236,9 +211,16 @@ func TestDoFTSearch_WithFakeUC_CurrentGenApplied(t *testing.T) { | |||
| 236 | 211 | // TestHistorySidebar_ReloadPreservesQuery verifies that Reload preserves the | |
| 237 | 212 | // active query while resetting browse/search state before the refreshed load. | |
| 238 | 213 | func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) { | |
| 214 | + | searchCalled := make(chan struct{}, 1) | |
| 215 | + | history := newMockHomepageHistory(t) | |
| 216 | + | history.EXPECT().Search(mock.Anything, dto.HistorySearchInput{Query: "preserved", Limit: sidebarSearchLimit}).RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) { | |
| 217 | + | searchCalled <- struct{}{} | |
| 218 | + | return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil | |
| 219 | + | }) | |
| 220 | + | ||
| 239 | 221 | hs := newTestSidebarSearchHarness() | |
| 240 | 222 | hs.currentQuery = "preserved" | |
| 241 | - | hs.historyUC = usecase.NewSearchHistoryUseCase(&fakeHistoryRepo{}) | |
| 223 | + | hs.historyUC = history | |
| 242 | 224 | hs.ctx = context.Background() | |
| 243 | 225 | ||
| 244 | 226 | hs.mu.Lock() | |
| @@ -254,6 +236,12 @@ func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) { | |||
| 254 | 236 | ||
| 255 | 237 | hs.Reload() | |
| 256 | 238 | ||
| 239 | + | select { | |
| 240 | + | case <-searchCalled: | |
| 241 | + | case <-time.After(time.Second): | |
| 242 | + | t.Fatal("timed out waiting for reload search to be invoked") | |
| 243 | + | } | |
| 244 | + | ||
| 257 | 245 | hs.mu.RLock() | |
| 258 | 246 | assert.Equal(t, "preserved", hs.currentQuery, "query must be preserved after Reload") | |
| 259 | 247 | assert.False(t, hs.loadDone, "loadDone must be reset") | |
| @@ -278,18 +266,16 @@ func TestFetchPage_StaleGenerationDoesNotMutateLoadingState(t *testing.T) { | |||
| 278 | 266 | getRecentCalled := make(chan struct{}) | |
| 279 | 267 | proceed := make(chan struct{}) | |
| 280 | 268 | ||
| 281 | - | repo := &fakeHistoryRepo{ | |
| 282 | - | getRecentFn: func(_ context.Context, _, _ int) ([]*entity.HistoryEntry, error) { | |
| 283 | - | close(getRecentCalled) | |
| 284 | - | <-proceed | |
| 285 | - | return []*entity.HistoryEntry{}, nil | |
| 286 | - | }, | |
| 287 | - | } | |
| 288 | - | fakeUC := usecase.NewSearchHistoryUseCase(repo) | |
| 269 | + | history := newMockHomepageHistory(t) | |
| 270 | + | history.EXPECT().GetRecent(mock.Anything, sidebarPageSize, 0).RunAndReturn(func(context.Context, int, int) ([]*entity.HistoryEntry, error) { | |
| 271 | + | close(getRecentCalled) | |
| 272 | + | <-proceed | |
| 273 | + | return []*entity.HistoryEntry{}, nil | |
| 274 | + | }) | |
| 289 | 275 | ||
| 290 | 276 | hs := newTestSidebarSearchHarness() | |
| 291 | 277 | hs.ctx = context.Background() | |
| 292 | - | hs.historyUC = fakeUC | |
| 278 | + | hs.historyUC = history | |
| 293 | 279 | ||
| 294 | 280 | // Simulate: gen 1 started, then gen 2 started (e.g. by Reload/Show). | |
| 295 | 281 | // The second call incremented loadGen and set isLoading/loadStarted. | |
| @@ -7,11 +7,12 @@ import ( | |||||
| 7 | "testing" | 7 | "testing" | ||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | - | "github.com/bnema/dumber/internal/application/usecase" | 10 | + | "github.com/bnema/dumber/internal/application/dto" |
| 11 | + | appportmocks "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 11 | "github.com/bnema/dumber/internal/domain/entity" | 12 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 12 | - | "github.com/bnema/dumber/internal/domain/repository" | |||
| 13 | "github.com/rs/zerolog" | 13 | "github.com/rs/zerolog" | ||
| 14 | "github.com/stretchr/testify/assert" | 14 | "github.com/stretchr/testify/assert" | ||
| 15 | + | "github.com/stretchr/testify/mock" | |||
| 15 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| 16 | ) | 17 | ) | ||
| 17 | 18 | ||||
| @@ -119,51 +120,27 @@ func TestApplySearchResults_EmptyResultsApplied(t *testing.T) { | |||||
| 119 | } | 120 | } | ||
| 120 | 121 | ||||
| 121 | // ============================================================================= | 122 | // ============================================================================= | ||
| 122 | - | // doFTSearch seam: controllable HistoryUC with fake repo | 123 | + | // doFTSearch seam: controllable history port mock |
| 123 | // ============================================================================= | 124 | // ============================================================================= | ||
| 124 | 125 | ||||
| 125 | - | // fakeHistoryRepo implements repository.HistoryRepository minimally for | 126 | + | func newMockHomepageHistory(t *testing.T) *appportmocks.MockHomepageHistory { |
| 126 | - | // sidebar search tests. Only Search and GetRecent are required; unused | 127 | + | return appportmocks.NewMockHomepageHistory(t) |
| 127 | - | // methods panic. | |||
| 128 | - | type fakeHistoryRepo struct { | |||
| 129 | - | repository.HistoryRepository | |||
| 130 | - | searchFn func(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) | |||
| 131 | - | getRecentFn func(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | |||
| 132 | - | } | |||
| 133 | - | ||||
| 134 | - | func (f *fakeHistoryRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | |||
| 135 | - | if f.searchFn != nil { | |||
| 136 | - | return f.searchFn(ctx, query, limit) | |||
| 137 | - | } | |||
| 138 | - | return []entity.HistoryMatch{}, nil | |||
| 139 | - | } | |||
| 140 | - | ||||
| 141 | - | func (f *fakeHistoryRepo) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) { | |||
| 142 | - | if f.getRecentFn != nil { | |||
| 143 | - | return f.getRecentFn(ctx, limit, offset) | |||
| 144 | - | } | |||
| 145 | - | // Permanently block if no handler set — safe for tests that want to | |||
| 146 | - | // hold a fetch in flight while the test controls timing. | |||
| 147 | - | <-make(chan struct{}) | |||
| 148 | - | return nil, nil | |||
| 149 | } | 128 | } | ||
| 150 | 129 | ||||
| 151 | func TestDoFTSearch_WithFakeUC_StaleGenerationDropsResults(t *testing.T) { | 130 | func TestDoFTSearch_WithFakeUC_StaleGenerationDropsResults(t *testing.T) { | ||
| 152 | searchCalled := make(chan struct{}, 1) | 131 | searchCalled := make(chan struct{}, 1) | ||
| 153 | 132 | ||||
| 154 | - | repo := &fakeHistoryRepo{ | 133 | + | history := newMockHomepageHistory(t) |
| 155 | - | searchFn: func(_ context.Context, query string, _ int) ([]entity.HistoryMatch, error) { | 134 | + | history.EXPECT().Search(mock.Anything, dto.HistorySearchInput{Query: "test", Limit: sidebarSearchLimit}).RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) { |
| 156 | - | searchCalled <- struct{}{} | 135 | + | searchCalled <- struct{}{} |
| 157 | - | return []entity.HistoryMatch{ | 136 | + | return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{ |
| 158 | - | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}}, | 137 | + | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://result.com", Title: "Result", LastVisited: time.Now()}}, |
| 159 | - | }, nil | 138 | + | }}, nil |
| 160 | - | }, | 139 | + | }) |
| 161 | - | } | |||
| 162 | - | fakeUC := usecase.NewSearchHistoryUseCase(repo) | |||
| 163 | 140 | ||||
| 164 | hs := newTestSidebarSearchHarness() | 141 | hs := newTestSidebarSearchHarness() | ||
| 165 | hs.ctx = context.Background() | 142 | hs.ctx = context.Background() | ||
| 166 | - | hs.historyUC = fakeUC | 143 | + | hs.historyUC = history |
| 167 | hs.searchGen = 1 | 144 | hs.searchGen = 1 | ||
| 168 | 145 | ||||
| 169 | // Start search with gen=1 | 146 | // Start search with gen=1 | ||
| @@ -192,19 +169,17 @@ func TestDoFTSearch_WithFakeUC_StaleGenerationDropsResults(t *testing.T) { | |||||
| 192 | func TestDoFTSearch_WithFakeUC_CurrentGenApplied(t *testing.T) { | 169 | func TestDoFTSearch_WithFakeUC_CurrentGenApplied(t *testing.T) { | ||
| 193 | searchCalled := make(chan struct{}, 1) | 170 | searchCalled := make(chan struct{}, 1) | ||
| 194 | 171 | ||||
| 195 | - | repo := &fakeHistoryRepo{ | 172 | + | history := newMockHomepageHistory(t) |
| 196 | - | searchFn: func(_ context.Context, query string, _ int) ([]entity.HistoryMatch, error) { | 173 | + | history.EXPECT().Search(mock.Anything, dto.HistorySearchInput{Query: "live", Limit: sidebarSearchLimit}).RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) { |
| 197 | - | searchCalled <- struct{}{} | 174 | + | searchCalled <- struct{}{} |
| 198 | - | return []entity.HistoryMatch{ | 175 | + | return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{ |
| 199 | - | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}}, | 176 | + | {Entry: &entity.HistoryEntry{ID: 1, URL: "https://live.com", Title: "Live", LastVisited: time.Now()}}, |
| 200 | - | }, nil | 177 | + | }}, nil |
| 201 | - | }, | 178 | + | }) |
| 202 | - | } | |||
| 203 | - | fakeUC := usecase.NewSearchHistoryUseCase(repo) | |||
| 204 | 179 | ||||
| 205 | hs := newTestSidebarSearchHarness() | 180 | hs := newTestSidebarSearchHarness() | ||
| 206 | hs.ctx = context.Background() | 181 | hs.ctx = context.Background() | ||
| 207 | - | hs.historyUC = fakeUC | 182 | + | hs.historyUC = history |
| 208 | hs.searchGen = 1 | 183 | hs.searchGen = 1 | ||
| 209 | 184 | ||||
| 210 | // Start the search and wait for the use case to be invoked. | 185 | // Start the search and wait for the use case to be invoked. | ||
| @@ -236,9 +211,16 @@ func TestDoFTSearch_WithFakeUC_CurrentGenApplied(t *testing.T) { | |||||
| 236 | // TestHistorySidebar_ReloadPreservesQuery verifies that Reload preserves the | 211 | // TestHistorySidebar_ReloadPreservesQuery verifies that Reload preserves the | ||
| 237 | // active query while resetting browse/search state before the refreshed load. | 212 | // active query while resetting browse/search state before the refreshed load. | ||
| 238 | func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) { | 213 | func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) { | ||
| 214 | + | searchCalled := make(chan struct{}, 1) | |||
| 215 | + | history := newMockHomepageHistory(t) | |||
| 216 | + | history.EXPECT().Search(mock.Anything, dto.HistorySearchInput{Query: "preserved", Limit: sidebarSearchLimit}).RunAndReturn(func(context.Context, dto.HistorySearchInput) (*dto.HistorySearchOutput, error) { | |||
| 217 | + | searchCalled <- struct{}{} | |||
| 218 | + | return &dto.HistorySearchOutput{Matches: []entity.HistoryMatch{}}, nil | |||
| 219 | + | }) | |||
| 220 | + | ||||
| 239 | hs := newTestSidebarSearchHarness() | 221 | hs := newTestSidebarSearchHarness() | ||
| 240 | hs.currentQuery = "preserved" | 222 | hs.currentQuery = "preserved" | ||
| 241 | - | hs.historyUC = usecase.NewSearchHistoryUseCase(&fakeHistoryRepo{}) | 223 | + | hs.historyUC = history |
| 242 | hs.ctx = context.Background() | 224 | hs.ctx = context.Background() | ||
| 243 | 225 | ||||
| 244 | hs.mu.Lock() | 226 | hs.mu.Lock() | ||
| @@ -254,6 +236,12 @@ func TestHistorySidebar_ReloadPreservesQuery(t *testing.T) { | |||||
| 254 | 236 | ||||
| 255 | hs.Reload() | 237 | hs.Reload() | ||
| 256 | 238 | ||||
| 239 | + | select { | |||
| 240 | + | case <-searchCalled: | |||
| 241 | + | case <-time.After(time.Second): | |||
| 242 | + | t.Fatal("timed out waiting for reload search to be invoked") | |||
| 243 | + | } | |||
| 244 | + | ||||
| 257 | hs.mu.RLock() | 245 | hs.mu.RLock() | ||
| 258 | assert.Equal(t, "preserved", hs.currentQuery, "query must be preserved after Reload") | 246 | assert.Equal(t, "preserved", hs.currentQuery, "query must be preserved after Reload") | ||
| 259 | assert.False(t, hs.loadDone, "loadDone must be reset") | 247 | assert.False(t, hs.loadDone, "loadDone must be reset") | ||
| @@ -278,18 +266,16 @@ func TestFetchPage_StaleGenerationDoesNotMutateLoadingState(t *testing.T) { | |||||
| 278 | getRecentCalled := make(chan struct{}) | 266 | getRecentCalled := make(chan struct{}) | ||
| 279 | proceed := make(chan struct{}) | 267 | proceed := make(chan struct{}) | ||
| 280 | 268 | ||||
| 281 | - | repo := &fakeHistoryRepo{ | 269 | + | history := newMockHomepageHistory(t) |
| 282 | - | getRecentFn: func(_ context.Context, _, _ int) ([]*entity.HistoryEntry, error) { | 270 | + | history.EXPECT().GetRecent(mock.Anything, sidebarPageSize, 0).RunAndReturn(func(context.Context, int, int) ([]*entity.HistoryEntry, error) { |
| 283 | - | close(getRecentCalled) | 271 | + | close(getRecentCalled) |
| 284 | - | <-proceed | 272 | + | <-proceed |
| 285 | - | return []*entity.HistoryEntry{}, nil | 273 | + | return []*entity.HistoryEntry{}, nil |
| 286 | - | }, | 274 | + | }) |
| 287 | - | } | |||
| 288 | - | fakeUC := usecase.NewSearchHistoryUseCase(repo) | |||
| 289 | 275 | ||||
| 290 | hs := newTestSidebarSearchHarness() | 276 | hs := newTestSidebarSearchHarness() | ||
| 291 | hs.ctx = context.Background() | 277 | hs.ctx = context.Background() | ||
| 292 | - | hs.historyUC = fakeUC | 278 | + | hs.historyUC = history |
| 293 | 279 | ||||
| 294 | // Simulate: gen 1 started, then gen 2 started (e.g. by Reload/Show). | 280 | // Simulate: gen 1 started, then gen 2 started (e.g. by Reload/Show). | ||
| 295 | // The second call incremented loadGen and set isLoading/loadStarted. | 281 | // The second call incremented loadGen and set isLoading/loadStarted. | ||
M internal/ui/window/main_window.go +2 -10
| @@ -38,9 +38,8 @@ type MainWindow struct { | |||
| 38 | 38 | sidebarBox *gtk.Box // Vertical container for sidebar (hidden by default) | |
| 39 | 39 | currentContent *gtk.Widget // Track current content for removal on tab switch | |
| 40 | 40 | ||
| 41 | - | tabBarPosition string // "top" or "bottom" | |
| 42 | - | lastSidebarWidthCfg SidebarWidthConfig // last config passed to SetSidebarWidth (zero-value = unset; test seam) | |
| 43 | - | logger zerolog.Logger | |
| 41 | + | tabBarPosition string // "top" or "bottom" | |
| 42 | + | logger zerolog.Logger | |
| 44 | 43 | } | |
| 45 | 44 | ||
| 46 | 45 | // New creates a new main browser window. | |
| @@ -228,7 +227,6 @@ func (mw *MainWindow) SidebarBox() *gtk.Box { | |||
| 228 | 227 | // config's [MinPx, MaxPx] bounds. Using the zero-value SidebarWidthConfig{} | |
| 229 | 228 | // sets sensible defaults (320px clamped to [280, 380]). | |
| 230 | 229 | func (mw *MainWindow) SetSidebarWidth(cfg SidebarWidthConfig) { | |
| 231 | - | mw.lastSidebarWidthCfg = cfg // record for testability | |
| 232 | 230 | if mw.sidebarBox == nil { | |
| 233 | 231 | return | |
| 234 | 232 | } | |
| @@ -269,12 +267,6 @@ func (mw *MainWindow) IsSidebarVisible() bool { | |||
| 269 | 267 | return mw.sidebarBox.GetVisible() | |
| 270 | 268 | } | |
| 271 | 269 | ||
| 272 | - | // LastSidebarWidthCfg returns the last SidebarWidthConfig passed to | |
| 273 | - | // SetSidebarWidth. Returns the zero value if never called (test seam). | |
| 274 | - | func (mw *MainWindow) LastSidebarWidthCfg() SidebarWidthConfig { | |
| 275 | - | return mw.lastSidebarWidthCfg | |
| 276 | - | } | |
| 277 | - | ||
| 278 | 270 | // SetSidebarWidget replaces the current sidebar content widget. | |
| 279 | 271 | func (mw *MainWindow) SetSidebarWidget(widget *gtk.Widget) { | |
| 280 | 272 | if mw.sidebarBox == nil { | |
| @@ -38,9 +38,8 @@ type MainWindow struct { | |||||
| 38 | sidebarBox *gtk.Box // Vertical container for sidebar (hidden by default) | 38 | sidebarBox *gtk.Box // Vertical container for sidebar (hidden by default) | ||
| 39 | currentContent *gtk.Widget // Track current content for removal on tab switch | 39 | currentContent *gtk.Widget // Track current content for removal on tab switch | ||
| 40 | 40 | ||||
| 41 | - | tabBarPosition string // "top" or "bottom" | 41 | + | tabBarPosition string // "top" or "bottom" |
| 42 | - | lastSidebarWidthCfg SidebarWidthConfig // last config passed to SetSidebarWidth (zero-value = unset; test seam) | 42 | + | logger zerolog.Logger |
| 43 | - | logger zerolog.Logger | |||
| 44 | } | 43 | } | ||
| 45 | 44 | ||||
| 46 | // New creates a new main browser window. | 45 | // New creates a new main browser window. | ||
| @@ -228,7 +227,6 @@ func (mw *MainWindow) SidebarBox() *gtk.Box { | |||||
| 228 | // config's [MinPx, MaxPx] bounds. Using the zero-value SidebarWidthConfig{} | 227 | // config's [MinPx, MaxPx] bounds. Using the zero-value SidebarWidthConfig{} | ||
| 229 | // sets sensible defaults (320px clamped to [280, 380]). | 228 | // sets sensible defaults (320px clamped to [280, 380]). | ||
| 230 | func (mw *MainWindow) SetSidebarWidth(cfg SidebarWidthConfig) { | 229 | func (mw *MainWindow) SetSidebarWidth(cfg SidebarWidthConfig) { | ||
| 231 | - | mw.lastSidebarWidthCfg = cfg // record for testability | |||
| 232 | if mw.sidebarBox == nil { | 230 | if mw.sidebarBox == nil { | ||
| 233 | return | 231 | return | ||
| 234 | } | 232 | } | ||
| @@ -269,12 +267,6 @@ func (mw *MainWindow) IsSidebarVisible() bool { | |||||
| 269 | return mw.sidebarBox.GetVisible() | 267 | return mw.sidebarBox.GetVisible() | ||
| 270 | } | 268 | } | ||
| 271 | 269 | ||||
| 272 | - | // LastSidebarWidthCfg returns the last SidebarWidthConfig passed to | |||
| 273 | - | // SetSidebarWidth. Returns the zero value if never called (test seam). | |||
| 274 | - | func (mw *MainWindow) LastSidebarWidthCfg() SidebarWidthConfig { | |||
| 275 | - | return mw.lastSidebarWidthCfg | |||
| 276 | - | } | |||
| 277 | - | ||||
| 278 | // SetSidebarWidget replaces the current sidebar content widget. | 270 | // SetSidebarWidget replaces the current sidebar content widget. | ||
| 279 | func (mw *MainWindow) SetSidebarWidget(widget *gtk.Widget) { | 271 | func (mw *MainWindow) SetSidebarWidget(widget *gtk.Widget) { | ||
| 280 | if mw.sidebarBox == nil { | 272 | if mw.sidebarBox == nil { | ||