cdpfleet Docs GitHub Dashboard

Cases / #24 · 2026-10-06 · Easy

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

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

WhatWhy
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 clickThe 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 handlerTo 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, with the raw output). Set these environment variables first:

// npm install [email protected]
// 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();
}

What we got

EventWhat happenedHandled withPages openopener() is the main page
link with target=_blanknew page: https://example.com/ — "Example Domain"context.waitForEvent("page") + popup.waitForLoadState()2yes
window.open()new page: https://example.com/?popup — "Example Domain"context.waitForEvent("page") + popup.waitForLoadState()2yes
confirm() with no dialog handlerpage saw confirm() return falsenothing — auto-dismissed1—
alert()alert: "Saved!"dialog.accept()1—
confirm() with a handlerconfirm: "Delete 3 items?" → page saw truedialog.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