1package systemviews
2
3// P4.2 readiness tests: the shell loading state clears after the initial
4// mount and action binding, never gated on async history-data completion.
5// Fatal mount/bind failures report to the shell while preserving the
6// original error.
7
8import (
9 "context"
10 "errors"
11 "testing"
12 "time"
13
14 "github.com/bnema/dumber/internal/domain/entity"
15 "github.com/stretchr/testify/require"
16)
17
18// signalingDOM records readiness signals on top of the recording mount.
19type signalingDOM struct {
20 recordingDOM
21 ready bool
22 failed string
23}
24
25func (d *signalingDOM) SignalReady() error {
26 d.mu.Lock()
27 defer d.mu.Unlock()
28 d.ready = true
29 return nil
30}
31
32func (d *signalingDOM) SignalError(message string) error {
33 d.mu.Lock()
34 defer d.mu.Unlock()
35 d.failed = message
36 return nil
37}
38
39func (d *signalingDOM) isReady() bool {
40 d.mu.Lock()
41 defer d.mu.Unlock()
42 return d.ready
43}
44
45func (d *signalingDOM) failure() string {
46 d.mu.Lock()
47 defer d.mu.Unlock()
48 return d.failed
49}
50
51// signalingActionDOM binds actions and signals readiness.
52type signalingActionDOM struct {
53 signalingDOM
54 handler DOMActionHandler
55}
56
57func (d *signalingActionDOM) BindActions(handler DOMActionHandler) error {
58 d.handler = handler
59 return nil
60}
61
62func TestReadinessSignaledAfterMountAndBind(t *testing.T) {
63 t.Parallel()
64
65 dom := &signalingActionDOM{}
66 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
67 require.NoError(t, app.Run())
68 require.True(t, dom.isReady(), "readiness must fire after mount and binding")
69 require.Empty(t, dom.failure())
70}
71
72func TestReadinessMountFailureSignalsError(t *testing.T) {
73 t.Parallel()
74
75 dom := &failingMountSignalingDOM{err: errors.New("mount blew up")}
76 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
77 err := app.Run()
78 require.ErrorIs(t, err, dom.err, "original mount error must propagate")
79 require.Contains(t, dom.failure(), "mount blew up")
80 require.False(t, dom.isReady())
81}
82
83// failingMountSignalingDOM fails Mount while recording signals.
84type failingMountSignalingDOM struct {
85 signalingDOM
86 err error
87}
88
89func (d *failingMountSignalingDOM) Mount(string) error {
90 return d.err
91}
92
93func TestReadinessBindFailureSignalsError(t *testing.T) {
94 t.Parallel()
95
96 dom := &failingBindSignalingDOM{err: errors.New("bind blew up")}
97 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
98 err := app.Run()
99 require.ErrorIs(t, err, dom.err, "original bind error must propagate")
100 require.Contains(t, dom.failure(), "bind blew up")
101 require.False(t, dom.isReady())
102}
103
104// failingBindSignalingDOM fails BindActions while recording signals.
105type failingBindSignalingDOM struct {
106 signalingDOM
107 err error
108}
109
110func (d *failingBindSignalingDOM) BindActions(DOMActionHandler) error {
111 return d.err
112}
113
114func TestReadinessWithoutSignalerKeepsLegacyBehavior(t *testing.T) {
115 t.Parallel()
116
117 dom := &recordingDOM{}
118 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
119 require.NoError(t, app.Run())
120 require.True(t, dom.Mounted())
121}
122
123func TestReadinessSignalFailurePropagates(t *testing.T) {
124 t.Parallel()
125
126 dom := &failingReadySignalingDOM{err: errors.New("signal blew up")}
127 app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
128 require.ErrorIs(t, app.Run(), dom.err)
129}
130
131// failingReadySignalingDOM binds fine but fails the readiness signal.
132type failingReadySignalingDOM struct {
133 signalingDOM
134 err error
135}
136
137func (d *failingReadySignalingDOM) BindActions(DOMActionHandler) error {
138 return nil
139}
140
141func (d *failingReadySignalingDOM) SignalReady() error {
142 return d.err
143}
144
145func TestReadinessNotGatedOnAsyncHistoryData(t *testing.T) {
146 t.Parallel()
147
148 dom := &signalingActionDOM{signalingDOM: signalingDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}}}
149 history := &recordingHistoryService{
150 entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Loaded entry"}},
151 timelineStarted: make(chan struct{}),
152 releaseTimeline: make(chan struct{}),
153 }
154 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
155
156 require.NoError(t, app.RunWithContext(context.Background()))
157 select {
158 case <-history.timelineStarted:
159 case <-time.After(5 * time.Second):
160 t.Fatal("history timeline was not started asynchronously")
161 }
162 // The async hydration is still blocked, yet readiness already fired:
163 // history-data completion is separate from shell readiness.
164 require.True(t, dom.isReady(), "readiness must not wait for history data")
165 close(history.releaseTimeline)
166}