LearnNewsExamplesServices

Whitebox E2E Testing with Neural Link

Traditional end-to-end (E2E) testing treats the application as a "black box," interacting purely through DOM element locators (e.g. page.locator('button')) and asserting visual outputs. In heavily virtualized environments like Neo.mjs, these tests can become flaky as DOM nodes are frequently recycled and state often resides in remote workers asynchronously decoupled from the main thread.

To tackle this, the Neo.mjs team pioneered the Neural Link Bridge. It allows our testing frameworks to penetrate the worker boundary, giving tests direct, synchronous access to the Neo.mjs runtime state. This is Whitebox E2E Testing.

Why Whitebox E2E?

  1. State Over Locators: Instead of attempting to find an ephemeral DOM node, you find the exact logical Component Instance (e.g., GridContainer, ComboBox) in the App Worker.
  2. Direct Value Assertions: You can read exact runtime values (component.value, component.store.getCount()) bypassing asynchronous DOM polling.
  3. Targeted Interaction Navigation: You combine Playwright's native DOM simulation with the semantic guarantees that the internal component is fully initialized and mapped to the UI.

Setup Instructions

Whitebox tests are executed using typical Playwright tools but rely on the neo-config.json configuration and a specific E2E config suite.

1. Enable Neural Link Inside your application directory (examples/yourApp/), you must ensure your neo-config.json mounts the AI client:

{
    "appPath": "app.mjs",
    "theme"  : ["neo-theme-dark", "neo-theme-light"],
    "useAiClient": true
}

2. Test Suite Config Target You MUST specify the E2E specific playwright config (playwright.config.e2e.mjs) when running these tests to ensure valid timeouts and environments.

npx playwright test test/playwright/e2e/<domain>/YourTestNL.spec.mjs -c test/playwright/playwright.config.e2e.mjs

3. Point the Fixture at a Brain The neuralLink fixture drives a live Brain. Set NEO_AGENTOS_RUNTIME_ROOT to the absolute path of a neo-agent-brain checkout whose dependencies are installed (npm ci). Without it, playwright.config.e2e.mjs deselects every spec that uses the fixture.

Hosted CI

The engine tier (.github/workflows/test-e2e.yml) runs on pull requests without a Brain, so it selects only the Brain-free specs.

The Brain-gated specs run in .github/workflows/test-e2e-brain.yml, and this tier gates Engine pull requests: merge readiness refuses any red, required or not. The workflow checks out the Brain at its dev tip, resolved once per run, and installs it with npm ci.

The shared scope classifier picks the tier's population, and it never skips:

  • A pull request whose change reaches the E2E impact boundary runs the full tier, on ten shards. So does every dev push.
  • Any other pull request runs the smoke subset (BRAIN_SMOKE), on one shard. Its change cannot reach the tier's specs, but the Brain's tip can still break the pairing. A green smoke is not full-tier evidence.

Each shard's test step stops at five minutes, the ceiling the engine tier enforces too.

The shards split the tier by measured time, not by test count. buildScripts/util/e2eBrainDurations.json holds each file's seconds, and e2eCiSelection.mjs gives each file, longest first, to the shard with the least time so far. A file the table does not list yet counts as its median, so a new spec needs no table edit. When the receipts' predictions drift from their test times, rebuild the table from a green full-tier run:

gh run view <run-id> --log | node buildScripts/util/e2eCiSelection.mjs --brain-durations > buildScripts/util/e2eBrainDurations.json

Each shard writes its receipt to its job summary and to its log. The log copy is the one gh run view --log can read. The receipt holds:

  • the event, attempt and tier;
  • the Engine and Brain SHAs;
  • its test time against the five-minute ceiling, and the time the duration table predicted;
  • what the tier selected, and every declared exclusion and quarantine, of which a smoke file may carry none;
  • the files the shard ran, with their outcome and measured seconds.

buildScripts/util/e2eCiSelection.mjs declares two kinds of gap, and the receipt prints both:

  • A capability excludes files only while the variable they need is unset. Provisioning it brings them back.
  • A quarantine deselects one test by file and title. It names who is accountable, what removes it and since when, and after a week every receipt flags it as stale.

A declaration whose file or title no longer exists fails the unit tier, and so does a declared smoke file.

Through 2026-10-25 the tier retries a failed test once, while #19655 diagnoses the intermittent set. Every receipt names each test that needed the retry, with its first-attempt error, and a test red on every attempt stays red. After that date the tier runs without retries.

Connecting the Neural Link (Fixtures)

Whitebox E2E tests are located in test/playwright/e2e/. Instead of manually establishing WebSocket connections, the Neo.mjs team provides a powerful Playwright fixture called neuralLink.

