1package selection
2
3import (
4 "context"
5 "image"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/nefercap/internal/ports"
12)
13
14func TestBackdropImageIsOpaqueRGBA(t *testing.T) {
15 // One XRGB8888 pixel is B, G, R, X in memory; the stride has padding.
16 f := ports.Frame{Pixels: []byte{
17 0x10, 0x20, 0x30, 0x00, 0x40, 0x50, 0x60, 0x00, 0xee, 0xee, 0xee, 0xee,
18 0x70, 0x80, 0x90, 0x00, 0xa0, 0xb0, 0xc0, 0x00,
19 }, Width: 2, Height: 2, Stride: 12, Format: ports.XRGB8888}
20 require.NoError(t, f.Validate())
21 img := backdropImage(f)
22 assert.Equal(t, image.Rect(0, 0, 2, 2), img.Bounds())
23 assert.Equal(t, []byte{0x30, 0x20, 0x10, 0xff, 0x60, 0x50, 0x40, 0xff, 0x90, 0x80, 0x70, 0xff, 0xc0, 0xb0, 0xa0, 0xff}, img.Pix)
24}
25
26func TestWithBackdrops(t *testing.T) {
27 small := ports.Frame{Pixels: make([]byte, 4*4*4), Width: 4, Height: 4, Stride: 16, Format: ports.XRGB8888}
28 s, err := New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{7: small})
29 require.NoError(t, err)
30 assert.Contains(t, s.backdrops, uint32(7))
31
32 huge := ports.Frame{Width: maxBackdrop + 1, Height: 1, Stride: (maxBackdrop + 1) * 4, Format: ports.XRGB8888}
33 _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})
34 assert.ErrorContains(t, err, "at most 8192", "the caller freezes only outputs CanBackdrop accepts")
35
36 huge = ports.Frame{Width: 7680, Height: 4320, Stride: 7680 * 4, Format: ports.XRGB8888}
37 _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})
38 assert.ErrorContains(t, err, "67108864 bytes")
39}
40
41func TestCanBackdrop(t *testing.T) {
42 assert.True(t, CanBackdrop(ports.Output{Width: 3840, Height: 2160}))
43 assert.True(t, CanBackdrop(ports.Output{Width: 2160, Height: 4096}))
44 assert.True(t, CanBackdrop(ports.Output{Width: 5120, Height: 2160}))
45 assert.True(t, CanBackdrop(ports.Output{Width: 8192, Height: 2048}), "exact byte budget")
46 assert.False(t, CanBackdrop(ports.Output{Width: 8192, Height: 2049}))
47 assert.False(t, CanBackdrop(ports.Output{Width: 7680, Height: 4320}))
48 assert.False(t, CanBackdrop(ports.Output{Width: 8193, Height: 1}))
49 assert.False(t, CanBackdrop(ports.Output{Width: 1, Height: 8193}))
50 assert.False(t, CanBackdrop(ports.Output{}), "an unknown mode stays live")
51}
52
53// The frozen screen is drawn under a screenshot selection only: a recording
54// selects on the live screen.
55func TestBackdropDrawnOnlyForScreenshots(t *testing.T) {
56 m := newTestModel(t)
57 m.backdrop = image.NewNRGBA(image.Rect(0, 0, 1920, 1080))
58 assert.True(t, drawsBackdrop(m))
59 m.mode = ports.Record
60 assert.False(t, drawsBackdrop(m))
61 m.mode, m.backdrop = ports.Screenshot, nil
62 assert.False(t, drawsBackdrop(m))
63}
64
65func frozenModel(t *testing.T, index int, sess *session) *model {
66 t.Helper()
67 m := newModel(ports.Screenshot, testOutputs, index, sess, false)
68 m.backdrop = image.NewNRGBA(image.Rect(0, 0, 1, 1))
69 m.wake = make(chan struct{}, 1)
70 m.resize(1920, 1080, 1)
71 return m
72}
73
74// F switches every overlay between the frozen and the live screen, the
75// footer says what F does next, and the accepted pick records the state.
76func TestFreezeKeyTogglesEveryOverlay(t *testing.T) {
77 sess, _ := testSession()
78 sess.canFreeze = true
79 a, b := frozenModel(t, 0, sess), frozenModel(t, 1, sess)
80 sess.wakes = []chan struct{}{a.wake, b.wake}
81 require.True(t, drawsBackdrop(a))
82 assert.Contains(t, a.footerText, "G grid · F live · Esc cancel")
83
84 assert.True(t, a.input(key("f")), "F redraws")
85 assert.False(t, drawsBackdrop(a))
86 assert.Equal(t, toastLive, a.toast.text)
87 assert.Contains(t, a.footerText, "F freeze")
88 require.Len(t, b.wake, 1, "the other overlay is woken")
89 b.input(motion(1, 1))
90 assert.False(t, drawsBackdrop(b), "the other overlay follows")
91
92 b.input(key("F"))
93 assert.True(t, drawsBackdrop(b))
94 a.input(motion(2, 2))
95 assert.True(t, drawsBackdrop(a))
96
97 a.input(key("f"))
98 a.input(key("m"))
99 a.input(key("Return"))
100 _, ok, err := sess.outcome(context.Background())
101 require.NoError(t, err)
102 require.True(t, ok)
103 assert.False(t, sess.acceptedFrozen(), "a pick on the live screen is captured live")
104}
105
106// The live state survives a round trip through record mode, where F is
107// ignored.
108func TestFreezeKeyWithTab(t *testing.T) {
109 sess, _ := testSession()
110 sess.canFreeze = true
111 m := frozenModel(t, 0, sess)
112 m.toggle = true
113 m.input(key("f"))
114 m.input(key("Tab"))
115 require.Equal(t, ports.Record, m.mode)
116 assert.False(t, m.input(key("f")), "F is ignored in record mode")
117 m.input(key("Tab"))
118 assert.False(t, drawsBackdrop(m), "still live after the round trip")
119 assert.Contains(t, m.footerText, "F freeze")
120}
121
122// Like Tab, F allocates only the rebuilt footer text.
123func TestFreezeKeyAllocations(t *testing.T) {
124 if raceEnabled {
125 t.Skip("the race detector adds allocations")
126 }
127 sess, _ := testSession()
128 sess.canFreeze = true
129 m := frozenModel(t, 0, sess)
130 n := testing.AllocsPerRun(100, func() {
131 m.input(key("f"))
132 _ = m.scene()
133 })
134 assert.LessOrEqual(t, n, 1.0, "an F press allocates only the rebuilt legend text")
135}
136
137// Without a frozen screen, or in record mode, F does nothing and is not shown.
138func TestFreezeKeyHiddenWithoutBackdrop(t *testing.T) {
139 m := newTestModel(t)
140 assert.NotContains(t, m.footerText, "F ")
141 assert.False(t, m.input(key("f")))
142
143 sess, _ := testSession()
144 sess.canFreeze = true
145 m = frozenModel(t, 0, sess)
146 m.mode = ports.Record
147 m.refreshFooter()
148 assert.NotContains(t, m.footerText, "F ")
149 assert.False(t, m.input(key("f")))
150 m.input(key("m"))
151 m.input(key("Return"))
152 assert.True(t, sess.acceptedFrozen(), "untouched, the frozen state is kept for the app to thaw")
153}