Skip to content

Commit 008ece0

Browse files
authored
[chromium-headless]: fix headless browsers start with no pages (#104)
This PR address a bug with creating headless browsers with `--no-startup-window` flag causing breaking Target.createTarget CDP calls. [Ticket](https://linear.app/onkernel/issue/KERNEL-638/headless-browsers-start-with-no-pages-breaking-targetcreatetarget-cdp) ## Testing - run the `TestCDPTargetCreation` test. Test should pass with `initial_page_count=1` - re-add `--no-startup-window` in `wrapper.sh`, rebuild the docker image in `images/chromium-headless` and rerun the `TestCDPTargetCreation` test, the test should fail <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Removes the --no-startup-window flag so headless Chromium starts with a page, and adds an e2e test that verifies CDP target creation. > > - **Chromium headless image**: > - Remove `--no-startup-window` from default `CHROMIUM_FLAGS` in `images/chromium-headless/image/wrapper.sh` to ensure an initial page target exists. > - **E2E tests**: > - Add `TestCDPTargetCreation` in `server/e2e/e2e_chromium_test.go` to assert at least one `page` target via CDP. > - Add `listCDPTargets` helper to query `http://localhost:9223/json/list`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d5c4556. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 36c7f00 commit 008ece0

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

images/chromium-headless/image/wrapper.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then
7070
--no-first-run \
7171
--no-sandbox \
7272
--no-service-autorun \
73-
--no-startup-window \
7473
--ozone-platform=headless \
7574
--password-store=basic \
7675
--unsafely-disable-devtools-self-xss-warnings \

server/e2e/e2e_chromium_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,3 +810,77 @@ func TestPlaywrightExecuteAPI(t *testing.T) {
810810

811811
logger.Info("[test]", "result", "playwright execute API test passed")
812812
}
813+
814+
// TestCDPTargetCreation tests that headless browsers can create new targets via CDP.
815+
func TestCDPTargetCreation(t *testing.T) {
816+
image := headlessImage
817+
name := containerName + "-cdp-target"
818+
819+
logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelInfo}))
820+
baseCtx := logctx.AddToContext(context.Background(), logger)
821+
822+
if _, err := exec.LookPath("docker"); err != nil {
823+
require.NoError(t, err, "docker not available: %v", err)
824+
}
825+
826+
// Clean slate
827+
_ = stopContainer(baseCtx, name)
828+
829+
// Start container
830+
width, height := 1024, 768
831+
_, exitCh, err := runContainer(baseCtx, image, name, map[string]string{"WIDTH": strconv.Itoa(width), "HEIGHT": strconv.Itoa(height)})
832+
require.NoError(t, err, "failed to start container: %v", err)
833+
defer stopContainer(baseCtx, name)
834+
835+
ctx, cancel := context.WithTimeout(baseCtx, 2*time.Minute)
836+
defer cancel()
837+
838+
logger.Info("[test]", "action", "waiting for API")
839+
require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready")
840+
841+
// Wait for CDP endpoint to be ready (via the devtools proxy)
842+
logger.Info("[test]", "action", "waiting for CDP endpoint")
843+
require.NoError(t, waitTCP(ctx, "127.0.0.1:9222"), "CDP endpoint not ready")
844+
845+
// Wait for Chromium to be fully initialized by checking if CDP responds
846+
logger.Info("[test]", "action", "waiting for Chromium to be fully ready")
847+
targets, err := listCDPTargets(ctx)
848+
if err != nil {
849+
logger.Error("[test]", "error", err.Error())
850+
require.Fail(t, "failed to list CDP targets")
851+
}
852+
853+
// Use CDP HTTP API to list targets (avoids Playwright's implicit page creation)
854+
logger.Info("[test]", "action", "listing initial targets via CDP HTTP API")
855+
initialPageCount := 0
856+
for _, target := range targets {
857+
if targetType, ok := target["type"].(string); ok && targetType == "page" {
858+
initialPageCount++
859+
}
860+
}
861+
logger.Info("[test]", "initial_page_count", initialPageCount, "total_targets", len(targets))
862+
863+
// Headless browser should start with at least 1 page target.
864+
// If --no-startup-window is enabled, the browser will start with 0 pages,
865+
// which will cause Target.createTarget to fail with "no browser is open (-32000)".
866+
require.GreaterOrEqual(t, initialPageCount, 1,
867+
"headless browser should start with at least 1 page target (got %d). "+
868+
"This usually means --no-startup-window flag is enabled in wrapper.sh, "+
869+
"which causes browsers to start without any pages.", initialPageCount)
870+
}
871+
872+
// listCDPTargets lists all CDP targets via the HTTP API (inside the container)
873+
func listCDPTargets(ctx context.Context) ([]map[string]interface{}, error) {
874+
// Use the internal CDP HTTP endpoint (port 9223) inside the container
875+
stdout, err := execCombinedOutput(ctx, "curl", []string{"-s", "http://localhost:9223/json/list"})
876+
if err != nil {
877+
return nil, fmt.Errorf("curl failed: %w, output: %s", err, stdout)
878+
}
879+
880+
var targets []map[string]interface{}
881+
if err := json.Unmarshal([]byte(stdout), &targets); err != nil {
882+
return nil, fmt.Errorf("failed to parse targets JSON: %w, output: %s", err, stdout)
883+
}
884+
885+
return targets, nil
886+
}

0 commit comments

Comments
 (0)