# How to Test ego-lite Components: A Complete Guide to Node.js Native Testing

> Learn how to test ego-lite components using Node.js 22 test runner and FakeEgo mocks. This guide covers asserting CDP sequences without a real browser for efficient testing.

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

---

**Testing ego-lite components relies on Node.js 22+'s built-in test runner, colocated `*.test.mjs` files, and a **FakeEgo** mock injected via `globalThis.ego` overrides to assert CDP sequences without a real browser.**

The **ego-browser** package in the [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) repository ships with a comprehensive test suite covering task-space management, element resolution, and Chrome DevTools Protocol (CDP) evaluation. All tests use native Node.js APIs, eliminating external test framework dependencies while maintaining deterministic execution through strategic mocking of low-level browser bindings.

## Test Architecture and Runtime Mocking

The testing strategy centers on **colocation** and **deterministic mocking**. Test files live alongside source code under `package/ego-browser/src/` using the `*.test.mjs` suffix, discovered automatically by `node --test`.

### The FakeEgo Pattern

Tests inject a lightweight **FakeEgo** double by temporarily overriding `globalThis.ego`. This allows helper functions to call browser bindings without launching Chrome, enabling fast, isolated unit tests.

In `src/helpers.test.mjs`, the `withEgo` utility swaps the global binding and automatically restores it after each test:

```javascript
import test from "node:test";
import assert from "node:assert/strict";
import { withEgo } from "./helpers.test.mjs";

test("newTaskSpace creates and selects an agent task space", async () => {
  const calls = [];
  await withEgo(
    {
      async createTaskSpace(name) {
        calls.push(["createTaskSpace", name]);
        return { taskId: name, id: 7, name, ownership: "agent" };
      },
      useTaskSpace(taskId) {
        calls.push(["useTaskSpace", taskId]);
        return taskId;
      },
    },
    async () => {
      const result = await newTaskSpace("checkout-flow");
      assert.deepEqual(result, {
        taskId: "checkout-flow",
        id: 7,
        name: "checkout-flow",
        ownership: "agent",
      });
    }
  );
  assert.deepEqual(calls, [
    ["createTaskSpace", "checkout-flow"],
    ["useTaskSpace", 7],
  ]);
});

```