The fixture connects to a Bridge already listening on aiConfig.port, and spawns one when none is. You never need a Bridge running first — the fixture supplies the working directory a spawn requires, by walking up from its own location to the package that declares the Bridge npm script.

It deliberately does not use process.cwd(). Playwright resolves testDir from the config file and never changes directory, so npx playwright test -c /abs/path/config.mjs invoked from anywhere is a valid run whose worker cwd is that anywhere. Where no ancestor declares the script, the fixture leaves the working directory unset and the spawn refuses by name, rather than starting npm somewhere it cannot resolve.

Core Connection Boilerplate

When tests launch, you can use the neuralLink parameter injected by Playwright. This abstracts away the internal component architecture and exposes a simple SDK.

import { test, expect } from '../../fixtures.mjs'; // Use custom fixtures!

test.describe('Grid BigData App (Neural Link)', () => {

    test('Verify connection and inspect state', async ({ page, neuralLink }) => {
        // 1. Navigate to the app inside the browser worker
        await page.goto('examples/grid/bigData/index.html');

        // 2. Connect the fixture to the running App Worker
        //
        // Session resolution: `connectToApp()` IDENTITY-BINDS to this page's own App Worker —
        // it polls the page's `Neo.worker.App.getWorkerId()` remote (bounded wait) and passes
        // that id to the bridge's exact-id session match. The appName argument is a FALLBACK
        // only (older builds without the remote, or non-Neo pages). This matters whenever a
        // second same-named app is connected to the bridge (e.g. a developer's open dev tab):
        // appName resolution binds the OLDEST matching session, so without the identity bind
        // the test's worker queries would silently read the other app while `page.mouse`
        // drives this page.
        //
        // Examples use their fully qualified namespace as the fallback name:
        const nlApp = await neuralLink.connectToApp('Neo.examples.grid.bigData');

        // Full Applications use their named string identifier:
        // const nlApp = await neuralLink.connectToApp('Workstation');

        // Tests go here...
    });
});

Component Queries & State Inspection

The core of Whitebox testing rests on queryComponent. By calling it through the nlApp fixture, you find components by semantic layout rather than DOM classes.

// Extract exact properties directly from the instance!
const queryResult = await nlApp.queryComponent(
    { ntype: 'combobox', name: 'amountRows' }, // Selector
    ['value'] // Return Properties
);

// Important: Return properties are nested under `.properties`
// Store records and complex objects are serialized.
expect(queryResult.properties.value.id).toBe("20000");

Advanced Discovery

Queries can also be hierarchically bounded by looking for children inside specific component graphs.

test('Verify foundational grid structure', async ({ page, neuralLink }) => {
    await page.goto('examples/grid/bigData/index.html');

    // Securely bridge to the specific Application Worker
    const nlApp = await neuralLink.connectToApp('Neo.examples.grid.bigData');

    // 1. Fetch MainView (It will always return the root wrapper)
    const viewResult = await nlApp.queryComponent({ className: 'Neo.examples.grid.bigData.MainContainer' });

    // 2. Discover children inside the root scope
    // Note: The fixture currently masks rootId natively if we query globally,
    // but the underlying API fully supports ID-scoped boundaries.
    const gridResult = await nlApp.queryComponent({ ntype: 'grid-container' });

    expect(gridResult.id).toBeDefined();
});

The Expanded Fixture API

While queryComponent and getComponent cover basic state inspection, nlApp provides a robust, native suite of tools to perfectly manipulate and measure the Application Worker from Playwright.

Layout & VDOM Validation

Instead of brittle CSS selectors, you can use Neo.mjs layout tools to query the Virtual DOM and extract physical measurements, natively executed inside the browser.

// Locate internal VNode elements to check render boundaries
const rowVdom = await nlApp.queryVdom({ cls: 'neo-grid-row' });

// Get exact screen boundaries of multiple components simultaneously
const rects = await nlApp.getDomRect(['component-id-1', 'component-id-2']);

// Directly extract CSS variables or specific computed values
const styles = await nlApp.getComputedStyles('component-id-1', ['--neo-grid-row-height', 'display']);

Interaction Simulation

If Playwright's synthetic DOM clicks fail or suffer from z-index races on complex components, you can manually dispatch native browser events straight onto the VNode target via the component ID.

// Bypass Playwright DOM clicking, and dispatch to the VNode listener natively
await nlApp.simulateEvent([
    {
        action: 'click',
        targetId: 'my-grid-row-1'
    }
]);

simulateEvent is the raw escape hatch for caller-authored event sequences. For a complete drag gesture, use driveDrag: Engine resolves live node/window geometry, reads the source window's live Mouse-sensor thresholds, dispatches one atomic source-document gesture, and returns a receipt distinguishing raw event dispatch from correlated drag:start / drag:move / drag:end observations:

