[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #24 · 2026-10-06 · Example · Easy · for [testing](https://cdpfleet.com/cross-browser-testing), [ai agents](https://cdpfleet.com/ai-agents)

# New tabs, window.open and the three blocking dialogs: everything that interrupts a script

A page that opens tabs and throws alert, confirm and prompt at you, handled on a remote browser: catching the new page, answering dialogs, and what happens when you don't.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

Run on production on 2026-10-06: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

Real sites open things: a link with `target="_blank"`, a login that pops a window, a "Delete 3 items?" confirm, a prompt. Each one changes what your script's `page` object refers to or blocks it until someone answers. On a local browser you'd see it happen; on a remote browser you see a timeout. The handling isn't hard, but it has to be in place \*before\* the click, and the default behaviour when nothing is in place isn't what most people assume.

## What we used, and why

| What | Why |
| --- | --- |
| `page.setContent(HTML)` | The test page is served by the browser itself: a new-tab link, a `window.open` button, and buttons that call `alert`, `confirm` and `prompt`. No third-party site to flake. |
| `context.waitForEvent("page")` started before the click | The new page can appear before the click resolves; waiting afterwards can miss it. |
| `popup.waitForLoadState()`, `popup.opener()` | A popup starts empty; wait for it to load before reading `url()` or `title()`. `opener()` ties it back to the page that opened it. |
| `page.on("dialog", …)` with `accept()`, `accept(text)` | You answer the dialog; the page gets `true`, your text, or `false`/`null` if you dismiss. |
| Clicking `confirm` once with no handler | To show the default: Playwright dismisses dialogs it has no handler for, so `confirm()` returns `false`. |

## How it works

1.  Load the test page into a fresh context.
2.  Click the `target="_blank"` link and the `window.open` button; each time, capture the new page via the context event, wait for it to load, read URL and title, check `opener()`, close it.
3.  Click `confirm` with no dialog handler and read what the page got back.
4.  Install a dialog handler that accepts everything (and types a name into the prompt); click `alert`, `confirm`, `prompt`; read what the page got back.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/24-popups-and-dialogs), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// A page with everything that interrupts a script: a new-tab link, window.open, and the
// three blocking dialogs. Served from the browser itself, so the case needs no third party.
const HTML = `<!doctype html><title>Interruptions</title>
<a id="blank" href="https://example.com/" target="_blank">open in a new tab</a>
<button id="open" onclick="window.open('https://example.com/?popup', 'pop', 'width=480,height=320')">window.open</button>
<button id="alert" onclick="alert('Saved!')">alert</button>
<button id="confirm" onclick="document.body.dataset.confirm = String(confirm('Delete 3 items?'))">confirm</button>
<button id="prompt" onclick="document.body.dataset.prompt = String(prompt('Your name?', 'anonymous'))">prompt</button>`;

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

try {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.setContent(HTML);
  const out = [];

  // New pages: listen on the context BEFORE the click, then wait for the popup to load.
  for (const [label, selector] of [['link with target=_blank', '#blank'], ['window.open()', '#open']]) {
    const [popup] = await Promise.all([context.waitForEvent('page'), page.click(selector)]);
    await popup.waitForLoadState('load', { timeout: 60000 });
    out.push({ event: label, what_happened: `new page: ${popup.url()} — "${await popup.title()}"`, handled_with: 'context.waitForEvent("page") + popup.waitForLoadState()', pages_open: context.pages().length, opener_is_main_page: (await popup.opener()) === page });
    await popup.close();
  }

  // Dialogs: without a handler Playwright dismisses them (confirm → false, prompt → null).
  await page.click('#confirm');
  out.push({ event: 'confirm() with no dialog handler', what_happened: `page saw confirm() return ${await page.evaluate(() => document.body.dataset.confirm)}`, handled_with: 'nothing — auto-dismissed', pages_open: context.pages().length, opener_is_main_page: null });

  // With a handler you decide: accept, dismiss, or type an answer.
  const seen = [];
  page.on('dialog', async (d) => {
    seen.push(`${d.type()}: "${d.message()}"`);
    if (d.type() === 'prompt') await d.accept('Ada Lovelace');
    else await d.accept();
  });
  await page.click('#alert');
  await page.click('#confirm');
  await page.click('#prompt');
  const results = await page.evaluate(() => ({ confirm: document.body.dataset.confirm, prompt: document.body.dataset.prompt }));
  out.push({ event: 'alert()', what_happened: seen[0], handled_with: 'dialog.accept()', pages_open: context.pages().length, opener_is_main_page: null });
  out.push({ event: 'confirm() with a handler', what_happened: `${seen[1]} → page saw ${results.confirm}`, handled_with: 'dialog.accept()', pages_open: context.pages().length, opener_is_main_page: null });
  out.push({ event: 'prompt()', what_happened: `${seen[2]} → page saw "${results.prompt}"`, handled_with: 'dialog.accept("Ada Lovelace")', pages_open: context.pages().length, opener_is_main_page: null });

  console.log(JSON.stringify(out, null, 2));
} finally {
  await browser.close();
}
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]

