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, "