package systemviews import ( "context" "errors" "sync" "testing" "time" "github.com/bnema/dumber/internal/application/dto" "github.com/bnema/dumber/internal/application/port" "github.com/bnema/dumber/internal/domain/entity" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseRoute(t *testing.T) { t.Parallel() tests := []struct { name string uri string want Route }{ {name: "empty", uri: "", want: RouteUnknown}, {name: "scheme only", uri: "://", want: RouteUnknown}, {name: "dumb scheme only", uri: "dumb://", want: RouteUnknown}, {name: "dumb unknown host", uri: "dumb://unknown", want: RouteUnknown}, {name: "dumb opaque bogus", uri: "dumb:bogus", want: RouteUnknown}, {name: "history host", uri: "dumb://history", want: RouteHistory}, {name: "history opaque", uri: "dumb:history", want: RouteHistory}, {name: "favorites host", uri: "dumb://favorites", want: RouteFavorites}, {name: "favorites opaque", uri: "dumb:favorites", want: RouteFavorites}, {name: "config host", uri: "dumb://config", want: RouteConfig}, {name: "config opaque", uri: "dumb:config", want: RouteConfig}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := ParseRoute(tt.uri); got != tt.want { t.Fatalf("ParseRoute(%q) = %v, want %v", tt.uri, got, tt.want) } }) } } func TestAppRunMountsPlaceholderAndRecordsRoute(t *testing.T) { t.Parallel() dom := &recordingDOM{} app := NewApp(Dependencies{ DOM: dom, LocationURI: "dumb://history", }) require.NoError(t, app.Run()) assert.Equal(t, RouteHistory, app.CurrentRoute()) assert.True(t, dom.Mounted()) assert.Contains(t, dom.HTML(), "history") assert.Contains(t, dom.HTML(), "systemviews") } func TestAppLoadInitialHistoryRouteUsesStyledSections(t *testing.T) { t.Parallel() history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ URL: "https://example.com", Title: "Example", }}} app := NewApp(Dependencies{ History: history, LocationURI: "dumb://history", }) require.NoError(t, app.LoadInitial(context.Background())) assert.Equal(t, RouteHistory, app.CurrentRoute()) assert.Len(t, app.historyEntries, 1) assert.Contains(t, app.renderedHTML, "Example") assert.Contains(t, app.renderedHTML, "https://example.com") assert.True(t, history.called) assert.False(t, history.analyticsCalled) assert.Equal(t, 0, history.limit) assert.Equal(t, 0, history.offset) // Shell frame present, no full document wrapper. assert.NotContains(t, app.renderedHTML, "History - 8904 entries, 40947 visits, 121 days`) assert.NotContains(t, app.renderedHTML, `sv-history-summary`) assert.NotContains(t, app.renderedHTML, `sv-stat-value`) } func TestAppRunMountsHistoryLoadingBeforeAsyncHydration(t *testing.T) { t.Parallel() dom := &recordingActionDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}} history := &recordingHistoryService{ entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Loaded entry"}}, timelineStarted: make(chan struct{}), releaseTimeline: make(chan struct{}), } app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) require.NoError(t, app.RunWithContext(context.Background())) loadingHTML := receiveMount(t, dom.mounts) assert.Contains(t, loadingHTML, "Loading history") assert.NotContains(t, loadingHTML, "Loaded entry") select { case <-history.timelineStarted: case <-time.After(time.Second): t.Fatal("history timeline was not started asynchronously") } close(history.releaseTimeline) hydratedHTML := receiveMount(t, dom.mounts) assert.Contains(t, hydratedHTML, "Loaded entry") assert.Contains(t, hydratedHTML, "Loaded 1 item") } func TestSurfaceActionErrorPreventsStaleAsyncHydrationMount(t *testing.T) { t.Parallel() dom := &recordingDOM{mounts: make(chan string, 4)} app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"}) app.currentRoute = RouteHistory staleHTML := "
Stale hydrated entry
" staleGeneration := app.renderGeneration ctx := context.Background() app.surfaceActionError(ctx, errors.New("action worker failed")) errorHTML := receiveMount(t, dom.mounts) assert.Contains(t, errorHTML, "action worker failed") assert.NotContains(t, errorHTML, "Stale hydrated entry") require.NoError(t, app.mountHTMLIfCurrent(ctx, staleHTML, staleGeneration)) assertNoMount(t, dom.mounts, 100*time.Millisecond) assert.Contains(t, dom.HTML(), "action worker failed") assert.NotContains(t, dom.HTML(), "Stale hydrated entry") } func TestSurfaceActionErrorWhileAsyncHydrationBlockedPreventsStaleMount(t *testing.T) { t.Parallel() dom := &recordingActionDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}} history := &recordingHistoryService{ entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Stale hydrated entry"}}, timelineStarted: make(chan struct{}), releaseTimeline: make(chan struct{}), } app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) ctx := context.Background() require.NoError(t, app.RunWithContext(ctx)) loadingHTML := receiveMount(t, dom.mounts) assert.Contains(t, loadingHTML, "Loading history") select { case <-history.timelineStarted: case <-time.After(time.Second): t.Fatal("history timeline was not started asynchronously") } app.surfaceActionError(ctx, errors.New("action worker failed")) errorHTML := receiveMount(t, dom.mounts) assert.Contains(t, errorHTML, "action worker failed") assert.NotContains(t, errorHTML, "Stale hydrated entry") close(history.releaseTimeline) assertNoMount(t, dom.mounts, 100*time.Millisecond) assert.Contains(t, dom.HTML(), "action worker failed") assert.NotContains(t, dom.HTML(), "Stale hydrated entry") } func TestCurrentHistoryRouteSnapshotIncludesWindowAfter(t *testing.T) { cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC) app := NewApp(Dependencies{History: &recordingHistoryService{}, LocationURI: "dumb://history"}) app.currentRoute = RouteHistory app.historyWindowAfter = cursor snapshot := app.currentHistoryRouteSnapshotLocked() assert.Equal(t, cursor, snapshot.windowAfter) } func TestAppHandleHistoryLoadMoreAppendsOlderWindow(t *testing.T) { t.Parallel() cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC) history := &recordingHistoryService{window: &entity.HistoryWindow{ Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}}, Before: cursor, CursorLastVisited: cursor.Add(-time.Hour), CursorID: 2, HasMore: true, }} dom := &recordingActionDOM{} app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) app.currentRoute = RouteHistory app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}} app.historyWindowAfter = cursor app.historyCursorID = 1 app.historyHasMore = true require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionLoadMore, Data: map[string]string{"before": cursor.Format(time.RFC3339Nano)}, })) assert.Equal(t, cursor, history.windowBefore) assert.Equal(t, int64(1), history.windowBeforeID) assert.Equal(t, int64(2), app.historyCursorID) assert.Equal(t, cursor.Add(-time.Hour), app.historyWindowAfter) assert.Len(t, app.historyEntries, 2) assert.Contains(t, dom.AppendedHTML(), "Older entry") assert.Contains(t, dom.AppendedHTML(), `data-sv-action="history.loadMore"`) assert.Empty(t, dom.HTML(), "load-more should append a fragment instead of remounting the whole page") } func TestAppHandleHistoryLoadMoreRemountsWhenAppendUnavailable(t *testing.T) { t.Parallel() cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC) history := &recordingHistoryService{window: &entity.HistoryWindow{ Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}}, Before: cursor, CursorLastVisited: cursor.Add(-time.Hour), CursorID: 2, HasMore: true, }} dom := &recordingDOM{} app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) app.currentRoute = RouteHistory app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}} app.historyWindowAfter = cursor app.historyCursorID = 1 app.historyHasMore = true require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionLoadMore, Data: map[string]string{"before": cursor.Format(time.RFC3339Nano)}, })) assert.Equal(t, cursor, history.windowBefore) assert.Equal(t, int64(1), history.windowBeforeID) assert.Len(t, app.historyEntries, 2) assert.Contains(t, dom.HTML(), "Older entry") assert.Contains(t, dom.HTML(), `data-sv-action="history.loadMore"`) } func TestAppHandleHistoryLoadMoreDisablesPartialNextCursor(t *testing.T) { t.Parallel() cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC) history := &recordingHistoryService{window: &entity.HistoryWindow{ Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example", Title: "Older entry", LastVisited: cursor.Add(-time.Hour)}}, Before: cursor, After: cursor.Add(-24 * time.Hour), HasMore: true, }} dom := &recordingActionDOM{} app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) app.currentRoute = RouteHistory app.historyEntries = []*entity.HistoryEntry{{ID: 1, URL: "https://newer.example", Title: "Newer entry", LastVisited: cursor}} app.historyWindowAfter = cursor app.historyCursorID = 1 app.historyHasMore = true require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionLoadMore, Data: map[string]string{"before": cursor.Format(time.RFC3339Nano)}, })) assert.False(t, app.historyHasMore) assert.Zero(t, app.historyCursorID) assert.True(t, app.historyWindowAfter.IsZero()) assert.NotContains(t, dom.AppendedHTML(), `data-sv-action="history.loadMore"`) } func TestAppHandleHistoryLoadMoreIgnoresStaleCursor(t *testing.T) { t.Parallel() cursor := time.Date(2026, 4, 25, 9, 0, 0, 0, time.UTC) history := &recordingHistoryService{window: &entity.HistoryWindow{Entries: []*entity.HistoryEntry{{ID: 2, URL: "https://older.example"}}}} dom := &recordingActionDOM{} app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) app.currentRoute = RouteHistory app.historyWindowAfter = cursor app.historyHasMore = true require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionLoadMore, Data: map[string]string{"before": cursor.Add(-24 * time.Hour).Format(time.RFC3339Nano)}, })) assert.False(t, history.called) assert.Empty(t, dom.AppendedHTML()) } func TestAppLoadInitialHistoryRouteRendersManagementActions(t *testing.T) { t.Parallel() history := &recordingHistoryService{ entries: []*entity.HistoryEntry{{ ID: 42, URL: "https://example.com/page", Title: "Example", }}, domainStats: []*entity.DomainStat{{Domain: "www.example.com:8080", PageCount: 1, TotalVisits: 3}}, } app := NewApp(Dependencies{ History: history, LocationURI: "dumb://history", }) require.NoError(t, app.LoadInitial(context.Background())) assert.Contains(t, app.renderedHTML, `data-sv-action="history.search"`) assert.Contains(t, app.renderedHTML, `data-sv-action="history.deleteEntry"`) assert.Contains(t, app.renderedHTML, `data-id="42"`) assert.Contains(t, app.renderedHTML, `data-sv-action="history.deleteRange"`) assert.Contains(t, app.renderedHTML, `data-range="hour"`) assert.Contains(t, app.renderedHTML, `data-sv-action="history.filterDomain"`) assert.Contains(t, app.renderedHTML, `data-sv-action="history.deleteDomain"`) assert.Contains(t, app.renderedHTML, `data-domain="example.com:8080"`) assert.Contains(t, app.renderedHTML, `>example.com`) assert.Contains(t, app.renderedHTML, `data-sv-history-search data-sv-autofocus`) assert.NotContains(t, app.renderedHTML, "Keys:") } func TestAppHandleHistoryActionsRefreshesDOM(t *testing.T) { dom := &recordingDOM{} history := &recordingHistoryService{ entries: []*entity.HistoryEntry{{ID: 42, URL: "https://example.com", Title: "Example"}}, searchEntries: []*entity.HistoryEntry{{ID: 7, URL: "https://search.example", Title: "Search result"}}, } app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionSearch, Data: map[string]string{"query": " example "}, })) assert.True(t, history.searchCalled) assert.Equal(t, "example", history.query) assert.Contains(t, dom.HTML(), "Search result") assert.Contains(t, dom.HTML(), "Query: example") app.historyOffset = historyTimelineLimit require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionDeleteEntry, Data: map[string]string{"id": "42"}, })) assert.Equal(t, int64(42), history.deletedEntryID) assert.Equal(t, 0, app.historyOffset) assert.Contains(t, dom.HTML(), "Deleted history entry") require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionDeleteRange, Data: map[string]string{"range": "week"}, })) assert.Equal(t, "week", history.deletedRangeID) assert.Contains(t, dom.HTML(), "Deleted history from this week") require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{Action: historyActionClear})) require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionFilterDomain, Data: map[string]string{"domain": "example.com"}, })) assert.True(t, history.called) assert.Equal(t, "example.com", history.windowDomain) assert.Empty(t, app.historyQuery) require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{ Action: historyActionDeleteDomain, Data: map[string]string{"domain": "example.com"}, })) assert.Equal(t, "example.com", history.deletedDomain) assert.Contains(t, dom.HTML(), "Deleted history for example.com") } func TestAppCloseStopsActionWorkerAndReleasesDOM(t *testing.T) { dom := &recordingActionDOM{} history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}} app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) ctx := t.Context() require.NoError(t, app.RunWithContext(ctx)) require.NotNil(t, dom.handler) app.Close() assert.True(t, dom.released) assert.False(t, app.enqueueDOMAction(DOMAction{Action: historyActionClear})) require.NotPanics(t, func() { dom.handler(DOMAction{Action: historyActionClear}) }) } func TestAppRunCleansActionWorkerWhenBindingFails(t *testing.T) { bindErr := errors.New("bind failed") dom := &failingActionDOM{bindErr: bindErr} app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"}) err := app.RunWithContext(context.Background()) require.ErrorIs(t, err, bindErr) assert.Nil(t, app.actionQueue) assert.Nil(t, app.actionErrorQueue) assert.Nil(t, app.actionCtx) assert.False(t, app.actionWorkerActive()) require.NotPanics(t, app.Close) } func TestAppRunReleasesDOMWhenActionWorkerCannotStart(t *testing.T) { dom := &recordingActionDOM{} history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}} app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"}) ctx, cancel := context.WithCancel(context.Background()) cancel() err := app.RunWithContext(ctx) require.ErrorContains(t, err, "systemview action worker unavailable") assert.True(t, dom.released) assert.Nil(t, app.actionQueue) assert.Nil(t, app.actionErrorQueue) assert.Nil(t, app.actionCtx) assert.False(t, app.actionWorkerActive()) } func TestAppLoadInitialHistoryRouteRendersErrorState(t *testing.T) { t.Parallel() history := &recordingHistoryService{err: errors.New("database unavailable")} app := NewApp(Dependencies{ History: history, LocationURI: "dumb://history", }) require.NoError(t, app.LoadInitial(context.Background())) assert.Equal(t, RouteHistory, app.CurrentRoute()) assert.True(t, history.called) assert.Contains(t, app.renderedHTML, "Could not load this system view") assert.Contains(t, app.renderedHTML, "database unavailable") assert.Contains(t, app.renderedHTML, `role="alert"`) } func TestAppLoadInitialHistoryRouteAppliesThemeTokens(t *testing.T) { t.Parallel() history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ URL: "https://example.com", Title: "Example", }}} config := &recordingConfigService{current: dto.SystemviewConfigPayload{ Appearance: dto.WebUIAppearanceConfig{ ColorScheme: "prefer-light", LightPalette: dto.ColorPalette{ Background: "#ffffff", Surface: "#fafafa", SurfaceVariant: "#eeeeee", Text: "#111111", Muted: "#666666", Accent: "#0055ff", Border: "#dddddd", }, DarkPalette: dto.ColorPalette{ Background: "#111111", Surface: "#1a1a1a", SurfaceVariant: "#2a2a2a", Text: "#f5f5f5", Muted: "#a0a0a0", Accent: "#66aaff", Border: "#333333", }, SansFont: "Inter", SerifFont: "Georgia", MonospaceFont: "JetBrains Mono", DefaultFontSize: 16, }, }} app := NewApp(Dependencies{ Config: config, History: history, LocationURI: "dumb://history", }) require.NoError(t, app.LoadInitial(context.Background())) assert.Contains(t, app.renderedHTML, `class="sv-app sv-light"`) assert.Contains(t, app.renderedHTML, `--sv-background: #ffffff;`) assert.Contains(t, app.renderedHTML, `--sv-surface-variant: #eeeeee;`) } func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) { t.Parallel() favorites := &recordingFavoritesService{ favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}}, tags: []*entity.Tag{{Name: "Go"}}, } app := NewApp(Dependencies{ Favorites: favorites, LocationURI: "dumb://favorites", }) require.NoError(t, app.LoadInitial(context.Background())) assert.Equal(t, RouteFavorites, app.CurrentRoute()) assert.Len(t, app.favorites, 1) assert.Len(t, app.tags, 1) assert.Contains(t, app.renderedHTML, "Favorites") assert.Contains(t, app.renderedHTML, "Example") assert.Contains(t, app.renderedHTML, "Go") assert.True(t, favorites.calledList) assert.True(t, favorites.calledTags) // Shell frame present, no full document wrapper. assert.NotContains(t, app.renderedHTML, "