Modly Testing Strategies: How to Test Electron Preload APIs with Node's Native Runner

Modly uses Node's native test runner (node:test) with co-located *.test.ts files, mocking the Electron IPC bridge to test preload APIs in pure Node without launching a full Electron instance.

Electron preload APIs present a unique testing challenge: they run in a privileged renderer context and depend on Electron's ipcRenderer and webFrame APIs. This article examines how the Modly project solves this with a lightweight, dependency-free approach that keeps tests fast and deterministic. All testing patterns described here are implemented in the lightningpixel/modly repository.

Core Testing Architecture

Modly's testing strategy prioritizes minimal dependencies and fast feedback loops. Rather than importing Jest, Vitest, or Mocha, the project relies entirely on Node.js built-ins.

Test Runner: Node Native

The package.json test script invokes Node directly:

node --test

This automatically discovers all files matching *.test.* patterns across the codebase. No configuration files, no global setup, no extra packages to install.

File Organization: Co-Location

Every test file lives adjacent to its implementation:


electron/preload/
├── electron-api.ts
├── index.ts
└── artifact-registry-preload.test.ts

electron/main/
├── extension-path-guard.ts
├── extension-path-guard.test.ts
├── extension-install-utils.ts
└── extension-install-utils.test.mjs

This convention makes the relationship between implementation and verification immediately obvious. When you modify electron-api.ts, the corresponding test file is right there.

Assertions: Node Assert Module

All tests import from node:assert/strict:

import assert from 'node:assert/strict'

This provides assert.equal, assert.deepEqual, assert.throws, and other familiar methods without pulling in Chai or similar libraries.

Testing Electron Preload APIs

The preload layer in electron/preload/electron-api.ts exposes a scoped API to the renderer process. Testing this requires simulating Electron's restricted environment.

The Challenge: Electron Dependencies

Preload scripts have access to:

  • ipcRenderer – for async IPC with the main process
  • webFrame – for renderer-level operations like zoom control

These APIs are only available inside an actual Electron renderer. Modly's solution: inject mock implementations through a factory function.

Factory Pattern for Testability

The createElectronApi function accepts its dependencies as parameters:

// Simplified signature from electron-api.ts
function createElectronApi(
  ipcRenderer: IpcRendererLike,
  webFrame: WebFrameLike
): ElectronApi

In production, the real Electron objects are passed in. In tests, a stub implementation records all interactions.

Recording IPC Calls

The test suite in electron/preload/artifact-registry-preload.test.ts creates a spy that captures every invoke call:

import assert from 'node:assert/strict'
import test from 'node:test'

import { createElectronApi } from './electron-api.ts'

test('preload exposes scoped workspace library list/read/open methods', async () => {
  const calls: Array<{ channel: string; payload?: unknown }> = []
  const api = createElectronApi(
    {
      invoke: async (channel: string, payload?: unknown) => {
        calls.push({ channel, payload })
        return { success: true, entries: [] }
      },
      send: () => undefined,
      on: () => undefined,
      removeAllListeners: () => undefined,
    },
    { setZoomFactor: () => undefined }
  )

  await api.workspace.library.list()
  await api.workspace.library.read({
    workspacePath: 'Workflows/checkpoints/hero.landmarks.v1.json',
    sourceWorkspacePath: 'Workflows/checkpoints/hero.glb',
  })
  await api.workspace.library.open({
    workspacePath: 'Workflows/checkpoints/hero.landmarks.v1.json',
    sourceWorkspacePath: 'Workflows/checkpoints/hero.glb',
  })

  assert.deepEqual(calls, [
    { channel: 'workspace:library:list', payload: undefined },
    {
      channel: 'workspace:library:read',
      payload: {
        workspacePath: 'Workflows/checkpoints/hero.landmarks.v1.json',
        sourceWorkspacePath: 'Workflows/checkpoints/hero.glb',
      },
    },
    {
      channel: 'workspace:library:open',
      payload: {
        workspacePath: 'Workflows/checkpoints/hero.landmarks.v1.json',
        sourceWorkspacePath: 'Workflows/checkpoints/hero.glb',
      },
    },
  ])
})

What This Verifies

Contract Aspect Test Assertion
Channel naming workspace:library:list, workspace:library:read, workspace:library:open
Payload structure Objects with workspacePath and sourceWorkspacePath fields
Return value propagation Promise resolves with stub's { success: true, entries: [] }
Method isolation Each call is independent, no shared state leakage

The test runs in milliseconds because no Electron instance is launched. The entire preload contract is validated in pure Node.js.

Testing Main Process Modules

The same patterns extend to main-process utilities.

Extension Path Guards

electron/main/extension-path-guard.ts validates that extension operations stay within permitted directories. Its test file extension-path-guard.test.ts stubs the file system and asserts on path traversal prevention.

Installer Utilities

electron/main/extension-install-utils.ts handles extension package extraction. The corresponding extension-install-utils.test.mjs uses the same node:test and node:assert pattern, mocking archive operations and verifying cleanup behavior.

CI Integration

The repository's .github/workflows/ci.yml runs npm test on every push. Because tests don't require Electron binaries, the CI pipeline remains lightweight and fast. Regression detection happens within seconds, not minutes.

Summary

  • Modly testing strategies rely on Node's native node:test runner—no external test framework required
  • Electron preload API testing uses factory injection to mock ipcRenderer and webFrame, enabling pure-Node verification
  • Test files follow *.test.ts/*.test.mjs naming and co-locate with source code
  • The createElectronApi factory captures IPC calls to verify channel names and payload contracts
  • All tests run via node --test, executed in CI on every push for rapid regression detection

Frequently Asked Questions

How does Modly test code that depends on Electron APIs without running Electron?

Modly's factory pattern exposes dependencies as parameters to createElectronApi. Tests pass stub objects that implement the same interface as ipcRenderer and webFrame. This allows the preload layer to execute in a pure Node environment while still verifying that it makes the correct IPC calls with the expected payloads.

Why doesn't Modly use Jest or Vitest for testing?

The project prioritizes dependency minimalism. Node's built-in test runner (stable since Node 18) provides sufficient features: test organization, async/await support, and native assertion methods. Avoiding external test frameworks reduces node_modules size, installation time, and long-term maintenance burden from dependency updates.

What naming convention should I follow when adding tests to Modly?

Name test files with the .test.ts or .test.mjs suffix and place them in the same directory as the source file they verify. The node --test flag automatically discovers these files. For example, electron-api.ts pairs with artifact-registry-preload.test.ts in electron/preload/.

How does Modly ensure preload tests stay synchronized with the main process IPC handlers?

The channel contract is the bridge. Preload tests assert that methods invoke specific channels like workspace:library:list. Main process tests verify that handlers exist for these same channels. While Modly doesn't currently use integration tests spanning both processes, the shared channel string constants (typically defined alongside the API types) keep both sides aligned.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →