How to Test ego-lite Components: A Complete Guide to Node.js Native Testing
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 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:
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
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.
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
Running the Test Suite
Execute the full suite from the repository root using npm, which handles the build step automatically.
-
Install dependencies:
npm install -
Build and test:
npm testThis command runs
npm run build(usingesbuildto producedist/) followed bynode --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.,
createTaskSpacefollowed byuseTaskSpace) - 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).
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.
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
Writing New Tests for ego-lite Components
Follow this pattern when adding coverage for new features:
- Colocate the test next to the source file with the
.test.mjssuffix (e.g.,src/driver/my-feature.test.mjs). - Import from the compiled output (
../dist/src/...) to mirror the runtime environment. - Mock the Ego runtime using
withEgofor high-level interactions or__testing.setOverridesfor CDP-level control. - Assert sequences with
assert.deepEqualfor call arrays orassert.rejectsfor error paths. - Cover edge cases including binding errors and malformed inputs.
Summary
- ego-lite uses Node.js 22+'s native test runner (
node --test) with*.test.mjsfiles colocated insrc/. - FakeEgo mocking via
globalThis.egooverrides enables fast, deterministic unit tests without browser dependencies. __testing.setOverridesprovides surgical control over CDP calls, sleep functions, and side effects.- Task-space tests verify lifecycle sequences in
helpers.test.mjsandtaskspace-e2e.test.mjs. - Element resolution tests classify locator failures as transient or permanent in
element-resolver.test.mjs. - Driver tests in
src/driver/*.test.mjsensure 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. 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). 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 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).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →