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