# A page with everything that interrupts a script: a new-tab link, window.open, and the
# three blocking dialogs. Served from the browser itself, so the case needs no third party.
HTML = """<!doctype html><title>Interruptions</title>
<a id="blank" href="https://example.com/" target="_blank">open in a new tab</a>
<button id="open" onclick="window.open('https://example.com/?popup', 'pop', 'width=480,height=320')">window.open</button>
<button id="alert" onclick="alert('Saved!')">alert</button>
<button id="confirm" onclick="document.body.dataset.confirm = String(confirm('Delete 3 items?'))">confirm</button>
<button id="prompt" onclick="document.body.dataset.prompt = String(prompt('Your name?', 'anonymous'))">prompt</button>"""

res = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY}, timeout=60,
                    json={"proxy": os.environ["PROXY_URL"], "headless": "new"})
if not res.ok:
    raise SystemExit(f"launch {res.status_code} {res.text}")
session = res.json()

with sync_playwright() as p:
    browser = p.chromium.connect(session["wsUrl"], headers={"x-api-key": KEY})
    try:
        context = browser.new_context()
        page = context.new_page()
        page.set_content(HTML)
        out = []

        # New pages: listen on the context BEFORE the click, then wait for the popup to load.
        for label, selector in [("link with target=_blank", "#blank"), ("window.open()", "#open")]:
            with context.expect_page() as popup_info:
                page.click(selector)
            popup = popup_info.value
            popup.wait_for_load_state("load", timeout=60000)
            out.append({"event": label, "what_happened": f'new page: {popup.url} — "{popup.title()}"',
                        "handled_with": 'context.waitForEvent("page") + popup.waitForLoadState()',
                        "pages_open": len(context.pages), "opener_is_main_page": popup.opener() is page})
            popup.close()

        # Dialogs: without a handler Playwright dismisses them (confirm → false, prompt → null).
        page.click("#confirm")
        out.append({"event": "confirm() with no dialog handler",
                    "what_happened": f"page saw confirm() return {page.evaluate('() => document.body.dataset.confirm')}",
                    "handled_with": "nothing — auto-dismissed", "pages_open": len(context.pages), "opener_is_main_page": None})

        # With a handler you decide: accept, dismiss, or type an answer.
        seen = []

        def on_dialog(d):
            seen.append(f'{d.type}: "{d.message}"')
            if d.type == "prompt":
                d.accept("Ada Lovelace")
            else:
                d.accept()

        page.on("dialog", on_dialog)
        page.click("#alert")
        page.click("#confirm")
        page.click("#prompt")
        results = page.evaluate("() => ({ confirm: document.body.dataset.confirm, prompt: document.body.dataset.prompt })")
        out.append({"event": "alert()", "what_happened": seen[0], "handled_with": "dialog.accept()",
                    "pages_open": len(context.pages), "opener_is_main_page": None})
        out.append({"event": "confirm() with a handler", "what_happened": f"{seen[1]} → page saw {results['confirm']}",
                    "handled_with": "dialog.accept()", "pages_open": len(context.pages), "opener_is_main_page": None})
        out.append({"event": "prompt()", "what_happened": f"{seen[2]} → page saw \"{results['prompt']}\"",
                    "handled_with": 'dialog.accept("Ada Lovelace")', "pages_open": len(context.pages), "opener_is_main_page": None})

        print(json.dumps(out, indent=2, ensure_ascii=False))
    finally:
        browser.close()
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import java.net.URI;
import java.net.http.*;
import java.util.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create();

  // A page with everything that interrupts a script: a new-tab link, window.open, and the
  // three blocking dialogs. Served from the browser itself, so the case needs no third party.
  static final String HTML = "<!doctype html><title>Interruptions</title>\n"
      + "<a id=\"blank\" href=\"https://example.com/\" target=\"_blank\">open in a new tab</a>\n"
      + "<button id=\"open\" onclick=\"window.open('https://example.com/?popup', 'pop', 'width=480,height=320')\">window.open</button>\n"
      + "<button id=\"alert\" onclick=\"alert('Saved!')\">alert</button>\n"
      + "<button id=\"confirm\" onclick=\"document.body.dataset.confirm = String(confirm('Delete 3 items?'))\">confirm</button>\n"
      + "<button id=\"prompt\" onclick=\"document.body.dataset.prompt = String(prompt('Your name?', 'anonymous'))\">prompt</button>";

  static JsonObject row(String event, String what, String handledWith, int pagesOpen, Boolean opener) {
    JsonObject row = new JsonObject();
    row.addProperty("event", event);
    row.addProperty("what_happened", what);
    row.addProperty("handled_with", handledWith);
    row.addProperty("pages_open", pagesOpen);
    if (opener == null) row.add("opener_is_main_page", JsonNull.INSTANCE); else row.addProperty("opener_is_main_page", opener);
    return row;
  }

  public static void main(String[] args) throws Exception {
    String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL")) + ", \"headless\": \"new\"}";
    HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch " + res.statusCode() + " " + res.body());
    JsonObject session = JsonParser.parseString(res.body()).getAsJsonObject();

    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(session.get("wsUrl").getAsString(),
          new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        BrowserContext context = browser.newContext();
        Page page = context.newPage();
        page.setContent(HTML);
        JsonArray out = new JsonArray();

        // New pages: listen on the context BEFORE the click, then wait for the popup to load.
        for (String[] target : new String[][] {{"link with target=_blank", "#blank"}, {"window.open()", "#open"}}) {
          Page popup = context.waitForPage(() -> page.click(target[1]));
          popup.waitForLoadState(LoadState.LOAD, new Page.WaitForLoadStateOptions().setTimeout(60000));
          out.add(row(target[0], "new page: " + popup.url() + " — \"" + popup.title() + "\"",
              "context.waitForEvent(\"page\") + popup.waitForLoadState()", context.pages().size(), popup.opener() == page));
          popup.close();
        }

        // Dialogs: without a handler Playwright dismisses them (confirm → false, prompt → null).
        page.click("#confirm");
        out.add(row("confirm() with no dialog handler", "page saw confirm() return " + page.evaluate("() => document.body.dataset.confirm"),
            "nothing — auto-dismissed", context.pages().size(), null));

        // With a handler you decide: accept, dismiss, or type an answer.
        List<String> seen = new ArrayList<>();
        page.onDialog(d -> {
          seen.add(d.type() + ": \"" + d.message() + "\"");
          if (d.type().equals("prompt")) d.accept("Ada Lovelace"); else d.accept();
        });
        page.click("#alert");
        page.click("#confirm");
        page.click("#prompt");
        JsonObject results = GSON.toJsonTree(page.evaluate("() => ({ confirm: document.body.dataset.confirm, prompt: document.body.dataset.prompt })")).getAsJsonObject();
        out.add(row("alert()", seen.get(0), "dialog.accept()", context.pages().size(), null));
        out.add(row("confirm() with a handler", seen.get(1) + " → page saw " + results.get("confirm").getAsString(), "dialog.accept()", context.pages().size(), null));
        out.add(row("prompt()", seen.get(2) + " → page saw \"" + results.get("prompt").getAsString() + "\"", "dialog.accept(\"Ada Lovelace\")", context.pages().size(), null));

        System.out.println(GSON.toJson(out));
      } finally {
        browser.close();
      }
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;

// A page with everything that interrupts a script: a new-tab link, window.open, and the
// three blocking dialogs. Served from the browser itself, so the case needs no third party.
const string Html = @"<!doctype html><title>Interruptions</title>
<a id=""blank"" href=""https://example.com/"" target=""_blank"">open in a new tab</a>
<button id=""open"" onclick=""window.open('https://example.com/?popup', 'pop', 'width=480,height=320')"">window.open</button>
<button id=""alert"" onclick=""alert('Saved!')"">alert</button>
<button id=""confirm"" onclick=""document.body.dataset.confirm = String(confirm('Delete 3 items?'))"">confirm</button>
<button id=""prompt"" onclick=""document.body.dataset.prompt = String(prompt('Your name?', 'anonymous'))"">prompt</button>";

static JsonObject Row(string ev, string what, string handledWith, int pagesOpen, bool? opener) => new()
{
    ["event"] = ev,
    ["what_happened"] = what,
    ["handled_with"] = handledWith,
    ["pages_open"] = pagesOpen,
    ["opener_is_main_page"] = opener is null ? null : JsonValue.Create(opener.Value),
};

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
    new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var session = await res.Content.ReadFromJsonAsync<JsonElement>();

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(session.GetProperty("wsUrl").GetString()!,
    new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
    var context = await browser.NewContextAsync();
    var page = await context.NewPageAsync();
    await page.SetContentAsync(Html);
    var output = new JsonArray();

    // New pages: listen on the context BEFORE the click, then wait for the popup to load.
    foreach (var (label, selector) in new[] { ("link with target=_blank", "#blank"), ("window.open()", "#open") })
    {
        var popup = await context.RunAndWaitForPageAsync(async () => await page.ClickAsync(selector));
        await popup.WaitForLoadStateAsync(LoadState.Load, new() { Timeout = 60000 });
        output.Add(Row(label, $"new page: {popup.Url} — \"{await popup.TitleAsync()}\"",
            "context.waitForEvent(\"page\") + popup.waitForLoadState()", context.Pages.Count, await popup.OpenerAsync() == page));
        await popup.CloseAsync();
    }

    // Dialogs: without a handler Playwright dismisses them (confirm → false, prompt → null).
    await page.ClickAsync("#confirm");
    output.Add(Row("confirm() with no dialog handler", $"page saw confirm() return {await page.EvaluateAsync<string>("() => document.body.dataset.confirm")}",
        "nothing — auto-dismissed", context.Pages.Count, null));

    // With a handler you decide: accept, dismiss, or type an answer.
    var seen = new List<string>();
    page.Dialog += async (_, d) =>
    {
        seen.Add($"{d.Type}: \"{d.Message}\"");
        if (d.Type == "prompt") await d.AcceptAsync("Ada Lovelace"); else await d.AcceptAsync();
    };
    await page.ClickAsync("#alert");
    await page.ClickAsync("#confirm");
    await page.ClickAsync("#prompt");
    var results = await page.EvaluateAsync<JsonElement>("() => ({ confirm: document.body.dataset.confirm, prompt: document.body.dataset.prompt })");
    output.Add(Row("alert()", seen[0], "dialog.accept()", context.Pages.Count, null));
    output.Add(Row("confirm() with a handler", $"{seen[1]} → page saw {results.GetProperty("confirm").GetString()}", "dialog.accept()", context.Pages.Count, null));
    output.Add(Row("prompt()", $"{seen[2]} → page saw \"{results.GetProperty("prompt").GetString()}\"", "dialog.accept(\"Ada Lovelace\")", context.Pages.Count, null));

    Console.WriteLine(output.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
    await browser.CloseAsync();
}
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

// A page with everything that interrupts a script: a new-tab link, window.open, and the
// three blocking dialogs. Served from the browser itself, so the case needs no third party.
const html = "<!doctype html><title>Interruptions</title>\n" +
	"<a id=\"blank\" href=\"https://example.com/\" target=\"_blank\">open in a new tab</a>\n" +
	"<button id=\"open\" onclick=\"window.open('https://example.com/?popup', 'pop', 'width=480,height=320')\">window.open</button>\n" +
	"<button id=\"alert\" onclick=\"alert('Saved!')\">alert</button>\n" +
	"<button id=\"confirm\" onclick=\"document.body.dataset.confirm = String(confirm('Delete 3 items?'))\">confirm</button>\n" +
	"<button id=\"prompt\" onclick=\"document.body.dataset.prompt = String(prompt('Your name?', 'anonymous'))\">prompt</button>"

type row struct {
	Event            string `json:"event"`
	WhatHappened     string `json:"what_happened"`
	HandledWith      string `json:"handled_with"`
	PagesOpen        int    `json:"pages_open"`
	OpenerIsMainPage *bool  `json:"opener_is_main_page"`
}

func must[T any](v T, err error) T {
	if err != nil {
		log.Fatal(err)
	}
	return v
}

func main() {
	body, _ := json.Marshal(map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": "new"})
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res := must(http.DefaultClient.Do(req))
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		log.Fatalf("launch %s %s", res.Status, msg)
	}
	var session map[string]any
	must(0, json.NewDecoder(res.Body).Decode(&session))

	pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
	defer pw.Stop()
	browser := must(pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}}))
	defer browser.Close()

	context := must(browser.NewContext())
	page := must(context.NewPage())
	must(0, page.SetContent(html))
	out := []row{}

	// New pages: listen on the context BEFORE the click, then wait for the popup to load.
	for _, target := range [][2]string{{"link with target=_blank", "#blank"}, {"window.open()", "#open"}} {
		selector := target[1]
		popup := must(context.ExpectPage(func() error { return page.Click(selector) }))
		must(0, popup.WaitForLoadState(playwright.PageWaitForLoadStateOptions{State: playwright.LoadStateLoad, Timeout: playwright.Float(60000)}))
		opener := must(popup.Opener()) == page
		out = append(out, row{target[0], "new page: " + popup.URL() + " — \"" + must(popup.Title()) + "\"",
			"context.waitForEvent(\"page\") + popup.waitForLoadState()", len(context.Pages()), &opener})
		must(0, popup.Close())
	}

	// Dialogs: without a handler Playwright dismisses them (confirm → false, prompt → null).
	must(0, page.Click("#confirm"))
	out = append(out, row{"confirm() with no dialog handler", fmt.Sprintf("page saw confirm() return %v", must(page.Evaluate("() => document.body.dataset.confirm"))),
		"nothing — auto-dismissed", len(context.Pages()), nil})

	// With a handler you decide: accept, dismiss, or type an answer.
	seen := []string{}
	page.OnDialog(func(d playwright.Dialog) {
		seen = append(seen, d.Type()+": \""+d.Message()+"\"")
		if d.Type() == "prompt" {
			d.Accept("Ada Lovelace")
		} else {
			d.Accept()
		}
	})
	must(0, page.Click("#alert"))
	must(0, page.Click("#confirm"))
	must(0, page.Click("#prompt"))
	results := must(page.Evaluate("() => ({ confirm: document.body.dataset.confirm, prompt: document.body.dataset.prompt })")).(map[string]any)
	out = append(out, row{"alert()", seen[0], "dialog.accept()", len(context.Pages()), nil})
	out = append(out, row{"confirm() with a handler", fmt.Sprintf("%s → page saw %v", seen[1], results["confirm"]), "dialog.accept()", len(context.Pages()), nil})
	out = append(out, row{"prompt()", fmt.Sprintf("%s → page saw \"%v\"", seen[2], results["prompt"]), "dialog.accept(\"Ada Lovelace\")", len(context.Pages()), nil})

	text, _ := json.MarshalIndent(out, "", "  ")
	fmt.Println(string(text))
}
```

## What we got

| Event | What happened | Handled with | Pages open | opener() is the main page |
| --- | --- | --- | --- | --- |
| link with target=\_blank | new page: https://example.com/ — "Example Domain" | context.waitForEvent("page") + popup.waitForLoadState() | 2 | yes |
| window.open() | new page: https://example.com/?popup — "Example Domain" | context.waitForEvent("page") + popup.waitForLoadState() | 2 | yes |
| confirm() with no dialog handler | page saw confirm() return false | nothing — auto-dismissed | 1 | — |
| alert() | alert: "Saved!" | dialog.accept() | 1 | — |
| confirm() with a handler | confirm: "Delete 3 items?" → page saw true | dialog.accept() | 1 | — |
| prompt() | prompt: "Your name?" → page saw "Ada Lovelace" | dialog.accept("Ada Lovelace") | 1 | — |

From the Node.js run on 2026-10-06. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
[
  {
    "event": "link with target=_blank",
    "what_happened": "new page: https://example.com/ — \"Example Domain\"",
    "handled_with": "context.waitForEvent(\"page\") + popup.waitForLoadState()",
    "pages_open": 2,
    "opener_is_main_page": true
  },
  {
    "event": "window.open()",
    "what_happened": "new page: https://example.com/?popup — \"Example Domain\"",
    "handled_with": "context.waitForEvent(\"page\") + popup.waitForLoadState()",
    "pages_open": 2,
    "opener_is_main_page": true
  },
  {
    "event": "confirm() with no dialog handler",
    "what_happened": "page saw confirm() return false",
    "handled_with": "nothing — auto-dismissed",
    "pages_open": 1,
    "opener_is_main_page": null
  },
  {
    "event": "alert()",
    "what_happened": "alert: \"Saved!\"",
    "handled_with": "dialog.accept()",
    "pages_open": 1,
    "opener_is_main_page": null
  },
  {
    "event": "confirm() with a handler",
    "what_happened": "confirm: \"Delete 3 items?\" → page saw true",
    "handled_with": "dialog.accept()",
    "pages_open": 1,
    "opener_is_main_page": null
  },
  {
    "event": "prompt()",
    "what_happened": "prompt: \"Your name?\" → page saw \"Ada Lovelace\"",
    "handled_with": "dialog.accept(\"Ada Lovelace\")",
    "pages_open": 1,
    "opener_is_main_page": null
  }
]
```

## Takeaways

-   **Both kinds of new page arrive the same way** — `context.waitForEvent("page")` catches a `target="_blank"` link and a `window.open` call alike, `opener()` points back at the main page, and after `waitForLoadState` the popup is a normal `page` you can drive. Both counted as a second page in the context, not a second session: tabs are free, threads are per session.
-   **Unhandled dialogs are dismissed, not accepted:** with no handler, `confirm("Delete 3 items?")` returned `false` to the page — the script continued, and the site silently took the "cancel" path. That is the opposite of the usual assumption.
-   **With a handler you choose the answer**: `alert` acknowledged, `confirm` → `true`, `prompt` → `"Ada Lovelace"`. The handler must exist before the click, and it must call `accept()` or `dismiss()`, or the page stays blocked.
-   **Nothing here depends on the browser being remote** — the events travel over the same connection as everything else; the only remote-specific cost is that a missed popup looks like a timeout rather than a visible window.

---
Source: https://cdpfleet.com/docs/cases/popups-and-dialogs · Markdown: https://cdpfleet.com/docs/cases/popups-and-dialogs.md · All docs for AI tools: https://cdpfleet.com/llms.txt
