Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion images/chromium-headless/image/wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then
--no-first-run \
--no-sandbox \
--no-service-autorun \
--no-startup-window \
--ozone-platform=headless \
--password-store=basic \
--unsafely-disable-devtools-self-xss-warnings \
Expand Down
74 changes: 74 additions & 0 deletions server/e2e/e2e_chromium_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -810,3 +810,77 @@ func TestPlaywrightExecuteAPI(t *testing.T) {

logger.Info("[test]", "result", "playwright execute API test passed")
}

// TestCDPTargetCreation tests that headless browsers can create new targets via CDP.
func TestCDPTargetCreation(t *testing.T) {
image := headlessImage
name := containerName + "-cdp-target"

logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelInfo}))
baseCtx := logctx.AddToContext(context.Background(), logger)

if _, err := exec.LookPath("docker"); err != nil {
require.NoError(t, err, "docker not available: %v", err)
}

// Clean slate
_ = stopContainer(baseCtx, name)

// Start container
width, height := 1024, 768
_, exitCh, err := runContainer(baseCtx, image, name, map[string]string{"WIDTH": strconv.Itoa(width), "HEIGHT": strconv.Itoa(height)})
require.NoError(t, err, "failed to start container: %v", err)
defer stopContainer(baseCtx, name)

ctx, cancel := context.WithTimeout(baseCtx, 2*time.Minute)
defer cancel()

logger.Info("[test]", "action", "waiting for API")
require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready")

// Wait for CDP endpoint to be ready (via the devtools proxy)
logger.Info("[test]", "action", "waiting for CDP endpoint")
require.NoError(t, waitTCP(ctx, "127.0.0.1:9222"), "CDP endpoint not ready")

// Wait for Chromium to be fully initialized by checking if CDP responds
logger.Info("[test]", "action", "waiting for Chromium to be fully ready")
targets, err := listCDPTargets(ctx)
if err != nil {
logger.Error("[test]", "error", err.Error())
require.Fail(t, "failed to list CDP targets")
}

// Use CDP HTTP API to list targets (avoids Playwright's implicit page creation)
logger.Info("[test]", "action", "listing initial targets via CDP HTTP API")
initialPageCount := 0
for _, target := range targets {
if targetType, ok := target["type"].(string); ok && targetType == "page" {
initialPageCount++
}
}
logger.Info("[test]", "initial_page_count", initialPageCount, "total_targets", len(targets))

// Headless browser should start with at least 1 page target.
// If --no-startup-window is enabled, the browser will start with 0 pages,
// which will cause Target.createTarget to fail with "no browser is open (-32000)".
require.GreaterOrEqual(t, initialPageCount, 1,
"headless browser should start with at least 1 page target (got %d). "+
"This usually means --no-startup-window flag is enabled in wrapper.sh, "+
"which causes browsers to start without any pages.", initialPageCount)
}

// listCDPTargets lists all CDP targets via the HTTP API (inside the container)
func listCDPTargets(ctx context.Context) ([]map[string]interface{}, error) {
// Use the internal CDP HTTP endpoint (port 9223) inside the container
stdout, err := execCombinedOutput(ctx, "curl", []string{"-s", "http://localhost:9223/json/list"})
if err != nil {
return nil, fmt.Errorf("curl failed: %w, output: %s", err, stdout)
}

var targets []map[string]interface{}
if err := json.Unmarshal([]byte(stdout), &targets); err != nil {
return nil, fmt.Errorf("failed to parse targets JSON: %w, output: %s", err, stdout)
}

return targets, nil
}
Loading