*Source*: [`src/helpers.test.mjs`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.test.mjs#L43-L66)

### Low-Level Override Mechanism

For CDP-level control, `__testing.setOverrides` replaces specific runtime behaviors such as `cdpOverride` and sleep functions. This isolates asynchronous flows and removes timing dependencies.

```javascript
import test from "node:test";
import assert from "node:assert/strict";
import { setOverrides } from "../dist/src/state.js";
import * as helpers from "../dist/src/helpers.js";

test("page.url reads the current URL asynchronously", async () => {
  const restore = setOverrides({
    cdpOverride: async (method) => {
      assert.equal(method, "Runtime.evaluate");
      return { result: { value: JSON.stringify({ url: "https://example.com" }) } };
    },
  });

  try {
    const urlPromise = helpers.helperContext().page.url();
    assert.equal(typeof urlPromise.then, "function");
    assert.equal(await urlPromise, "https://example.com");
  } finally {
    restore();
  }
});

```

*Source*: [`src/helpers.test.mjs`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.test.mjs#L27-L50)

## Running the Test Suite

Execute the full suite from the repository root using npm, which handles the build step automatically.

1. **Install dependencies**:
   ```bash
   npm install
   ```

2. **Build and test**:
   ```bash
   npm test
   ```

   
   This command runs `npm run build` (using `esbuild` to produce `dist/`) followed by `node --test src/**/*.test.mjs`, printing progress and any failures.

## Testing Task-Space Lifecycle Management

The task-space API—critical for agent orchestration—requires verifying the correct sequence of binding calls. Tests in `src/helpers.test.mjs` cover the full lifecycle: `newTaskSpace`, `useOrCreateTaskSpace`, `switchTaskSpace`, `completeTaskSpace`, `handOffTaskSpace`, and `waitForAgentControl`.

Each test checks:
- **Correct binding sequences** (e.g., `createTaskSpace` followed by `useTaskSpace`)
- **Error handling** for binding-error objects like `{ error: "..."}`
- **State transitions** between agent and user ownership

The `src/taskspace-e2e.test.mjs` file provides end-to-end coverage for complex orchestration scenarios.

## Testing Element Resolution and Error Classification

The `src/element-resolver.test.mjs` module validates how locators (e.g., `css:`, `role:`, `xpath=`) are parsed and whether resolution failures are classified as **transient** (retryable) or **permanent** (fatal).

```javascript
import test from "node:test";
import assert from "node:assert/strict";
import { resolveElement } from "../dist/src/element-resolver.js";

test("resolveElement returns transient error for missing element", async () => {
  const result = await resolveElement("css:#does-not-exist");
  assert.equal(result.kind, "transient");
});

```

This ensures that agent loops correctly retry on timing-related DOM misses while failing fast on invalid selector syntax.

## Testing CDP Evaluation and Browser Drivers

The `src/cdp-eval.test.mjs` file confirms that `cdp()` and `js()` wrappers correctly marshal expressions, handle return values, and surface protocol errors. Individual driver modules—**nav**, **pointer**, **keyboard**, and **downloads**—each maintain dedicated `*.test.mjs` files in `src/driver/` asserting that high-level helpers emit the correct CDP messages and behave like Playwright-compatible facades.

For example, `src/driver/pointer.test.mjs` validates click coordinates, drag sequences, and scroll actions against mocked CDP responses.

## Validating the Learning Subsystem

Site-specific "learnings" (automation scripts) are validated against JSON schemas in `src/learning/index.test.mjs`. Tests ensure manifests contain valid semantic versions, required tool definitions, and safe execution parameters.

```javascript
import test from "node:test";
import assert from "node:assert/strict";
import { validateLearning } from "../dist/src/learning/index.js";

test("validateLearning rejects malformed manifest", async () => {
  const badManifest = { name: "bad", version: "not-a-semver" };
  await assert.rejects(() => validateLearning(badManifest), /invalid version/);
});

```

*Source*: [`src/learning/index.test.mjs`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/learning/index.test.mjs)

## Writing New Tests for ego-lite Components

Follow this pattern when adding coverage for new features:

1. **Colocate the test** next to the source file with the `.test.mjs` suffix (e.g., `src/driver/my-feature.test.mjs`).
2. **Import from the compiled output** (`../dist/src/...`) to mirror the runtime environment.
3. **Mock the Ego runtime** using `withEgo` for high-level interactions or `__testing.setOverrides` for CDP-level control.
4. **Assert sequences** with `assert.deepEqual` for call arrays or `assert.rejects` for error paths.
5. **Cover edge cases** including binding errors and malformed inputs.

## Summary

- **ego-lite** uses Node.js 22+'s native test runner (`node --test`) with `*.test.mjs` files colocated in `src/`.
- **FakeEgo** mocking via `globalThis.ego` overrides enables fast, deterministic unit tests without browser dependencies.
- **`__testing.setOverrides`** provides surgical control over CDP calls, sleep functions, and side effects.
- **Task-space tests** verify lifecycle sequences in `helpers.test.mjs` and `taskspace-e2e.test.mjs`.
- **Element resolution tests** classify locator failures as transient or permanent in `element-resolver.test.mjs`.
- **Driver tests** in `src/driver/*.test.mjs` ensure CDP message correctness for user actions.

## Frequently Asked Questions

### What Node.js version is required to test ego-lite components?

**Node.js 22 or higher is required.** The test suite depends on the built-in `node:test` runner and `node:assert/strict` modules, which became stable and fully featured in recent LTS releases.

### How do I mock Chrome DevTools Protocol calls when testing ego-lite?

**Use `__testing.setOverrides` from [`src/state.js`](https://github.com/citrolabs/ego-lite/blob/main/src/state.js).** Pass a `cdpOverride` function to intercept protocol methods like `Runtime.evaluate` and return fake responses, making asynchronous flows deterministic and eliminating network dependencies.

### Where should I place new test files in the ego-browser package?

**Always colocate tests with the source code under `package/ego-browser/src/`.** Name the file with the `.test.mjs` suffix (e.g., `feature.test.mjs` next to [`feature.js`](https://github.com/citrolabs/ego-lite/blob/main/feature.js)). The `npm test` command automatically discovers these files using the glob `src/**/*.test.mjs`.

### How does ego-lite classify element resolution errors in tests?

**Errors are classified as either transient or permanent.** The `resolveElement` function in [`src/element-resolver.js`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.js) returns objects with a `kind` property. Tests in `element-resolver.test.mjs` verify that missing DOM elements yield `{ kind: "transient" }` (triggering retry logic) while invalid syntax yields `{ kind: "permanent" }` (failing immediately).