Files
netbird/client/ui/services/windowmanager_test.go
T
Zoltan PappandClaude Opus 5 bc44cdc37a [client] Fix browser login popup show from go (#7408)
* [client] Show the SSO login popup and open the browser from Go

The browser-login popup was created hidden and relied on its own webview
to size and show itself and to launch the external browser. On macOS a
hidden WKWebView gets throttled or suspended (App Nap / hidden-window
throttling), so on the first-use path nothing appeared and the browser
never opened, leaving the session-expiration dialog disabled until the
PKCE flow timed out. Reproduced by freezing the popup's WebContent
process: the old code showed nothing, the new code shows the popup and
opens the browser within 30 ms regardless of the webview state.

Show and focus the popup from Go right after creation and launch the
browser from Go on both the create and reuse paths. The popup's frontend
no longer shows or focuses itself, so the browser keeps the foreground
once it activates. This also fixes the reuse path, where a fragment-only
SetURL kept the mounted React tree and the once-only guard skipped
opening the browser for the new URI. Browser launch failures surface in
the error dialog instead of being swallowed.

* [client] Show every dialog window from Go once its frontend has painted

Dialog windows (browser-login, session-expiration, install-progress,
welcome, error) were created hidden and made visible only by their own
webview's Show call after sizing. A hidden WKWebView on macOS can be
throttled or suspended before that code runs, which left the window
hidden forever. The main and settings windows already avoided this with
the painted event plus a fallback timer, but that timer was armed on
WindowRuntimeReady, which a frozen webview never reaches either.

Route all dialogs through the same mechanism: the auto-size hook emits
the painted event instead of showing the window, Go shows and focuses
it on that event, and a fallback timer armed at creation shows it after
3 s regardless. The browser-login popup opens the browser in an
after-show callback so the browser still lands in front of the popup,
also on the fallback path.

* [client] Tie install-progress hidden-window restore to the current popup

CloseInstallProgress nils s.installProgress before calling w.Close(), so a
replacement popup can open before the old window's WindowClosing event runs.
The old callback then restored the windows the replacement had just hidden,
because the restore sat outside the identity check.

Guard the restore with the same check the state reset uses, and restore from
CloseInstallProgress itself so the programmatic close path still re-shows the
hidden windows — mirroring how CloseBrowserLogin already handles it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [client] Correlate painted reports with the window generation that sent them

A painted report carried only the window name, so a late report from a popup
that was already closed and replaced marked its replacement ready. The
replacement was then shown before its own frontend had rendered, which is the
blank-dialog case this flow exists to prevent.

Each dialog start URL now carries a monotonic generation token, echoed back by
ReadySignal, and a report whose token no longer matches the live window is
dropped.

* [client] Separate a window being painted from its frontend being mounted

One flag gated both showing a window and emitting to it, so the fallback timer
set it for a frontend that had not subscribed yet: the queued events were
flushed into a window that could not hear them, losing the login trigger and
the settings tab selection.

Showing is now gated on painted and emitting on mounted, and only a real
frontend report sets mounted. The fallback timer also moved to its own helper
so the runtime-ready hook can rearm it, giving the frontend a full budget to
mount rather than sharing one with webview boot.

* [client] Tag hidden windows with the popup that hid them

Windows hidden while a popup owned the screen went into one untagged list, so
whichever popup closed first restored all of them and emptied the list. An
install started during SSO login re-showed the main window the login popup had
deliberately hidden, and left the login popup with nothing to restore.

Each entry now records the popup that hid it, and a restore releases only that
popup's own entries. This also subsumes the manual filtering CloseRenewFlow did
to keep its own session-expiration window from being re-shown.

* [client] Cover the hidden-window bookkeeping with tests

application.Window carries unexported methods, so the hide/restore paths could
not be faked and the earlier tests could only assert which entries survived a
restore, never which windows were actually shown.

The bookkeeping now goes through hideableWindow, the four methods it needs,
with the window enumeration and the main-window raise behind seams that are nil
in production. That makes the case the owner tag exists for testable end to
end: an install started during SSO login restores only the login popup it hid,
and leaves the main window hidden until the login popup itself closes.

* [client] Report the first paint from unstamped windows too

The main and settings windows carry no generation token, so ReadySignal
saw an empty generation that already matched the ref's initial value and
never emitted the painted event. Those windows only became visible through
the fallback timer, and their frontend was never marked mounted, so the
login trigger and the requested settings tab stayed queued.

Start the ref from null so the first report goes out regardless of the
generation value.

* [client] Hand covered windows over when a popup closes under another

Closing the browser-login popup while the install-progress popup was
still up restored the main window the login had hidden, even though the
install popup was meant to own the screen until it finished. The owner
tag on each hidden entry only stops a popup from restoring another's
windows; it says nothing about what to do with its own when a second
popup still covers them.

Track which popups currently own the screen and, on restore, re-tag the
entries another live popup covers to that popup instead of showing them.
A popup is never handed its own window, so closing the popup on top still
brings the one below back.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 15:32:48 +02:00

640 lines
20 KiB
Go

//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))
})
}
}