返回 DeepSeek-Reasonix
window_state_test.go
根目录 / desktop / window_state_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "sync"
8 "testing"
9 )
10
11 func TestValidateWindowStateAcceptsWindowsBorderOrigin(t *testing.T) {
12 state := DesktopWindowState{Width: 1240, Height: 720, X: -8, Y: -8, Maximised: false}
13 if err := validateWindowState(state); err != nil {
14 t.Fatalf("x=-8,y=-8 must be accepted: %v", err)
15 }
16 }
17
18 func TestValidateWindowStateRejectsIllegalSizes(t *testing.T) {
19 cases := []DesktopWindowState{
20 {Width: 0, Height: 720, X: 10, Y: 10},
21 {Width: 100, Height: 720, X: 10, Y: 10},
22 {Width: 1240, Height: 100, X: 10, Y: 10},
23 {Width: maxWindowDimension + 1, Height: 720, X: 10, Y: 10},
24 {Width: 1240, Height: 720, X: minWindowOrigin - 1, Y: 10},
25 {Width: 1240, Height: 720, X: 10, Y: maxWindowOriginAbs + 1},
26 }
27 for _, c := range cases {
28 if err := validateWindowState(c); err == nil {
29 t.Fatalf("expected rejection for %+v", c)
30 }
31 }
32 }
33
34 func TestParseWindowStateJSONCorruptPayload(t *testing.T) {
35 if _, err := parseWindowStateJSON([]byte(`{not-json`)); err == nil {
36 t.Fatal("corrupt JSON must fail")
37 }
38 if _, err := parseWindowStateJSON([]byte(`{"width":50,"height":50,"x":0,"y":0}`)); err == nil {
39 t.Fatal("undersized geometry must fail")
40 }
41 }
42
43 func TestWindowStateRoundTripNegativeDisplayOrigin(t *testing.T) {
44 seedWindowStateFile(t, DesktopWindowState{Width: 1100, Height: 700, X: -1800, Y: -900, Maximised: true})
45 saved, ok := loadWindowState()
46 if !ok {
47 t.Fatal("negative monitor origin rejected")
48 }
49 app := NewApp()
50 if err := app.SaveWindowState(saved); err != nil {
51 t.Fatal(err)
52 }
53 restored, ok := loadWindowState()
54 if !ok || restored != saved {
55 t.Fatalf("round trip: %+v", restored)
56 }
57 }
58
59 func TestSaveWindowStatePersistsAndSeedsLastKnown(t *testing.T) {
60 isolateDesktopUserDirs(t)
61 resetLastKnownWindowStateForTest()
62 t.Cleanup(resetLastKnownWindowStateForTest)
63
64 app := NewApp()
65 want := DesktopWindowState{Width: 1100, Height: 700, X: -8, Y: -8, Maximised: true}
66 if err := app.SaveWindowState(want); err != nil {
67 t.Fatalf("SaveWindowState: %v", err)
68 }
69
70 got, ok := loadWindowState()
71 if !ok {
72 t.Fatal("loadWindowState failed after save")
73 }
74 if got != want {
75 t.Fatalf("loaded state = %+v, want %+v", got, want)
76 }
77 last, ok := lastKnownWindowState()
78 if !ok || last != want {
79 t.Fatalf("last known = %+v ok=%v, want %+v", last, ok, want)
80 }
81 }
82
83 func TestSaveWindowStateRejectsInvalidWithoutMutatingLastKnown(t *testing.T) {
84 isolateDesktopUserDirs(t)
85 resetLastKnownWindowStateForTest()
86 t.Cleanup(resetLastKnownWindowStateForTest)
87
88 app := NewApp()
89 good := DesktopWindowState{Width: 1000, Height: 600, X: 12, Y: 24, Maximised: false}
90 if err := app.SaveWindowState(good); err != nil {
91 t.Fatal(err)
92 }
93 if err := app.SaveWindowState(DesktopWindowState{Width: 10, Height: 10, X: 0, Y: 0}); err == nil {
94 t.Fatal("expected invalid size rejection")
95 }
96 last, ok := lastKnownWindowState()
97 if !ok || last != good {
98 t.Fatalf("last known mutated to %+v ok=%v", last, ok)
99 }
100 // Disk must still hold the last valid report.
101 got, ok := loadWindowState()
102 if !ok || got != good {
103 t.Fatalf("disk state = %+v ok=%v, want %+v", got, ok, good)
104 }
105 }
106
107 func TestSaveWindowStateSyncUsesLastKnownWithoutNativeQuery(t *testing.T) {
108 // Regression: shutdown/beforeClose must not call WindowGetSize/Position/
109 // IsMaximised. saveWindowStateSync only re-writes the last frontend report.
110 isolateDesktopUserDirs(t)
111 resetLastKnownWindowStateForTest()
112 t.Cleanup(resetLastKnownWindowStateForTest)
113
114 app := NewApp()
115 // Simulate a frontend report that was accepted in-memory but not yet on disk
116 // (e.g. crash between remember and write, or deleted file).
117 want := DesktopWindowState{Width: 1280, Height: 800, X: 40, Y: 50, Maximised: false}
118 rememberWindowState(want)
119 _ = os.Remove(windowStatePath())
120
121 app.saveWindowStateSync()
122
123 raw, err := os.ReadFile(windowStatePath())
124 if err != nil {
125 t.Fatalf("expected re-persist: %v", err)
126 }
127 var got DesktopWindowState
128 if err := json.Unmarshal(raw, &got); err != nil {
129 t.Fatal(err)
130 }
131 if got != want {
132 t.Fatalf("re-persisted = %+v, want %+v", got, want)
133 }
134 }
135
136 func TestSaveWindowStateSyncNoopWithoutLastKnown(t *testing.T) {
137 isolateDesktopUserDirs(t)
138 resetLastKnownWindowStateForTest()
139 t.Cleanup(resetLastKnownWindowStateForTest)
140
141 app := NewApp()
142 app.saveWindowStateSync() // must not panic or create a file
143 if _, err := os.Stat(windowStatePath()); !os.IsNotExist(err) {
144 t.Fatalf("expected no file, err=%v", err)
145 }
146 }
147
148 func TestLoadWindowStateCorruptJSON(t *testing.T) {
149 isolateDesktopUserDirs(t)
150 resetLastKnownWindowStateForTest()
151 t.Cleanup(resetLastKnownWindowStateForTest)
152
153 path := windowStatePath()
154 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
155 t.Fatal(err)
156 }
157 if err := os.WriteFile(path, []byte("{broken"), 0o644); err != nil {
158 t.Fatal(err)
159 }
160 if _, ok := loadWindowState(); ok {
161 t.Fatal("corrupt JSON must not restore")
162 }
163 }
164
165 func TestBackgroundHideUsesLastKnownMaximised(t *testing.T) {
166 isolateDesktopUserDirs(t)
167 resetLastKnownWindowStateForTest()
168 t.Cleanup(resetLastKnownWindowStateForTest)
169
170 app := NewApp()
171 if app.lastKnownMaximised() {
172 t.Fatal("default maximised should be false")
173 }
174 if err := app.SaveWindowState(DesktopWindowState{Width: 900, Height: 600, X: 0, Y: 0, Maximised: true}); err != nil {
175 t.Fatal(err)
176 }
177 if !app.lastKnownMaximised() {
178 t.Fatal("expected maximised from last report")
179 }
180 }
181
182 func TestSaveWindowStateConcurrentReports(t *testing.T) {
183 // Shutdown race: concurrent frontend polls + saveWindowStateSync must not
184 // panic or leave invalid JSON.
185 isolateDesktopUserDirs(t)
186 resetLastKnownWindowStateForTest()
187 t.Cleanup(resetLastKnownWindowStateForTest)
188
189 app := NewApp()
190 var wg sync.WaitGroup
191 for i := range 32 {
192 wg.Add(1)
193 go func(i int) {
194 defer wg.Done()
195 state := DesktopWindowState{
196 Width: 800 + i,
197 Height: 600 + i,
198 X: -8 + i%3,
199 Y: -8 + i%5,
200 }
201 _ = app.SaveWindowState(state)
202 app.saveWindowStateSync()
203 }(i)
204 }
205 wg.Wait()
206
207 got, ok := loadWindowState()
208 if !ok {
209 t.Fatal("final load failed after concurrent writes")
210 }
211 if err := validateWindowState(got); err != nil {
212 t.Fatalf("final state invalid: %v (%+v)", err, got)
213 }
214 }
215
216 func TestSaveWindowStateDPIZeroShutdownPathNeverQueriesNative(t *testing.T) {
217 // Contract test: saveWindowStateSync must succeed with ctx nil (no shell
218 // runtime) and never require native DPI/window APIs. This is the regression
219 // guard for ScaleToDefaultDPI panics during Windows shutdown.
220 isolateDesktopUserDirs(t)
221 resetLastKnownWindowStateForTest()
222 t.Cleanup(resetLastKnownWindowStateForTest)
223
224 app := NewApp()
225 app.ctx = nil
226 if err := app.SaveWindowState(DesktopWindowState{Width: 1024, Height: 768, X: -8, Y: -8}); err != nil {
227 t.Fatal(err)
228 }
229 // Wipe disk and re-persist from memory only.
230 _ = os.Remove(windowStatePath())
231 app.saveWindowStateSync()
232 got, ok := loadWindowState()
233 if !ok {
234 t.Fatal("expected re-persisted state without native queries")
235 }
236 if got.X != -8 || got.Y != -8 {
237 t.Fatalf("border origin lost: %+v", got)
238 }
239 }
240
240 lines GO