const receipt = await nlApp.driveDrag({
    source     : {targetId: splitterId, windowId},
    destination: {deltaX: 60, deltaY: 0},
    steps      : 8,
    durationMs : 160
});

expect(receipt).toMatchObject({success: true, phase: 'complete', released: true});
expect(receipt.observed.moveCount).toBeGreaterThan(0);

The physical receipt deliberately says nothing about the component's semantic outcome. Assert the grid order, splitter size, or dock document separately through Neural Link.

Use Playwright's native mouse when the test must hold the pointer down, inspect mid-gesture state, press Escape, or interleave application-specific readiness waits. driveDrag owns a whole transaction; it is not a pause/resume API. A native stepped move remains the right pattern for those interactive assertions:

// Held pointer-drag for mid-interaction assertions:
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(targetX, targetY, { steps: 60 });
await page.mouse.up();

See test/playwright/e2e/grid/ColumnCrossBodyDnD.spec.mjs for a worked locked-column drag (in-region landing + cross-region re-home) that pairs the gesture with Neural Link worker assertions.

Topology, Nodes, & Namespaces

Get a bird's eye view of the entire runtime environment, discovering connected workers or resolving the class hierarchy.

// Map component hierarchy dynamically
const compTree = await nlApp.getComponentTree(undefined, 2); // Depth limit 2

// View all connected Apps / Windows
const workers = await nlApp.getWorkerTopology();
const windows = await nlApp.getWindowTopology();

// Verify that specific classes loaded successfully
const hasGrid = await nlApp.checkNamespace('Neo.grid.Container');

Runtime & App State

Tools exist to inspect stores, active listeners, global config, or drag states.

// View what raw events are actually bound to this specific layout block
const listeners = await nlApp.getDomEventListeners('my-grid-container');

// Dump the full raw store records (paginated) without querying the component
const storeData = await nlApp.inspectStore('my-data-store', 50, 0);

// Review global configurations
const globalConfig = await nlApp.manageNeoConfig('get');

Advanced Metaprogramming

The Neural Link Fixture exposes powerful introspection and runtime patching capabilities for high-level whitebox debugging.

[!WARNING] The patchCode method requires Neo.config.enableHotPatching = true to be set in your neo-config.json. Due to significant security concerns, hot patching is disabled by default and should never be enabled in production environments.

// Extract the fully resolved configuration blueprint and API hooks for a class
const blueprint = await nlApp.inspectClass('Neo.button.Base');

// View the exact source code of a method currently loaded in memory
const methodSource = await nlApp.getMethodSource('Neo.button.Base', 'onClick');

// Dynamically replace the method implementation during E2E testing
await nlApp.patchCode(
    'Neo.button.Base',
    'onClick',
    'function(data) { console.log("Patched!"); }'
);

Legacy RMA vs Neural Link

Before Neural Link, Early Neo.mjs testing relied on Remote Method Access (RMA) using the window.Neo object mapped from the browser context to the worker (e.g. await page.evaluate(() => Neo.worker.App.getConfigs(...))).

This legacy strategy, provided by the fallback neo fixture, suffers from serialization drops and lacks targeted debugging interfaces. The neuralLink SDK (nlApp) completely supersedes these legacy calls and connects directly to the core MCP server logic via postMessage boundaries. You should always use neuralLink for Neo native testing. Legacy RMA is strictly retained as a fallback layer for React/Angular wrapped Neo integrations where the Neural Link is disabled.

A Complete Example Walkthrough

Let's look at how Whitebox testing effectively validates complex grid data inputs, without writing a single CSS locator for the inner workings.

This test simulates interaction on the Grid row counter and uses Neural Link to assert the engine accurately digested the new variable asynchronously.

test('Simulate DOM selection and verify Neo.mjs engine data', async ({ page, neuralLink }) => {
    await page.goto('examples/grid/bigData/index.html');

    // Bound the bridge to the specific namespace to prevent inference races
    const nlApp = await neuralLink.connectToApp('Neo.examples.grid.bigData');

    // 1. Visually identify the ComboBox using typical Playwright Locator on the DOM wrapper layer
    const rowComboBox = page.locator('.neo-combobox').filter({ hasText: 'Amount Rows' });

    // Simulate standard user input
    await rowComboBox.click();
    await page.keyboard.press('ArrowDown');
    await page.keyboard.press('Enter');

    // 2. Validate Engine Memory. Was the data actually stored by the Application Worker?
    const queryResult = await nlApp.queryComponent(
        { ntype: 'combobox', name: 'amountRows' },
        ['value']
    );

    // Instead of looking for DOM text reading '40k', we verify what the class actually believes to be true.
    expect(queryResult.properties.value.id).toBe("40000");
});

Using this architecture, you get the absolute best of both paradigms: Playwright manages browser/input simulation with massive parity while the Neural Link ensures you never have to guess about component health or internal state logic.