Files
Ant-Browser/backend/app_instance_start_test.go
T

365 lines
10 KiB
Go

package backend
import (
"ant-chrome/backend/internal/browser"
"ant-chrome/backend/internal/config"
"errors"
"net/http"
"os/exec"
"reflect"
"strings"
"testing"
"time"
)
func TestEnsureNewWindowLaunchArgAddsFlagOnce(t *testing.T) {
t.Parallel()
got := ensureNewWindowLaunchArg([]string{"--lang=en-US"})
want := []string{"--lang=en-US", "--new-window"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ensureNewWindowLaunchArg 结果错误: got=%v want=%v", got, want)
}
got = ensureNewWindowLaunchArg([]string{"--new-window", "--lang=en-US"})
want = []string{"--new-window", "--lang=en-US"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ensureNewWindowLaunchArg 不应重复追加: got=%v want=%v", got, want)
}
}
func TestShouldPreferVisibleWindowForStartWithParams(t *testing.T) {
t.Parallel()
tests := []struct {
name string
startURLs []string
want bool
}{
{
name: "nil start URLs",
startURLs: nil,
want: false,
},
{
name: "empty start URLs",
startURLs: []string{},
want: false,
},
{
name: "blank start URLs",
startURLs: []string{" ", "\t"},
want: false,
},
{
name: "valid start URL",
startURLs: []string{"https://finance.sina.com.cn"},
want: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := shouldPreferVisibleWindowForStartWithParams(tt.startURLs); got != tt.want {
t.Fatalf("shouldPreferVisibleWindowForStartWithParams() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsBrowserProfileLive(t *testing.T) {
t.Parallel()
ln := mustListenLoopback(t)
defer ln.Close()
profile := &BrowserProfile{
Running: true,
DebugPort: listenerPort(t, ln),
}
if !isBrowserProfileLive(profile, nil) {
t.Fatal("期望存活中的调试端口被识别为运行中实例")
}
if isBrowserProfileLive(&BrowserProfile{Running: true, DebugPort: 0}, nil) {
t.Fatal("debugPort=0 不应被识别为运行中实例")
}
}
func TestIsBrowserProfileLiveKeepsPendingDebugProcessAlive(t *testing.T) {
t.Parallel()
cmd := longLivedCommand(2 * time.Second)
if err := cmd.Start(); err != nil {
t.Fatalf("启动长生命周期测试进程失败: %v", err)
}
defer func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
_, _ = cmd.Process.Wait()
}
}()
profile := &BrowserProfile{
Running: true,
Pid: cmd.Process.Pid,
DebugPort: 0,
DebugReady: false,
}
if !isBrowserProfileLive(profile, cmd) {
t.Fatal("期望调试接口未就绪但进程仍存活时识别为运行中实例")
}
}
func TestWaitBrowserDebugPortStableKeepsListeningPort(t *testing.T) {
t.Parallel()
server := startDevToolsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/json/version":
_, _ = w.Write([]byte(`{"Browser":"Chrome/142.0","webSocketDebuggerUrl":"ws://127.0.0.1/devtools/browser"}`))
case "/json/list":
_, _ = w.Write([]byte(`[{"id":"page-1"}]`))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
if _, err := waitBrowserDebugPortStable(server.port, "", time.Second, 250*time.Millisecond, nil); err != nil {
t.Fatalf("waitBrowserDebugPortStable 返回错误: %v", err)
}
}
func TestWaitBrowserDebugPortStableRejectsEphemeralPort(t *testing.T) {
t.Parallel()
server := startDevToolsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/json/version":
_, _ = w.Write([]byte(`{"Browser":"Chrome/142.0","webSocketDebuggerUrl":"ws://127.0.0.1/devtools/browser"}`))
case "/json/list":
_, _ = w.Write([]byte(`[{"id":"page-1"}]`))
default:
http.NotFound(w, r)
}
}))
port := server.port
time.AfterFunc(120*time.Millisecond, func() {
_ = server.Close()
})
_, err := waitBrowserDebugPortStable(port, "", time.Second, 400*time.Millisecond, nil)
if err == nil {
t.Fatal("期望短暂就绪后关闭的端口被判定为失败")
}
}
func TestWaitBrowserDebugPortStableRejectsPlainTCPPort(t *testing.T) {
t.Parallel()
ln := mustListenLoopback(t)
defer ln.Close()
_, err := waitBrowserDebugPortStable(listenerPort(t, ln), "", 700*time.Millisecond, 250*time.Millisecond, nil)
if err == nil {
t.Fatal("期望仅开放 TCP 端口但无 DevTools HTTP 时启动失败")
}
}
func TestWaitBrowserDebugPortStableDiscoversPortFromStderr(t *testing.T) {
t.Parallel()
server := startDevToolsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/json/version":
_, _ = w.Write([]byte(`{"Browser":"Chrome/142.0","webSocketDebuggerUrl":"ws://127.0.0.1/devtools/browser"}`))
case "/json/list":
_, _ = w.Write([]byte(`[{"id":"page-1"}]`))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
cmd := stderrPortCommand(server.port, 2*time.Second)
monitor, err := newBrowserProcessMonitor(cmd)
if err != nil {
t.Fatalf("初始化浏览器进程监控失败: %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("启动测试命令失败: %v", err)
}
monitor.Start()
debugPort, err := waitBrowserDebugPortStable(0, "", 2*time.Second, 250*time.Millisecond, monitor)
if err != nil {
t.Fatalf("期望从 stderr 自动发现调试端口,实际错误: %v", err)
}
if debugPort != server.port {
t.Fatalf("期望发现调试端口 %d,实际=%d", server.port, debugPort)
}
}
func TestWaitBrowserDebugPortStableDiscoversPortFromDevToolsFile(t *testing.T) {
t.Parallel()
server := startDevToolsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/json/version":
_, _ = w.Write([]byte(`{"Browser":"Chrome/142.0","webSocketDebuggerUrl":"ws://127.0.0.1/devtools/browser"}`))
case "/json/list":
_, _ = w.Write([]byte(`[{"id":"page-1"}]`))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
userDataDir := t.TempDir()
writeDevToolsActivePortFile(t, userDataDir, server.port)
debugPort, err := waitBrowserDebugPortStable(0, userDataDir, time.Second, 250*time.Millisecond, nil)
if err != nil {
t.Fatalf("期望从 DevToolsActivePort 自动发现调试端口,实际错误: %v", err)
}
if debugPort != server.port {
t.Fatalf("期望发现调试端口 %d,实际=%d", server.port, debugPort)
}
}
func TestWaitBrowserDebugPortStableReturnsProcessExitDetail(t *testing.T) {
t.Parallel()
cmd := stderrFailingCommand("missing libEGL.dll")
monitor, err := newBrowserProcessMonitor(cmd)
if err != nil {
t.Fatalf("初始化浏览器进程监控失败: %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("启动测试命令失败: %v", err)
}
monitor.Start()
startedAt := time.Now()
_, err = waitBrowserDebugPortStable(0, "", 2*time.Second, 250*time.Millisecond, monitor)
if err == nil {
t.Fatal("期望启动前退出被判定为失败")
}
if time.Since(startedAt) >= 2*time.Second {
t.Fatalf("期望在超时前返回进程退出错误,实际耗时=%s", time.Since(startedAt))
}
var exitErr *browserStartupExitError
if !errors.As(err, &exitErr) {
t.Fatalf("期望 browserStartupExitError,实际=%T %v", err, err)
}
if !strings.Contains(exitErr.Detail(), "missing libEGL.dll") {
t.Fatalf("期望 stderr 细节被捕获,实际=%q", exitErr.Detail())
}
}
func TestWaitBrowserDebugPortStableAllowsDebugPortAfterLauncherExit(t *testing.T) {
t.Parallel()
port := freeLoopbackPort(t)
cmd := shortLivedCommand()
monitor, err := newBrowserProcessMonitor(cmd)
if err != nil {
t.Fatalf("初始化浏览器进程监控失败: %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("启动短命测试命令失败: %v", err)
}
monitor.Start()
serverReady := make(chan *devToolsTestServer, 1)
go func() {
time.Sleep(300 * time.Millisecond)
serverReady <- startDevToolsServerOnPort(t, port, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/json/version":
_, _ = w.Write([]byte(`{"Browser":"Chrome/142.0","webSocketDebuggerUrl":"ws://127.0.0.1/devtools/browser"}`))
case "/json/list":
_, _ = w.Write([]byte(`[{"id":"page-1"}]`))
default:
http.NotFound(w, r)
}
}))
}()
debugPort, err := waitBrowserDebugPortStable(port, "", 100*time.Millisecond, 250*time.Millisecond, monitor)
server := <-serverReady
defer server.Close()
if err != nil {
t.Fatalf("期望启动器退出后仍能等待到调试端口就绪,实际错误: %v", err)
}
if debugPort != port {
t.Fatalf("期望发现调试端口 %d,实际=%d", port, debugPort)
}
}
func TestWaitBrowserProcessKeepsRunningWhileDebugPortAlive(t *testing.T) {
ln := mustListenLoopback(t)
port := listenerPort(t, ln)
app := NewApp("")
app.browserMgr = browser.NewManager(config.DefaultConfig(), "")
app.browserMgr.Profiles = map[string]*BrowserProfile{
"profile-detached": {
ProfileId: "profile-detached",
ProfileName: "Detached Browser",
Running: true,
DebugPort: port,
DebugReady: true,
Pid: 12345,
},
}
app.browserMgr.BrowserProcesses = make(map[string]*exec.Cmd)
cmd := shortLivedCommand()
monitor, err := newBrowserProcessMonitor(cmd)
if err != nil {
t.Fatalf("初始化测试进程监控失败: %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("启动短命测试进程失败: %v", err)
}
monitor.Start()
app.browserMgr.BrowserProcesses["profile-detached"] = cmd
done := make(chan struct{})
go func() {
app.waitBrowserProcess("profile-detached", monitor)
close(done)
}()
waitForCondition(t, 3*time.Second, func() bool {
app.browserMgr.Mutex.Lock()
defer app.browserMgr.Mutex.Unlock()
profile := app.browserMgr.Profiles["profile-detached"]
_, tracked := app.browserMgr.BrowserProcesses["profile-detached"]
return profile != nil && profile.Running && !tracked
})
_ = ln.Close()
waitForCondition(t, 4*time.Second, func() bool {
app.browserMgr.Mutex.Lock()
defer app.browserMgr.Mutex.Unlock()
profile := app.browserMgr.Profiles["profile-detached"]
return profile != nil && !profile.Running && profile.DebugPort == 0 && profile.Pid == 0
})
select {
case <-done:
case <-time.After(4 * time.Second):
t.Fatal("waitBrowserProcess 未在调试端口关闭后结束")
}
}