@@ -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+ var initialTargets []map [string ]interface {}
848+ targets , err := listCDPTargets (ctx )
849+ if err == nil {
850+ initialTargets = 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 initialTargets {
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 (initialTargets ))
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