mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 07:03:34 +08:00
- 单次快捷键可重新唤起停驻 AI 子窗,并过滤重复键盘事件 - 打开 AI 设置前原子隐藏子窗,避免设置窗口被遮挡 - 用可见性版本阻止延迟隐藏或设置事件覆盖新焦点 - 拒绝复用终态子进程,并恢复失败的附加与关闭操作 - 串行展示、隐藏和终态退出,消除关闭门闩并发竞态 - 增加原生窗口生命周期、结果回滚和焦点恢复回归测试
706 lines
22 KiB
Go
706 lines
22 KiB
Go
package nativewindow
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDetachedChildQueuesFocusUntilFrontendReadyHandshake(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
shows := 0
|
|
focuses := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
|
|
control.markDOMReady(ctx)
|
|
if result := control.Focus(); !result.Success {
|
|
t.Fatalf("pre-ready Focus result = %#v", result)
|
|
}
|
|
if shows != 0 || focuses != 0 {
|
|
t.Fatalf("pre-ready presentation = show %d focus %d, want 0/0", shows, focuses)
|
|
}
|
|
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 1 {
|
|
t.Fatalf("ready presentation = show %d focus %d, want 1/1", shows, focuses)
|
|
}
|
|
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("repeated ready Action result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 1 {
|
|
t.Fatalf("repeated ready presentation = show %d focus %d, want 1/1", shows, focuses)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildShowsAfterReadyAndFocusesWithoutShowingAgain(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
shows := 0
|
|
focuses := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
|
|
control.markDOMReady(ctx)
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 0 {
|
|
t.Fatalf("ready presentation = show %d focus %d, want 1/0", shows, focuses)
|
|
}
|
|
|
|
if result := control.Focus(); !result.Success {
|
|
t.Fatalf("post-ready Focus result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 1 {
|
|
t.Fatalf("post-ready presentation = show %d focus %d, want 1/1", shows, focuses)
|
|
}
|
|
|
|
if result := control.Focus(); !result.Success {
|
|
t.Fatalf("later Focus result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 2 {
|
|
t.Fatalf("later presentation = show %d focus %d, want 1/2", shows, focuses)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildAcknowledgesFocusOnlyAfterDelayedPresentation(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
steps := make([]string, 0, 4)
|
|
control.showWindow = func(context.Context) { steps = append(steps, "show") }
|
|
control.focusWindow = func(context.Context) { steps = append(steps, "focus") }
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
switch request.URL.Path {
|
|
case CommandStatePath:
|
|
var acknowledgement commandStateRequest
|
|
if err := json.NewDecoder(request.Body).Decode(&acknowledgement); err != nil {
|
|
return nil, err
|
|
}
|
|
steps = append(steps, "ack-focus")
|
|
if acknowledgement.Action != "ack-focus" || acknowledgement.VisibilityRevision != 7 {
|
|
t.Fatalf("focus acknowledgement = %#v", acknowledgement)
|
|
}
|
|
case ActionPath:
|
|
steps = append(steps, "post-ready")
|
|
}
|
|
return successfulVisibilityResponse(), nil
|
|
})
|
|
|
|
if result := control.FocusRevision(7); !result.Success {
|
|
t.Fatalf("pre-ready FocusRevision result = %#v", result)
|
|
}
|
|
if len(steps) != 0 {
|
|
t.Fatalf("pre-ready steps = %#v, want none", steps)
|
|
}
|
|
control.markDOMReady(ctx)
|
|
if len(steps) != 0 {
|
|
t.Fatalf("DOM-ready steps = %#v, want none before frontend presentation", steps)
|
|
}
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
if got := strings.Join(steps, ","); got != "show,focus,ack-focus,post-ready" {
|
|
t.Fatalf("delayed focus sequence = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildFailedFocusAcknowledgementLeavesParentPendingForRetry(t *testing.T) {
|
|
manager := newHTTPTestManager(t)
|
|
manager.windows["ai-chat"] = &windowEntry{
|
|
info: WindowInfo{ID: "ai-chat", Kind: "ai-chat", Ready: true},
|
|
visibilityRevision: 7,
|
|
pendingFocusRevision: 7,
|
|
}
|
|
bridge := newBridge(ChildOptions{
|
|
ParentURL: "http://127.0.0.1:43119",
|
|
Token: manager.token,
|
|
ID: "ai-chat",
|
|
Kind: "ai-chat",
|
|
})
|
|
control := newControl(bridge)
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
control.showWindow = func(context.Context) {}
|
|
focuses := 0
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
control.markDOMReady(ctx)
|
|
if result := control.markFrontendReady(); !result.Success {
|
|
t.Fatalf("markFrontendReady result = %#v", result)
|
|
}
|
|
|
|
failAcknowledgement := true
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
if failAcknowledgement {
|
|
return nil, errors.New("temporary parent connection failure")
|
|
}
|
|
request.RemoteAddr = "127.0.0.1:51003"
|
|
recorder := httptest.NewRecorder()
|
|
manager.authenticatedHandler().ServeHTTP(recorder, request)
|
|
return recorder.Result(), nil
|
|
})
|
|
|
|
if result := control.FocusRevision(7); !result.Success {
|
|
t.Fatalf("FocusRevision with failed acknowledgement = %#v", result)
|
|
}
|
|
manager.mu.RLock()
|
|
pendingAfterFailure := manager.windows["ai-chat"].pendingFocusRevision
|
|
manager.mu.RUnlock()
|
|
if pendingAfterFailure != 7 {
|
|
t.Fatalf("pending focus after failed acknowledgement = %d, want 7", pendingAfterFailure)
|
|
}
|
|
|
|
failAcknowledgement = false
|
|
if result := control.FocusRevision(7); !result.Success {
|
|
t.Fatalf("FocusRevision retry result = %#v", result)
|
|
}
|
|
manager.mu.RLock()
|
|
pendingAfterRetry := manager.windows["ai-chat"].pendingFocusRevision
|
|
manager.mu.RUnlock()
|
|
if pendingAfterRetry != 0 || focuses != 2 {
|
|
t.Fatalf("retry state = pending %d focuses %d, want 0/2", pendingAfterRetry, focuses)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildIgnoresLateHideAfterNewerFocus(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
shows := 0
|
|
hides := 0
|
|
focuses := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
control.hideWindow = func(context.Context) { hides++ }
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
control.markDOMReady(ctx)
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
|
|
if result := control.Hide(1); !result.Success || result.VisibilityRevision != 1 {
|
|
t.Fatalf("Hide result = %#v", result)
|
|
}
|
|
if hides != 1 {
|
|
t.Fatalf("hides after revision 1 = %d, want 1", hides)
|
|
}
|
|
if result := control.FocusRevision(2); !result.Success || result.VisibilityRevision != 2 {
|
|
t.Fatalf("FocusRevision result = %#v", result)
|
|
}
|
|
if shows != 2 || focuses != 1 {
|
|
t.Fatalf("presentation after focus = show %d focus %d, want 2/1", shows, focuses)
|
|
}
|
|
|
|
lateHide := control.Hide(1)
|
|
if !lateHide.Success || lateHide.VisibilityRevision != 2 ||
|
|
!strings.Contains(lateHide.Message, "stale") {
|
|
t.Fatalf("late Hide result = %#v", lateHide)
|
|
}
|
|
if hides != 1 {
|
|
t.Fatalf("late hide reached native window: hides = %d, want 1", hides)
|
|
}
|
|
control.mu.RLock()
|
|
visible := control.visible
|
|
revision := control.visibilityRevision
|
|
control.mu.RUnlock()
|
|
if !visible || revision != 2 {
|
|
t.Fatalf("final visibility state = visible %v revision %d", visible, revision)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildSerializesNativeHideBeforeNewerFocus(t *testing.T) {
|
|
_, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
steps := make(chan string, 8)
|
|
hideStarted := make(chan struct{})
|
|
releaseHide := make(chan struct{})
|
|
control.showWindow = func(context.Context) { steps <- "show" }
|
|
control.hideWindow = func(context.Context) {
|
|
steps <- "hide-start"
|
|
close(hideStarted)
|
|
<-releaseHide
|
|
steps <- "hide-end"
|
|
}
|
|
control.focusWindow = func(context.Context) { steps <- "focus" }
|
|
control.markDOMReady(ctx)
|
|
if result := control.markFrontendReady(); !result.Success {
|
|
t.Fatalf("markFrontendReady result = %#v", result)
|
|
}
|
|
if step := <-steps; step != "show" {
|
|
t.Fatalf("initial presentation step = %q, want show", step)
|
|
}
|
|
|
|
hideDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
hideDone <- control.Hide(1)
|
|
}()
|
|
select {
|
|
case <-hideStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("native hide did not start")
|
|
}
|
|
|
|
focusCallStarted := make(chan struct{})
|
|
focusDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
close(focusCallStarted)
|
|
focusDone <- control.FocusRevision(2)
|
|
}()
|
|
<-focusCallStarted
|
|
select {
|
|
case result := <-focusDone:
|
|
close(releaseHide)
|
|
<-hideDone
|
|
t.Fatalf("newer focus completed before the older native hide: %#v", result)
|
|
case <-time.After(25 * time.Millisecond):
|
|
}
|
|
|
|
close(releaseHide)
|
|
if result := <-hideDone; !result.Success || result.VisibilityRevision != 1 {
|
|
t.Fatalf("Hide result = %#v", result)
|
|
}
|
|
if result := <-focusDone; !result.Success || result.VisibilityRevision != 2 {
|
|
t.Fatalf("FocusRevision result = %#v", result)
|
|
}
|
|
|
|
sequence := make([]string, 0, 4)
|
|
for len(sequence) < 4 {
|
|
select {
|
|
case step := <-steps:
|
|
sequence = append(sequence, step)
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("native visibility sequence stopped at %#v", sequence)
|
|
}
|
|
}
|
|
if got := strings.Join(sequence, ","); got != "hide-start,hide-end,show,focus" {
|
|
t.Fatalf("native visibility sequence = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildDoesNotBlockHideWhileAcknowledgingFocus(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
control.showWindow = func(context.Context) {}
|
|
control.focusWindow = func(context.Context) {}
|
|
hideCalled := make(chan struct{}, 1)
|
|
control.hideWindow = func(context.Context) { hideCalled <- struct{}{} }
|
|
control.markDOMReady(ctx)
|
|
if result := control.markFrontendReady(); !result.Success {
|
|
t.Fatalf("markFrontendReady result = %#v", result)
|
|
}
|
|
|
|
acknowledgementStarted := make(chan struct{})
|
|
releaseAcknowledgement := make(chan struct{})
|
|
bridge.client.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
if request.URL.Path == CommandStatePath {
|
|
close(acknowledgementStarted)
|
|
<-releaseAcknowledgement
|
|
}
|
|
return successfulVisibilityResponse(), nil
|
|
})
|
|
|
|
focusDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
focusDone <- control.FocusRevision(1)
|
|
}()
|
|
select {
|
|
case <-acknowledgementStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("focus acknowledgement did not start")
|
|
}
|
|
|
|
hideDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
hideDone <- control.Hide(2)
|
|
}()
|
|
select {
|
|
case result := <-hideDone:
|
|
if !result.Success || result.VisibilityRevision != 2 {
|
|
t.Fatalf("Hide result = %#v", result)
|
|
}
|
|
case <-time.After(25 * time.Millisecond):
|
|
close(releaseAcknowledgement)
|
|
<-focusDone
|
|
<-hideDone
|
|
t.Fatal("native hide waited for the focus acknowledgement network request")
|
|
}
|
|
select {
|
|
case <-hideCalled:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("native hide callback was not called")
|
|
}
|
|
|
|
close(releaseAcknowledgement)
|
|
if result := <-focusDone; !result.Success || result.VisibilityRevision != 1 {
|
|
t.Fatalf("FocusRevision result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildClosePreventsConcurrentHideFromCancellingExit(t *testing.T) {
|
|
_, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
quitStarted := make(chan struct{})
|
|
releaseQuit := make(chan struct{})
|
|
control.quit = func(context.Context) {
|
|
close(quitStarted)
|
|
<-releaseQuit
|
|
}
|
|
hideCalled := make(chan struct{}, 1)
|
|
control.hideWindow = func(context.Context) { hideCalled <- struct{}{} }
|
|
|
|
closeDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
closeDone <- control.Close()
|
|
}()
|
|
select {
|
|
case <-quitStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("native close did not reach quit")
|
|
}
|
|
if !control.closeGate.isAllowed() {
|
|
t.Fatal("close gate was not opened before quit")
|
|
}
|
|
|
|
hideDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
hideDone <- control.Hide(1)
|
|
}()
|
|
select {
|
|
case result := <-hideDone:
|
|
close(releaseQuit)
|
|
<-closeDone
|
|
t.Fatalf("hide completed while native close was in progress: %#v", result)
|
|
case <-time.After(25 * time.Millisecond):
|
|
}
|
|
|
|
close(releaseQuit)
|
|
if result := <-closeDone; !result.Success {
|
|
t.Fatalf("Close result = %#v", result)
|
|
}
|
|
if result := <-hideDone; result.Success || !strings.Contains(result.Message, "already committed") {
|
|
t.Fatalf("Hide result after committed close = %#v", result)
|
|
}
|
|
if !control.closeGate.isAllowed() {
|
|
t.Fatal("concurrent hide cancelled the committed close gate")
|
|
}
|
|
select {
|
|
case <-hideCalled:
|
|
t.Fatal("concurrent hide reached the native window after close committed")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildCloseFallbackPreventsConcurrentHideFromCancellingExit(t *testing.T) {
|
|
_, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
control.closeFallbackDelay = time.Millisecond
|
|
quitStarted := make(chan struct{})
|
|
releaseQuit := make(chan struct{})
|
|
control.quit = func(context.Context) {
|
|
close(quitStarted)
|
|
<-releaseQuit
|
|
}
|
|
hideCalled := make(chan struct{}, 1)
|
|
control.hideWindow = func(context.Context) { hideCalled <- struct{}{} }
|
|
|
|
control.scheduleCloseFallback(ctx)
|
|
select {
|
|
case <-quitStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("native close fallback did not reach quit")
|
|
}
|
|
if !control.closeGate.isAllowed() {
|
|
t.Fatal("close fallback did not open the gate before quit")
|
|
}
|
|
|
|
hideDone := make(chan OperationResult, 1)
|
|
go func() {
|
|
hideDone <- control.Hide(1)
|
|
}()
|
|
select {
|
|
case result := <-hideDone:
|
|
close(releaseQuit)
|
|
t.Fatalf("hide completed while native close fallback was in progress: %#v", result)
|
|
case <-time.After(25 * time.Millisecond):
|
|
}
|
|
|
|
close(releaseQuit)
|
|
if result := <-hideDone; result.Success || !strings.Contains(result.Message, "already committed") {
|
|
t.Fatalf("Hide result after fallback committed close = %#v", result)
|
|
}
|
|
if !control.closeGate.isAllowed() {
|
|
t.Fatal("concurrent hide cancelled the fallback close gate")
|
|
}
|
|
select {
|
|
case <-hideCalled:
|
|
t.Fatal("concurrent hide reached the native window after fallback close committed")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildStaleCloseFallbackKeepsNewerTimer(t *testing.T) {
|
|
_, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
control.closeFallbackDelay = time.Hour
|
|
quits := 0
|
|
control.quit = func(context.Context) { quits++ }
|
|
|
|
control.mu.Lock()
|
|
control.closeFallbackGeneration = 1
|
|
control.closeFallback = time.AfterFunc(time.Hour, func() {})
|
|
control.mu.Unlock()
|
|
|
|
control.visibilityOpMu.Lock()
|
|
staleFallbackDone := make(chan struct{})
|
|
go func() {
|
|
control.runCloseFallback(1, ctx)
|
|
close(staleFallbackDone)
|
|
}()
|
|
|
|
if result := control.CancelClose(); !result.Success {
|
|
control.visibilityOpMu.Unlock()
|
|
t.Fatalf("CancelClose result = %#v", result)
|
|
}
|
|
control.scheduleCloseFallback(ctx)
|
|
control.mu.RLock()
|
|
newGeneration := control.closeFallbackGeneration
|
|
newFallback := control.closeFallback
|
|
control.mu.RUnlock()
|
|
if newGeneration != 3 || newFallback == nil {
|
|
control.visibilityOpMu.Unlock()
|
|
t.Fatalf("new fallback state = generation %d timer %p, want 3/non-nil", newGeneration, newFallback)
|
|
}
|
|
|
|
control.visibilityOpMu.Unlock()
|
|
select {
|
|
case <-staleFallbackDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("stale close fallback did not complete")
|
|
}
|
|
control.mu.RLock()
|
|
retainedFallback := control.closeFallback
|
|
retainedGeneration := control.closeFallbackGeneration
|
|
closeCommitted := control.closeCommitted
|
|
control.mu.RUnlock()
|
|
if retainedFallback != newFallback || retainedGeneration != newGeneration {
|
|
t.Fatalf(
|
|
"fallback after stale callback = generation %d timer %p, want %d/%p",
|
|
retainedGeneration,
|
|
retainedFallback,
|
|
newGeneration,
|
|
newFallback,
|
|
)
|
|
}
|
|
if closeCommitted || control.closeGate.isAllowed() || quits != 0 {
|
|
t.Fatalf(
|
|
"stale fallback exit state = committed %v gate %v quits %d, want false/false/0",
|
|
closeCommitted,
|
|
control.closeGate.isAllowed(),
|
|
quits,
|
|
)
|
|
}
|
|
if result := control.CancelClose(); !result.Success {
|
|
t.Fatalf("final CancelClose result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildDoesNotPresentOrFocusAfterCloseCommitted(t *testing.T) {
|
|
_, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
shows := 0
|
|
focuses := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
control.quit = func(context.Context) {}
|
|
control.markDOMReady(ctx)
|
|
|
|
if result := control.Close(); !result.Success {
|
|
t.Fatalf("Close result = %#v", result)
|
|
}
|
|
if result := control.markFrontendReady(); result.Success {
|
|
t.Fatalf("markFrontendReady after close = %#v, want failure", result)
|
|
}
|
|
if result := control.Present(); result.Success {
|
|
t.Fatalf("Present after close = %#v, want failure", result)
|
|
}
|
|
if result := control.FocusRevision(1); result.Success {
|
|
t.Fatalf("FocusRevision after close = %#v, want failure", result)
|
|
}
|
|
if shows != 0 || focuses != 0 {
|
|
t.Fatalf("post-close presentation = show %d focus %d, want 0/0", shows, focuses)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildPresentsBeforePaintReadyWithoutShowingTwice(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
control.markDOMReady(ctx)
|
|
|
|
shows := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
if result := control.Present(); !result.Success {
|
|
t.Fatalf("Present result = %#v", result)
|
|
}
|
|
if shows != 1 {
|
|
t.Fatalf("present shows = %d, want 1", shows)
|
|
}
|
|
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
if shows != 1 {
|
|
t.Fatalf("post-ready shows = %d, want 1", shows)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildPresentConsumesQueuedFocus(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
shows := 0
|
|
focuses := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
control.markDOMReady(ctx)
|
|
|
|
if result := control.Focus(); !result.Success {
|
|
t.Fatalf("pre-present Focus result = %#v", result)
|
|
}
|
|
if shows != 0 || focuses != 0 {
|
|
t.Fatalf("queued focus presentation = show %d focus %d, want 0/0", shows, focuses)
|
|
}
|
|
if result := control.Present(); !result.Success {
|
|
t.Fatalf("Present result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 1 {
|
|
t.Fatalf("presented queued focus = show %d focus %d, want 1/1", shows, focuses)
|
|
}
|
|
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 1 {
|
|
t.Fatalf("post-ready presentation = show %d focus %d, want 1/1", shows, focuses)
|
|
}
|
|
if result := control.Focus(); !result.Success {
|
|
t.Fatalf("post-ready Focus result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 2 {
|
|
t.Fatalf("repeated focus presentation = show %d focus %d, want 1/2", shows, focuses)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildWaitsForDOMReadyEvenAfterFrontendHandshake(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
|
|
shows := 0
|
|
focuses := 0
|
|
posts := 0
|
|
control.showWindow = func(context.Context) { shows++ }
|
|
control.focusWindow = func(context.Context) { focuses++ }
|
|
bridge.client.Transport = roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
posts++
|
|
return successfulVisibilityResponse(), nil
|
|
})
|
|
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); result.Success {
|
|
t.Fatalf("pre-DOM ready Action result = %#v, want failure", result)
|
|
}
|
|
if result := control.Focus(); !result.Success {
|
|
t.Fatalf("pre-DOM Focus result = %#v", result)
|
|
}
|
|
if shows != 0 || focuses != 0 || posts != 0 {
|
|
t.Fatalf("pre-DOM state = show %d focus %d post %d, want 0/0/0", shows, focuses, posts)
|
|
}
|
|
|
|
control.markDOMReady(ctx)
|
|
if shows != 0 || focuses != 0 || posts != 0 {
|
|
t.Fatalf("DOM-ready state before retry = show %d focus %d post %d, want 0/0/0", shows, focuses, posts)
|
|
}
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("retried ready Action result = %#v", result)
|
|
}
|
|
if shows != 1 || focuses != 1 {
|
|
t.Fatalf("DOM-ready presentation = show %d focus %d, want 1/1", shows, focuses)
|
|
}
|
|
if posts != 1 {
|
|
t.Fatalf("parent ready posts = %d, want 1", posts)
|
|
}
|
|
}
|
|
|
|
func TestDetachedChildShowsBeforePostingParentReady(t *testing.T) {
|
|
bridge, control := newVisibilityTestChild()
|
|
ctx := context.Background()
|
|
InitializeControl(control, ctx)
|
|
control.markDOMReady(ctx)
|
|
|
|
steps := make([]string, 0, 2)
|
|
control.showWindow = func(context.Context) {
|
|
steps = append(steps, "show")
|
|
}
|
|
bridge.client.Transport = roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
steps = append(steps, "post-parent-ready")
|
|
return successfulVisibilityResponse(), nil
|
|
})
|
|
|
|
if result := bridge.Action("ready", map[string]any{"id": "window-1"}); !result.Success {
|
|
t.Fatalf("ready Action result = %#v", result)
|
|
}
|
|
if got := strings.Join(steps, ","); got != "show,post-parent-ready" {
|
|
t.Fatalf("ready sequence = %q, want show,post-parent-ready", got)
|
|
}
|
|
}
|
|
|
|
func newVisibilityTestChild() (*Bridge, *Control) {
|
|
bridge := newBridge(ChildOptions{
|
|
ParentURL: "http://127.0.0.1:43119",
|
|
Token: "test-token",
|
|
ID: "window-1",
|
|
Kind: "workbench",
|
|
})
|
|
bridge.client.Transport = roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return successfulVisibilityResponse(), nil
|
|
})
|
|
control := newControl(bridge)
|
|
bridge.setReadyHandler(control.markFrontendReady)
|
|
return bridge, control
|
|
}
|
|
|
|
func successfulVisibilityResponse() *http.Response {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(`{"success":true,"id":"window-1"}`)),
|
|
Header: make(http.Header),
|
|
}
|
|
}
|