diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go index ecdd8a6..0167ff3 100644 --- a/backend/controller/remoteBrowser.go +++ b/backend/controller/remoteBrowser.go @@ -370,6 +370,10 @@ func (m *RemoteBrowserController) RunByID(g *gin.Context) { } }() + // NoKeepAlive=true so keepAlive() in test scripts returns immediately instead + // of parking the browser — closing the test panel must always stop cleanly. + runner.NoKeepAlive = true + // Run the script in a goroutine; Events channel is closed when done. go runner.Run(ctx) //nolint:errcheck diff --git a/backend/remotebrowser/runner.go b/backend/remotebrowser/runner.go index 819fe55..425af31 100644 --- a/backend/remotebrowser/runner.go +++ b/backend/remotebrowser/runner.go @@ -72,6 +72,10 @@ type Runner struct { // ExecPath is the server-configured Chrome binary path (from config.json, // not user-supplied). Empty = Rod auto-download. ExecPath string + // NoKeepAlive disables the keepAlive() parking behaviour. When true, + // keepAlive() returns immediately so the script exits normally. Used by + // the test runner to prevent leaking browsers when the test panel closes. + NoKeepAlive bool Events chan RunEvent // server → client Incoming chan IncomingMsg // client → script (victim events / test injections) // BrowserCh receives the *rod.Page as soon as newSession() spawns the browser. @@ -386,6 +390,10 @@ func (r *Runner) Run(ctx context.Context) error { // is available for streaming. It blocks until the runner context // is cancelled (e.g. the admin closes the live session). session.Set("keepAlive", func(call goja.FunctionCall) goja.Value { + if r.NoKeepAlive { + emitter.log("[session] keepAlive skipped (test mode)") + return goja.Undefined() + } emitter.log("[session] keeping alive for remote takeover") select { case r.LiveCh <- page: