# How Ego‑Browser Handles JavaScript Dialogs (alert, confirm, prompt)

> Discover how Ego-Browser handles JavaScript dialogs like alert, confirm, and prompt. Learn about its interception via Chrome DevTools Protocol and session-scoped storage to keep your tests running smoothly.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-07-26

---

**Ego‑Browser intercepts JavaScript dialogs via Chrome DevTools Protocol events and stores them in a session-scoped Map, exposing a `pendingDialog()` accessor that helpers like `pageInfo()` and `screenshot()` use to adapt their behavior when alerts, confirms, or prompts block the page.**

The `citrolabs/ego-lite` repository provides a headless browser automation framework that must gracefully handle modal JavaScript dialogs. Instead of letting `alert()`, `confirm()`, or `prompt()` calls hang indefinitely, ego-browser tracks these events through the Chrome DevTools Protocol (CDP) and exposes detection mechanisms that downstream consumers use to avoid interaction deadlocks.

## Capturing Dialog Events in browser-runtime.ts

All CDP message routing happens inside [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts). The core logic resides in `handleMessage`, which inspects incoming protocol events to maintain an accurate view of the browser state.

### The pendingDialogs Map

When `handleMessage` receives a `Page.javascriptDialogOpening` event, it extracts the dialog parameters—including `type`, `message`, and `defaultPrompt`—and stores them in an internal `Map` named `pendingDialogs`. The key for each entry is the current CDP session ID, ensuring isolation across multiple browser contexts.

```typescript
// Conceptual flow inside browser-runtime.ts
if (method === 'Page.javascriptDialogOpening') {
  pendingDialogs.set(sessionId, {
    type: params.type,        // "alert", "confirm", or "prompt"
    message: params.message,
    defaultPrompt: params.defaultPrompt
  });
}

```

When the page closes the dialog (either programmatically or via user action), the `Page.javascriptDialogClosed` event triggers removal of that session’s entry from the Map, preventing stale state.

### Session Lifecycle Management

The Map is automatically cleared when a session detaches or its target is destroyed. This best-effort cleanup ensures that crashed or closed tabs do not leave phantom dialog entries behind, maintaining consistency for long-running automation scripts.

## Detecting Active Dialogs with pendingDialog()

[`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) exports a public accessor function named `pendingDialog(sessionId = state.sessionId)` that returns a shallow copy of the stored dialog object or `null` if no dialog is pending for that session. This function serves as the canonical check for any component needing to react to modal interference.

```typescript
import { pendingDialog } from "browser-runtime";

const dlg = pendingDialog();  // → {type:"alert", message:"…"} or null

if (dlg) {
  console.log(`Blocking dialog detected: ${dlg.type}`);
}

```

Because the function returns a copy rather than the internal reference, consumers cannot accidentally mutate the `pendingDialogs` Map.

## Practical Usage in Navigation and Observation

Higher-level modules in [`driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/nav.ts) and [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) leverage `pendingDialog()` to short-circuit operations that would otherwise fail or produce meaningless results while a modal is open.

### Short-Circuiting pageInfo in driver/nav.ts

The `pageInfo()` function checks `pendingDialog()` before gathering page metrics. If a dialog is active, it immediately returns a `{ dialog }` object containing the pending dialog details instead of the usual viewport dimensions and scroll positions.

```typescript
import { pageInfo } from "driver/nav";

const info = await pageInfo();

if ("dialog" in info) {
  // Navigation is blocked by a modal
  console.log("Active dialog:", info.dialog);
} else {
  // Safe to use page metrics
  console.log(`Page dimensions: ${info.w}x${info.h}`);
}

```

### Fallback Screenshots in driver/observe.ts

The `screenshot()` helper uses `pendingDialog()` to determine the capture strategy. When a dialog exists, it falls back to a *raw* screenshot (passing `raw: true`) that captures the modal overlay itself rather than attempting a standard clipped capture that would omit the blocking dialog.

```typescript
import { screenshot } from "driver/observe";

// Automatically switches to raw mode if a dialog is present
const path = await screenshot();
console.log(`Screenshot saved to ${path}`);

```

## Robustness and Error Handling

Dialog tracking is wrapped in defensive programming. Enabling page events via `Page.enable` is executed inside a try/catch block because certain internal browser pages (such as chrome-extension URLs or internal `about:` pages) reject the command. This ensures that dialog monitoring continues on standard web pages without crashing the entire runtime when encountering restricted contexts.

## Summary

- **Event-Driven Tracking**: [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) listens to `Page.javascriptDialogOpening` and `Page.javascriptDialogClosed` CDP events to maintain a `pendingDialogs` Map keyed by session ID.
- **Safe Accessor**: The exported `pendingDialog()` function returns a shallow copy of active dialog data or `null`, preventing external mutation of internal state.
- **Navigation Awareness**: `pageInfo()` in [`driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/nav.ts) returns a `{ dialog }` object instead of page metrics when a modal blocks the context.
- **Visual Capture**: `screenshot()` in [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) automatically falls back to raw capture mode to include the dialog in the image.
- **Session Safety**: Entries are cleared on session detach or target destruction, and `Page.enable` errors are caught to handle restricted internal pages gracefully.

## Frequently Asked Questions

### How does ego-browser detect when a JavaScript dialog opens?

Ego-browser subscribes to the `Page.javascriptDialogOpening` event via Chrome DevTools Protocol. When this event fires, `handleMessage` in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) extracts the dialog type and message and stores them in the `pendingDialogs` Map under the current session ID.

### What happens if I call pageInfo() while an alert() is blocking the page?

The `pageInfo()` function in [`driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/nav.ts) calls `pendingDialog()` before gathering metrics. If an alert, confirm, or prompt is active, it returns an object containing only the `{ dialog }` property with the dialog details, allowing your automation logic to handle or dismiss the modal before proceeding.

### Can ego-browser take screenshots that include the dialog box?

Yes. The `screenshot()` function in [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) checks `pendingDialog()` before capturing. If a dialog is present, it automatically switches to raw screenshot mode, which captures the entire viewport including the modal overlay, ensuring you can see the blocking dialog in the output image.

### Is dialog tracking isolated between browser tabs?

Absolutely. The `pendingDialogs` Map uses CDP session IDs as keys. Each tab or target maintains its own session, so concurrent automation across multiple pages does not leak dialog state between contexts.