# How Ego-Browser Handles JavaScript Dialogs (Alert, Confirm, Prompt)

> Learn how Ego Browser handles JavaScript dialogs like alert, confirm, and prompt using Chrome DevTools Protocol events. Discover its efficient tracking mechanism for seamless testing.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-08-06

---

**Ego-Browser tracks JavaScript dialogs via Chrome DevTools Protocol events `Page.javascriptDialogOpening` and `Page.javascriptDialogClosed`, storing active dialogs in a session-scoped `pendingDialogs` Map with a public `pendingDialog()` accessor for downstream consumers.**

JavaScript dialogs (`alert()`, `confirm()`, `prompt()`) block page interaction and break standard automation workflows. Ego-Browser, a lightweight browser automation framework from [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite), implements a robust, session-aware dialog tracking system that lets agents detect and adapt to these blocking conditions in real time. This article examines the implementation details, key source files, and practical usage patterns.

---

## CDP Event Handling in browser-runtime.ts

The core dialog tracking logic lives in [`src/runtime/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/runtime/browser-runtime.ts). Here, the `handleMessage` function routes incoming Chrome DevTools Protocol messages and maintains dialog state.

### Storing Dialog State

When a `Page.javascriptDialogOpening` event arrives, ego-browser extracts the dialog parameters and persists them:

```typescript
// Conceptual flow from browser-runtime.ts
pendingDialogs.set(sessionId, {
  type: dialog.type,        // "alert" | "confirm" | "prompt"
  message: dialog.message,
  defaultPrompt: dialog.defaultPrompt  // for prompt dialogs only
});

```

The `pendingDialogs` Map uses the CDP session ID as its key, ensuring isolation across concurrent browser sessions.

### Cleaning Up Closed Dialogs

On `Page.javascriptDialogClosed`, the corresponding entry is removed:

```typescript
pendingDialogs.delete(sessionId);

```

The Map is also cleared automatically when a session detaches or its target is destroyed, preventing memory leaks from stale dialog entries.

---

## The pendingDialog() Public API

Ego-Browser exports a **`pendingDialog(sessionId?)`** utility from [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) that consumers use to check for blocking dialogs:

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

const dlg = pendingDialog();  // Defaults to current state's sessionId
// Returns: { type, message, defaultPrompt } | null

```

The function returns a **shallow copy** of the stored dialog object, protecting internal state from external mutation. If no dialog is active for the given session, it returns `null`.

---

## Consumer Integrations: Adaptive Behavior Based on Dialog State

Two critical helpers in ego-browser consume `pendingDialog()` to modify their behavior when a dialog blocks the page.

### pageInfo() in driver/nav.ts

The `pageInfo()` navigation helper checks for dialogs before gathering page metrics. In [`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/nav.ts):

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

const info = await pageInfo();

if ("dialog" in info) {
  // Dialog is blocking normal page state
  console.log("Dialog open:", info.dialog.type, info.dialog.message);
} else {
  // Standard page info available
  console.log("Page dimensions:", info.w, info.h);
}

```

When `pendingDialog()` returns a truthy value, `pageInfo()` short-circuits and returns `{ dialog }` instead of the usual `{ url, w, h, scale, ... }` metrics object. This pattern lets calling code branch immediately without additional state checks.

### screenshot() in driver/observe.ts

The screenshot helper in [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) uses dialog detection to avoid capture failures. Standard screenshots often fail or exclude modal dialogs that overlay the viewport. Ego-browser handles this transparently:

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

const path = await screenshot();  // Automatically adapts to dialog presence

```

Internally, `screenshot()` checks `pendingDialog()`:

- **No dialog**: Proceed with normal screenshot logic
- **Dialog detected**: Set `raw: true` to capture the full viewport including the blocking dialog

This **best-effort fallback** ensures automation continues without throwing errors when dialogs appear unexpectedly.

---

## Best-Effort Page Event Enablement

Dialog tracking depends on `Page.enable` CDP commands, which some internal Chrome pages (e.g., `chrome://` URLs) reject. Ego-browser wraps this initialization in try/catch:

```typescript
// From browser-runtime.ts - defensive enablement
try {
  await sendCDP("Page.enable");
} catch {
  // Continue without dialog tracking; don't break other features
}

```

This **graceful degradation** ensures core automation works even on restricted pages, with dialog tracking available where supported.

---

## Summary

- **Event-driven architecture**: CDP's `Page.javascriptDialogOpening` and `Page.javascriptDialogClosed` provide the foundation for real-time dialog detection
- **Session-scoped storage**: The `pendingDialogs` Map in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) isolates state per CDP session
- **Public accessor**: `pendingDialog()` returns immutable snapshots of active dialog state
- **Adaptive consumers**: `pageInfo()` and `screenshot()` 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) branch their behavior based on dialog presence
- **Resilient design**: Defensive `Page.enable` handling and automatic cleanup prevent errors and memory leaks

---

## Frequently Asked Questions

### How does ego-browser distinguish between alert, confirm, and prompt dialogs?

The `type` field in the dialog object returned by `pendingDialog()` contains the string `"alert"`, `"confirm"`, or `"prompt"` as reported by Chrome DevTools Protocol. This matches the JavaScript API that created the dialog, letting your code respond appropriately—for example, knowing that only `prompt` dialogs have a `defaultPrompt` value.

### Can I dismiss or interact with dialogs programmatically through ego-browser?

The current implementation in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) focuses on **detection and observation** rather than control. The `pendingDialog()` accessor is read-only; actual dismissal requires sending CDP commands directly (e.g., `Page.handleJavaScriptDialog`) or using UI automation to click dialog buttons. The detection API informs when such intervention is needed.

### What happens to pending dialogs when a page navigates or closes?

The `pendingDialogs` Map automatically clears entries when the CDP session detaches or the target destroys, as handled in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts). This prevents stale state from persisting across navigations or leaking memory during long-running automation sessions with many page lifecycles.

### Does dialog tracking work on all page types?

Dialog tracking is **best-effort**—initialization attempts `Page.enable` but catches exceptions for restricted pages like `chrome://` URLs. Standard `http://` and `https://` pages support full dialog tracking. Internal Chrome pages may lack CDP Page domain events, so `pendingDialog()` returns `null` unconditionally in those contexts.