# How Test Overrides and FakeEgo Doubles Enable Reliable Testing in ego-lite

> Learn how ego-lite uses test overrides and FakeEgo doubles to mock the Chrome DevTools Protocol, enabling fast and reliable browser testing without a real browser.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: testing
- Published: 2026-08-25

---

**Test overrides and the `FakeEgo` double work together to replace the live Chrome DevTools Protocol runtime with in-memory mocks, allowing fast, deterministic unit tests without launching a real browser.**

The `ego-lite` project implements a lightweight testing harness centered on two core mechanisms: the `__testing.setOverrides()` API for injecting custom stubs and a `FakeEgo` object that mimics the full `globalThis.ego` runtime. Together they let tests simulate CDP interactions, error conditions, and browser events while exercising the same code paths used in production.

## Understanding the Test Override System

The override system lives in `src/helpers.test.mjs` and provides a controlled way to swap implementations during test execution. It targets low-level helpers that normally communicate with the browser runtime.

### How setOverrides Works

The `__testing.setOverrides()` function accepts an object mapping function names to replacement implementations. When invoked, it temporarily replaces the default behavior of helpers like `ensureSession`, `cdp`, `js`, and the CDP transport layer.

```javascript
// src/some-feature.test.mjs
import { __testing } from './helpers.test.mjs';
import { navigate } from './helpers.js';

__testing.setOverrides({
  // Force Page.navigate to reject with a custom error
  sendCDPMessage: async (method, params) => {
    if (method === 'Page.navigate') {
      throw new Error('Navigation failed – simulated by test');
    }
    return await __testing.originalSendCDPMessage(method, params);
  },
});

await t.rejects(
  navigate('https://example.com'),
  /Navigation failed – simulated by test/
);

```

### Scope and Cleanup

Overrides are scoped to the current test file and automatically revert when the test finishes. For explicit cleanup, tests call `__testing.restoreOriginals()`:

```javascript
// In a test file's afterEach hook
afterEach(() => {
  __testing.restoreOriginals();   // Restores real implementations
});

```

## The FakeEgo Double Architecture

When the test environment initializes, `src/helpers.test.mjs` creates a `FakeEgo` object and assigns it to `globalThis.ego`. This double implements the complete public API surface—`send`, `on`, `sendCDPMessage`, and related methods—without requiring an actual CDP session.

### Recording and Programmability

The `FakeEgo` instance records every `sendCDPMessage` call for later inspection. Tests can program predetermined responses and simulate asynchronous browser events through its event emitter interface.

```javascript
// src/click.test.mjs
import { __testing } from './helpers.test.mjs';
import { click } from './helpers.js';

await click('@5');                           // Element referenced by @5
const lastCall = __testing.fakeEgo.lastSend; // Stored by FakeEgo

t.same(lastCall.method, 'DOM.focus');
t.same(lastCall.params, { backendNodeId: 5 });

```

### Simulating Browser Events

The `on` method allows code under test to register listeners for simulated CDP events such as `Page.loadEventFired`. This enables testing of async workflows that depend on browser lifecycle notifications.

## Key Testing Capabilities

The combination of overrides and the `FakeEgo` double enables specific testing scenarios that would be difficult or slow with real browser automation.

### Mocking Asynchronous CDP Interactions

Tests execute entirely in-process without browser startup overhead. Complex sequences of CDP calls—navigation, DOM queries, JavaScript execution—can be validated in milliseconds.

### Simulating Error Conditions

The harness supports both transient and permanent failures by throwing `ElementResolutionError` with the appropriate `transient` flag set. This validates retry logic and error handling paths.

### Exercising Production Code Paths

High-level helpers (`click`, `navigate`, `waitForSelector`) are invoked through the same modules used in production. The only difference is that [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) detects and applies overrides when present, then delegates to [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) for real CDP transport or the fake implementation during tests.

## State Isolation

The mutable runtime singleton defined in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) presents a testing challenge: shared state can leak between tests. The override system addresses this by ensuring each test receives a fresh copy of the state after `restoreOriginals()` completes, preventing cross-test contamination.

## Source File Reference

| File | Purpose |
|------|---------|
| `src/helpers.test.mjs` | Defines `__testing`, creates the `FakeEgo` double, and provides `setOverrides` / `restoreOriginals` helpers |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Public helper surface that reads current overrides when present |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Real CDP transport implementation replaced by the fake during testing |
| [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Mutable runtime singleton; isolated per test via override reset |

## Summary

- **`__testing.setOverrides()`** injects custom stubs for low-level CDP helpers, scoped per test file with automatic cleanup.
- **`FakeEgo`** replaces `globalThis.ego` with an in-memory double that records calls, programs responses, and emits simulated browser events.
- **Combined capabilities** include fast async mocking, precise error simulation, and full production code path coverage without browser overhead.
- **State isolation** prevents cross-test pollution through fresh state initialization on each override reset.

## Frequently Asked Questions

### How do test overrides differ from traditional mocking libraries?

Test overrides in `ego-lite` operate at the module level rather than replacing individual functions. The `__testing` API swaps implementations for named helpers across the entire import graph, ensuring consistency when multiple modules depend on the same underlying function. This approach avoids the complexity of managing mock scopes across nested imports while preserving the ability to inspect and verify calls.

### What happens if I forget to call restoreOriginals?

Overrides are automatically cleaned up when the test file finishes executing, as each test run receives an isolated override context. However, explicit `restoreOriginals()` calls in `afterEach` hooks remain a best practice for tests that make multiple override changes, ensuring predictable state between assertions within the same file.

### Can FakeEgo simulate network latency or timeouts?

Yes. The `FakeEgo` implementation controls all promise resolution timing. Tests can return delayed promises from `sendCDPMessage` overrides or use the `on` emitter to dispatch events at scheduled intervals. This enables validation of timeout handling, debouncing logic, and race condition prevention without actual network delays.

### Is the override system available in production builds?

No. The `__testing` export and `FakeEgo` construction are gated behind `NODE_ENV=test` or equivalent environment detection during the build process. Production bundles exclude `src/helpers.test.mjs` entirely, ensuring no test code ships to runtime environments.