//go:build !android && !ios && !freebsd && !js package services import ( "fmt" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "github.com/wailsapp/wails/v3/pkg/application" ) func newTestWindowManager() *WindowManager { return &WindowManager{ creating: map[string]bool{}, pendingOps: map[string][]windowOp{}, pendingClose: map[string]windowCloser{}, restoreGen: map[string]uint64{}, hiding: map[string]bool{}, generation: map[string]uint64{}, } } type fakeWindow struct { name string visible bool shown int hidden int } func newFakeWindow(name string) *fakeWindow { return &fakeWindow{name: name, visible: true} } func (f *fakeWindow) Show() application.Window { f.visible = true f.shown++ return nil } func (f *fakeWindow) Hide() application.Window { f.visible = false f.hidden++ return nil } func (f *fakeWindow) IsVisible() bool { return f.visible } func (f *fakeWindow) Name() string { return f.name } type fakeDesktop struct { windows []*fakeWindow raised int } func newFakeDesktop(s *WindowManager, windows ...*fakeWindow) *fakeDesktop { d := &fakeDesktop{windows: windows} s.allWindows = func() []hideableWindow { all := make([]hideableWindow, 0, len(d.windows)) for _, w := range d.windows { all = append(all, w) } return all } s.raiseMain = func() { d.raised++ } return d } func ownersOf(hidden []hiddenWindow) []string { owners := make([]string, 0, len(hidden)) for _, h := range hidden { owners = append(owners, h.owner) } return owners } func waitDone(t *testing.T, done <-chan struct{}, msg string) { t.Helper() select { case <-done: case <-time.After(5 * time.Second): t.Fatal(msg) } } func TestWithWindowReusesExistingWindow(t *testing.T) { s := newTestWindowManager() existing := &application.WebviewWindow{} slot := existing factoryCalls := 0 var got *application.WebviewWindow created := true s.withWindow(windowMain, &slot, func() *application.WebviewWindow { factoryCalls++ return &application.WebviewWindow{} }, func(w *application.WebviewWindow, c bool) { got, created = w, c }) require.Equal(t, 0, factoryCalls) require.Same(t, existing, got) require.False(t, created) } func TestWithWindowNilFactoryWithoutWindowSkipsOp(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow opCalls := 0 s.withWindow(windowMain, &slot, nil, func(*application.WebviewWindow, bool) { opCalls++ }) require.Equal(t, 0, opCalls) require.Nil(t, slot) } func TestWithWindowReentrantCallDuringCreationIsQueued(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow factoryCalls := 0 var order []string var factory func() *application.WebviewWindow factory = func() *application.WebviewWindow { factoryCalls++ // Simulates the Windows message pump re-entering the tray click handler // while WebView2 is still initialising the window being created. s.withWindow(windowMain, &slot, factory, func(_ *application.WebviewWindow, created bool) { order = append(order, fmt.Sprintf("reentrant:%v", created)) }) return &application.WebviewWindow{} } done := make(chan struct{}) go func() { defer close(done) s.withWindow(windowMain, &slot, factory, func(_ *application.WebviewWindow, created bool) { order = append(order, fmt.Sprintf("outer:%v", created)) }) }() waitDone(t, done, "withWindow deadlocked on a re-entrant call during creation") require.Equal(t, 1, factoryCalls) require.Equal(t, []string{"outer:true", "reentrant:false"}, order) require.NotNil(t, slot) require.Empty(t, s.creating) require.Empty(t, s.pendingOps) } func TestWithWindowConcurrentCallersShareOneCreation(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow factoryEntered := make(chan struct{}) release := make(chan struct{}) var factoryCalls, opCalls atomic.Int32 factory := func() *application.WebviewWindow { factoryCalls.Add(1) close(factoryEntered) <-release return &application.WebviewWindow{} } op := func(*application.WebviewWindow, bool) { opCalls.Add(1) } first := make(chan struct{}) go func() { defer close(first) s.withWindow(windowSettings, &slot, factory, op) }() <-factoryEntered second := make(chan struct{}) go func() { defer close(second) s.withWindow(windowSettings, &slot, factory, op) }() waitDone(t, second, "second caller blocked while the window was being created") require.Equal(t, int32(0), opCalls.Load()) close(release) waitDone(t, first, "creator did not finish") require.Equal(t, int32(1), factoryCalls.Load()) require.Equal(t, int32(2), opCalls.Load()) require.NotNil(t, slot) } func TestWithWindowOpsQueuedDuringCreationRunInArrivalOrder(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow var order []string record := func(label string) windowOp { return func(_ *application.WebviewWindow, created bool) { order = append(order, fmt.Sprintf("%s:%v", label, created)) } } var factory func() *application.WebviewWindow factory = func() *application.WebviewWindow { s.withWindow(windowMain, &slot, factory, func(w *application.WebviewWindow, created bool) { record("a")(w, created) // Arrives while the creator is still draining the queue: it must not // jump ahead of "b" through the existing-window fast path. s.withWindow(windowMain, &slot, factory, record("c")) }) s.withWindow(windowMain, &slot, factory, record("b")) return &application.WebviewWindow{} } done := make(chan struct{}) go func() { defer close(done) s.withWindow(windowMain, &slot, factory, record("outer")) }() waitDone(t, done, "withWindow deadlocked while draining queued operations") require.Equal(t, []string{"outer:true", "a:false", "b:false", "c:false"}, order) require.Empty(t, s.creating) require.Empty(t, s.pendingOps) } func TestWithWindowFactoryPanicReleasesCreation(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow func() { defer func() { require.NotNil(t, recover()) }() s.withWindow(windowMain, &slot, func() *application.WebviewWindow { panic("factory failed") }, func(*application.WebviewWindow, bool) {}) }() require.Empty(t, s.creating) require.Empty(t, s.pendingOps) require.Nil(t, slot) created := false s.withWindow(windowMain, &slot, func() *application.WebviewWindow { return &application.WebviewWindow{} }, func(_ *application.WebviewWindow, c bool) { created = c }) require.True(t, created) require.NotNil(t, slot) } func TestWithWindowNilFromFactoryReleasesCreation(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow opCalls := 0 s.withWindow(windowMain, &slot, func() *application.WebviewWindow { return nil }, func(*application.WebviewWindow, bool) { opCalls++ }) require.Equal(t, 0, opCalls) require.Empty(t, s.creating) require.Nil(t, slot) } func TestCloseWindowDuringCreationDefersCloseAndSkipsOps(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow created := &application.WebviewWindow{} opCalls, closeCalls := 0, 0 var closed *application.WebviewWindow s.withWindow(windowError, &slot, func() *application.WebviewWindow { s.closeWindow(windowError, &slot, func(w *application.WebviewWindow) { closeCalls++ closed = w }) require.Equal(t, 0, closeCalls) return created }, func(*application.WebviewWindow, bool) { opCalls++ }) require.Equal(t, 0, opCalls) require.Equal(t, 1, closeCalls) require.Same(t, created, closed) require.Nil(t, slot) require.Empty(t, s.creating) require.Empty(t, s.pendingOps) require.Empty(t, s.pendingClose) factoryCalls := 0 reopened := false s.withWindow(windowError, &slot, func() *application.WebviewWindow { factoryCalls++ return &application.WebviewWindow{} }, func(_ *application.WebviewWindow, c bool) { reopened = c }) require.Equal(t, 1, factoryCalls) require.True(t, reopened) require.NotNil(t, slot) } func TestCloseWindowDuringDrainStopsRemainingOps(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow var order []string closeCalls := 0 var factory func() *application.WebviewWindow factory = func() *application.WebviewWindow { s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) { order = append(order, "a") s.closeWindow(windowWelcome, &slot, func(*application.WebviewWindow) { closeCalls++ }) s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) { order = append(order, "c") }) }) s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) { order = append(order, "b") }) return &application.WebviewWindow{} } s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) { order = append(order, "outer") }) // "b" was queued before the close and "c" after it; a close supersedes both // rather than showing a window that is about to be destroyed. require.Equal(t, []string{"outer", "a"}, order) require.Equal(t, 1, closeCalls) require.Nil(t, slot) require.Empty(t, s.creating) require.Empty(t, s.pendingOps) require.Empty(t, s.pendingClose) } func TestCloseWindowWithoutWindowSkipsCloser(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow calls := 0 s.closeWindow(windowBrowserLogin, &slot, func(*application.WebviewWindow) { calls++ }) require.Equal(t, 0, calls) require.Nil(t, slot) require.Empty(t, s.pendingClose) } func TestCloseWindowWithExistingWindowRunsCloser(t *testing.T) { s := newTestWindowManager() existing := &application.WebviewWindow{} slot := existing var got *application.WebviewWindow s.closeWindow(windowError, &slot, func(w *application.WebviewWindow) { got = w }) require.Same(t, existing, got) require.Nil(t, slot) require.Empty(t, s.pendingClose) } func TestWithWindowNilFromFactoryDropsPendingClose(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow closeCalls := 0 s.withWindow(windowError, &slot, func() *application.WebviewWindow { s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { closeCalls++ }) return nil }, func(*application.WebviewWindow, bool) {}) require.Equal(t, 0, closeCalls) require.Nil(t, slot) require.Empty(t, s.creating) require.Empty(t, s.pendingClose) } func TestWithWindowFactoryPanicDropsPendingClose(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow closeCalls := 0 func() { defer func() { require.NotNil(t, recover()) }() s.withWindow(windowError, &slot, func() *application.WebviewWindow { s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { closeCalls++ }) panic("factory failed") }, func(*application.WebviewWindow, bool) {}) }() require.Equal(t, 0, closeCalls) require.Empty(t, s.creating) require.Empty(t, s.pendingClose) } func TestCloseWindowKeepsFirstDeferredCloser(t *testing.T) { s := newTestWindowManager() var slot *application.WebviewWindow var ran []string s.withWindow(windowError, &slot, func() *application.WebviewWindow { s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { ran = append(ran, "first") }) s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { ran = append(ran, "second") }) return &application.WebviewWindow{} }, func(*application.WebviewWindow, bool) {}) require.Equal(t, []string{"first"}, ran) require.Nil(t, slot) require.Empty(t, s.pendingClose) } func TestCloseRenewFlowDuringBrowserLoginCreationRestoresHiddenWindows(t *testing.T) { s := newTestWindowManager() s.withWindow(windowBrowserLogin, &s.browserLogin, func() *application.WebviewWindow { s.CloseRenewFlow() // Seeded after the call so the deferred closer, not CloseRenewFlow's own // immediate restore, is what has to drain it. A nil entry is skipped by // restoreHiddenWindows, so no Wails window is needed. s.hiddenWindows = []hiddenWindow{{owner: windowBrowserLogin}} return &application.WebviewWindow{} }, func(*application.WebviewWindow, bool) {}) require.Nil(t, s.browserLogin) require.Empty(t, s.hiddenWindows) require.Empty(t, s.creating) require.Empty(t, s.pendingClose) } func TestHideOtherWindowsSkipsKeepNameAndInvisible(t *testing.T) { main := newFakeWindow(windowMain) settings := newFakeWindow(windowSettings) settings.visible = false popup := newFakeWindow(windowBrowserLogin) s := newTestWindowManager() newFakeDesktop(s, main, settings, popup) s.hideOtherWindows(windowBrowserLogin) require.False(t, main.visible) require.Equal(t, 1, main.hidden) require.Equal(t, 0, settings.hidden, "an already hidden window must not be recorded") require.Equal(t, 0, popup.hidden, "the popup itself must stay visible") require.Equal(t, []string{windowBrowserLogin}, ownersOf(s.hiddenWindows)) } func TestInstallDuringLoginKeepsMainHiddenUntilLoginCloses(t *testing.T) { main := newFakeWindow(windowMain) login := newFakeWindow(windowBrowserLogin) install := newFakeWindow(windowInstallProgress) install.visible = false s := newTestWindowManager() d := newFakeDesktop(s, main, login, install) s.hideOtherWindows(windowBrowserLogin) require.False(t, main.visible) install.visible = true s.hideOtherWindows(windowInstallProgress) require.False(t, login.visible, "the install popup hides the login popup") s.restoreHiddenWindows(windowInstallProgress) require.True(t, login.visible, "the install popup restores the login popup it hid") require.False(t, main.visible, "the main window stays hidden for the login popup") require.Equal(t, 0, d.raised) require.Equal(t, []string{windowBrowserLogin}, ownersOf(s.hiddenWindows)) s.restoreHiddenWindows(windowBrowserLogin) require.True(t, main.visible) require.Equal(t, 1, d.raised, "restoring the main window raises it above the SSO browser") require.Empty(t, s.hiddenWindows) } func TestLoginClosingUnderInstallHandsMainToInstall(t *testing.T) { main := newFakeWindow(windowMain) login := newFakeWindow(windowBrowserLogin) install := newFakeWindow(windowInstallProgress) install.visible = false s := newTestWindowManager() d := newFakeDesktop(s, main, login, install) s.hideOtherWindows(windowBrowserLogin) install.visible = true s.hideOtherWindows(windowInstallProgress) require.False(t, main.visible) require.False(t, login.visible, "the install popup hides the login popup") // The login popup closes while the install popup is still up: the main window it // hid must not resurface under the install popup, it is handed over instead. s.restoreHiddenWindows(windowBrowserLogin) require.False(t, main.visible, "the install popup still covers the main window") require.Equal(t, 0, d.raised) require.Equal(t, []string{windowInstallProgress, windowInstallProgress}, ownersOf(s.hiddenWindows)) s.restoreHiddenWindows(windowInstallProgress) require.True(t, main.visible, "the install popup restores the handed-over main window") require.Equal(t, 1, d.raised) require.Empty(t, s.hiddenWindows) } func TestInstallClosingUnderLoginHandsMainToLogin(t *testing.T) { main := newFakeWindow(windowMain) install := newFakeWindow(windowInstallProgress) login := newFakeWindow(windowBrowserLogin) login.visible = false s := newTestWindowManager() d := newFakeDesktop(s, main, install, login) s.hideOtherWindows(windowInstallProgress) login.visible = true s.hideOtherWindows(windowBrowserLogin) require.False(t, install.visible, "the login popup hides the install popup") s.restoreHiddenWindows(windowInstallProgress) require.False(t, main.visible, "the login popup still covers the main window") require.Equal(t, 0, d.raised) require.Equal(t, []string{windowBrowserLogin, windowBrowserLogin}, ownersOf(s.hiddenWindows)) s.restoreHiddenWindows(windowBrowserLogin) require.True(t, main.visible) require.Equal(t, 1, d.raised) require.Empty(t, s.hiddenWindows) } func TestPopupClosingReshowsTheCoveringPopupItself(t *testing.T) { main := newFakeWindow(windowMain) install := newFakeWindow(windowInstallProgress) login := newFakeWindow(windowBrowserLogin) login.visible = false s := newTestWindowManager() d := newFakeDesktop(s, main, install, login) s.hideOtherWindows(windowInstallProgress) login.visible = true s.hideOtherWindows(windowBrowserLogin) // The login popup hid the install popup itself; closing the login popup must bring // the install popup back rather than hand it over to its own owner. s.restoreHiddenWindows(windowBrowserLogin) require.True(t, install.visible, "a popup is never handed over to itself") require.False(t, main.visible, "the main window stays with the install popup") require.Equal(t, 0, d.raised) require.Equal(t, []string{windowInstallProgress}, ownersOf(s.hiddenWindows)) } func TestRestoreHiddenWindowsUnknownOwnerKeepsEverything(t *testing.T) { main := newFakeWindow(windowMain) s := newTestWindowManager() d := newFakeDesktop(s, main) s.hideOtherWindows(windowBrowserLogin) s.restoreHiddenWindows(windowWelcome) require.False(t, main.visible) require.Equal(t, []string{windowBrowserLogin}, ownersOf(s.hiddenWindows)) require.Equal(t, 0, d.raised) } func TestRestoreHiddenWindowsWithoutMainDoesNotRaise(t *testing.T) { settings := newFakeWindow(windowSettings) s := newTestWindowManager() d := newFakeDesktop(s, settings) s.hideOtherWindows(windowBrowserLogin) s.restoreHiddenWindows(windowBrowserLogin) require.True(t, settings.visible) require.Equal(t, 0, d.raised) } func TestRestoreHiddenWindowsEmptyIsNoop(t *testing.T) { s := newTestWindowManager() require.NotPanics(t, func() { s.restoreHiddenWindows(windowBrowserLogin) }) require.Empty(t, s.hiddenWindows) } func TestHideOtherWindowsRacingOwnRestoreReshowsWhatItHid(t *testing.T) { main := newFakeWindow(windowMain) s := newTestWindowManager() d := newFakeDesktop(s, main) enumerate := s.allWindows // A restore for the same owner lands between the generation snapshot and the record. s.allWindows = func() []hideableWindow { s.restoreHiddenWindows(windowBrowserLogin) return enumerate() } s.hideOtherWindows(windowBrowserLogin) require.True(t, main.visible) require.Equal(t, 1, main.hidden) require.Empty(t, s.hiddenWindows) require.Equal(t, 0, d.raised) } func TestHideOtherWindowsIgnoresRestoreOfAnotherOwner(t *testing.T) { main := newFakeWindow(windowMain) s := newTestWindowManager() newFakeDesktop(s, main) enumerate := s.allWindows s.allWindows = func() []hideableWindow { s.restoreHiddenWindows(windowInstallProgress) return enumerate() } s.hideOtherWindows(windowBrowserLogin) require.False(t, main.visible) require.Equal(t, []string{windowBrowserLogin}, ownersOf(s.hiddenWindows)) } func TestRestoringCloserRestoresOnlyItsOwner(t *testing.T) { main := newFakeWindow(windowMain) s := newTestWindowManager() newFakeDesktop(s, main) s.hideOtherWindows(windowBrowserLogin) s.hiddenWindows = append(s.hiddenWindows, hiddenWindow{owner: windowInstallProgress}) s.restoringCloser(windowBrowserLogin)(&application.WebviewWindow{}) require.True(t, main.visible) require.Equal(t, []string{windowInstallProgress}, ownersOf(s.hiddenWindows)) } func TestStampGenerationTracksLatestPerWindow(t *testing.T) { s := newTestWindowManager() first := s.stampGeneration(windowBrowserLogin, "/#/dialog/browser-login") require.Equal(t, "/#/dialog/browser-login?gen=1", first) require.True(t, s.matchesGeneration(windowBrowserLogin, 1)) second := s.stampGeneration(windowBrowserLogin, "/#/dialog/browser-login?uri=x") require.Equal(t, "/#/dialog/browser-login?uri=x&gen=2", second) require.False(t, s.matchesGeneration(windowBrowserLogin, 1)) require.True(t, s.matchesGeneration(windowBrowserLogin, 2)) } func TestMatchesGenerationUntrackedWindowAccepts(t *testing.T) { s := newTestWindowManager() require.True(t, s.matchesGeneration(windowMain, 0)) } func TestPaintedGeneration(t *testing.T) { tests := []struct { name string data any want uint64 }{ {"string", "7", 7}, {"float", float64(7), 7}, {"slice", []any{"7"}, 7}, {"empty slice", []any{}, 0}, {"unparsable", "abc", 0}, {"nil", nil, 0}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.want, paintedGeneration(tc.data)) }) } }