npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jest-mock-xapi

v1.0.6

Published

A jest mock xAPI module validating and testing Cisco Collaboration Device macros

Readme

Jest Mock xAPI

Run Cisco RoomOS JavaScript macro tests in Node.js while preserving the normal import xapi from "xapi" developer experience.

This project provides a Jest-compatible mock of the RoomOS xapi module so JavaScript macros for Cisco RoomOS devices can be tested locally in a standard Node environment. It exists so macro developers can validate behavior without deploying to a device for every change or changing production macro imports.

Overview

The package exposes a mocked default xapi export that mirrors the RoomOS macro runtime and is backed by generated RoomOS schemas. Known paths, product-specific availability, default configuration values, command parameters, and xapi.doc(...) results behave much closer to a real device.

If you are setting up Jest for the first time, start with the official Jest Getting Started guide.

New style API support

// Call commands with schema-backed new style paths.
await xapi.Command.Audio.Volume.Set({ Level: 10 });

// Read status values from new style status paths.
await xapi.Status.Audio.Volume.get();

// Set config values and notify matching config listeners.
await xapi.Config.Audio.DefaultVolume.set(10);

// Subscribe to event payloads with the same path shape used by RoomOS.
xapi.Event.UserInterface.Extensions.Panel.Clicked.on((event) => {
  console.log("Panel:", event.PanelId);
});

See Use RoomOS API in tests and Set values and trigger xAPI updates.

Old style API support

// Call commands with the public RoomOS spaced path style.
await xapi.command("Audio Volume Set", { Level: 10 });

// Read status values with a spaced old style path.
await xapi.status.get("Audio Volume");

// Set config values with the old style API.
await xapi.config.set("Audio DefaultVolume", 10);

// Subscribe to event payloads with an old style path.
xapi.event.on("UserInterface Extensions Panel Clicked", (event) => {
  console.log("Panel:", event.PanelId);
});

Later examples keep the new style form visible and place old style equivalents in expandable details blocks.

Product-enforced xAPI usage

// Desk Pro is the default product platform.
await xapi.Status.SystemUnit.ProductPlatform.get();

// Switch the mock to another product platform and its supported xAPIs.
xapi.Status.SystemUnit.ProductPlatform.set("Codec EQ");

// Schema docs are product-aware.
await xapi.doc("Command Audio Setup Clear");

// Switching back to Desk Pro removes unsupported product-specific docs/paths.
xapi.Status.SystemUnit.ProductPlatform.set("Desk Pro");

See Select a RoomOS product and Read schema docs.

Test helper functions

// Set a status value and notify matching status listeners.
xapi.Status.Audio.Volume.set(30);

// Set a config value and notify matching config listeners.
await xapi.Config.Audio.DefaultVolume.set(70);

// Emit an event payload to matching event listeners.
xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
  PanelId: "speed-dial-panel",
});

// Assert command calls with normal Jest matchers.
expect(xapi.Command.Dial).toHaveBeenCalledWith({
  Number: "[email protected]",
});

See Set leaf status and config values, Emit xEvent payloads, Assert xCommand calls, and Mock utilities.

Setup

Prerequisites

  • Node.js 20 or later is recommended for local development and testing.

  • A Jest-based test setup is expected in the macro project that consumes this package.

  • The macro under test should import xapi exactly as it would on a Cisco RoomOS device:

    import xapi from "xapi";

Installation

  1. Install jest-mock-xapi and Jest in your macro project.

    npm install --save-dev jest jest-mock-xapi
  2. Choose one Jest integration option in your macro project's package.json.

    Option 1 (recommended): Map xapi directly to jest-mock-xapi with moduleNameMapper.

    {
      "type": "module",
      "scripts": {
        "test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand",
        "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watchAll --runInBand"
      },
      "jest": {
        "testEnvironment": "node",
        "moduleNameMapper": {
          "^xapi$": "jest-mock-xapi"
        }
      }
    }

    See the speed-dial-macro example package.json for this option in a working macro project.

    Option 2: Register the virtual xapi module through the package's setup entrypoint if you prefer a setup-file workflow.

    {
      "type": "module",
      "scripts": {
        "test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand",
        "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watchAll --runInBand"
      },
      "jest": {
        "testEnvironment": "node",
        "setupFiles": ["jest-mock-xapi/register"]
      }
    }
  3. Write tests that import your macro, set xAPI values or emit xAPI changes, and then assert the macro responded correctly.

    import { beforeEach, describe, expect, it, jest } from "@jest/globals";
    
    describe("my roomos macro", () => {
      beforeEach(() => {
        jest.resetModules();
      });
    
      it("dials when a panel event is triggered", async () => {
        const { default: xapi } = await import("xapi");
        xapi.reset();
    
        await import("./my-macro.js");
    
        xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
          PanelId: "speed-dial-panel",
        });
    
        expect(xapi.Command.Dial).toHaveBeenCalledWith({
          Number: "[email protected]",
        });
      });
    });
  4. Run the tests from your macro project.

    Run your tests once with:

    npm test

    Expected output:

    PASS ./my-macro.test.js
      my roomos macro
        ✓ dials when a panel event is triggered
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total

    Run your tests continuously upon macro/test code changes with:

    npm run test:watch

The package has two public entrypoints:

  • jest-mock-xapi exports the mock object itself and is the recommended target for moduleNameMapper.
  • jest-mock-xapi/register registers a virtual xapi module for Jest setup-file workflows.

Usage

The expected development flow is that a macro developer installs jest-mock-xapi, keeps production macro code written for the native RoomOS runtime, and uses Jest to control mock device state from tests. In practice, a test imports the macro, seeds status or config values, emits events or updates, and then asserts the macro reacted with the expected xAPI calls.

Supported xAPI surface

The mock is exposed as a default module export, matching the RoomOS macro runtime:

import xapi from "xapi";

The supported macro-facing surface includes:

// Commands are schema-backed Jest mock functions.
await xapi.Command.Audio.Volume.Set({ Level: 10 });

// Status paths can be read, subscribed to, and removed in tests.
await xapi.Status.Audio.Volume.get();
xapi.Status.Audio.Volume.on(listener);
xapi.Status.Audio.Volume.once(listener);
xapi.Status.Call[1].remove();

// Config paths can be read, set, and subscribed to.
await xapi.Config.Audio.DefaultVolume.get();
await xapi.Config.Audio.DefaultVolume.set(70);
xapi.Config.Audio.DefaultVolume.on(listener);
xapi.Config.Audio.DefaultVolume.once(listener);

// Event paths can be subscribed to.
xapi.Event.UserInterface.Extensions.Panel.Clicked.on(listener);
xapi.Event.UserInterface.Extensions.Panel.Clicked.once(listener);

// Indexed paths use bracket notation.
await xapi.Status.Call[1].Status.get();

// New style command paths and operation functions expose Jest helpers.
xapi.Command.Dial.mockResolvedValueOnce(result);
xapi.Status.Audio.Volume.get.mockResolvedValueOnce(55);

The same operation shape is available for any schema-backed path supported by the selected product.

// Commands use spaced public RoomOS paths.
await xapi.command("Audio Volume Set", { Level: 10 });

// Status paths can be read and subscribed to.
await xapi.status.get("Audio Volume");
xapi.status.on("Audio Volume", listener);
xapi.status.once("Audio Volume", listener);

// Passing only a listener subscribes at the root.
xapi.status.on(listener);
xapi.status.once(listener);

// Config paths can be read, set, and subscribed to.
await xapi.config.get("Audio DefaultVolume");
await xapi.config.set("Audio DefaultVolume", 70);
xapi.config.on("Audio DefaultVolume", listener);
xapi.config.once("Audio DefaultVolume", listener);

// Event paths can be subscribed to.
xapi.event.on("UserInterface Extensions Panel Clicked", listener);
xapi.event.once("UserInterface Extensions Panel Clicked", listener);

// Path arguments can also use arrays for indexed paths.
await xapi.status.get(["Call", 1, "Status"]);

// Old style functions expose Jest helpers too.
xapi.status.get.mockResolvedValueOnce(55);
xapi.command.mockResolvedValueOnce("Dial", result);

Additional runtime surface

// Read schema-derived docs for status, config, command, and event paths.
await xapi.doc("Status Audio Volume");

// The mocked xapi version defaults to 6.0.0.
xapi.version;

Test-only mock controls

The mock has two kinds of test controls:

// State and event helpers set mock xAPI values or emit xAPI updates.
xapi.Status.Audio.Volume.set(30);

// Jest mock function controls override or inspect mocked xAPI calls.
xapi.Command.Dial.mockResolvedValueOnce(result);
State and event helpers

Use these when a test needs to prepare mock device state or simulate an xAPI update that should notify macro listeners.

// Set a status value and notify matching status listeners.
xapi.Status.Audio.Volume.set(30);

// Set a config value and notify matching config listeners.
await xapi.Config.Audio.DefaultVolume.set(70);

// Emit an event payload to matching event listeners.
xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
  PanelId: "speed-dial-panel",
});

// Remove a status branch and emit the RoomOS-style ghost payload for indexed branches.
xapi.Status.Call[7].remove();

// Reset values, listeners, command overrides, and Jest mock call counts.
xapi.reset();
xapi.setStatus("Audio Volume", 30);

await xapi.config.set("Audio DefaultVolume", 70);
await xapi.setConfig("Audio DefaultVolume", 70);

xapi.emitEvent("UserInterface Extensions Panel Clicked", {
  PanelId: "speed-dial-panel",
});

xapi.removeStatus("Call 7");
Jest mock function controls

Use these when a test needs normal Jest mock behavior, such as asserting calls, inspecting call history, or setting a one-off command response.

// New style command paths use Jest helpers directly.
xapi.Command.Dial.mockResolvedValueOnce(result);

// New style operation functions expose Jest helpers too.
xapi.Status.Audio.Volume.get.mockResolvedValueOnce(55);
xapi.Event.UserInterface.Extensions.Panel.Clicked.on.mockImplementationOnce(
  handler,
);

// Lower-level helpers can override shared command behavior.
xapi.setCommandResult("Dial", result);
xapi.setCommandHandler("Dial", handler);
// Mixed old/new style calls share cached path-level mocks.
await xapi.command("Dial", params);
expect(xapi.Command.Dial).toHaveBeenCalledWith(params);

await xapi.status.get("Audio Volume");
expect(xapi.Status.Audio.Volume.get).toHaveBeenCalled();

// Old style functions expose Jest helpers on the function being called.
xapi.status.get.mockResolvedValueOnce(55);
xapi.event.on.mockImplementationOnce(handler);

// Old style commands use the same helper names with the path first.
xapi.command.mockImplementationOnce("Dial", handler);
xapi.command.mockResolvedValueOnce("Dial", value);
xapi.command.mockRejectedValueOnce("Dial", error);
xapi.command.mockReturnValueOnce("Dial", value);

Jest mock APIs

The mock exposes Jest's mock-function API on mocked xAPI functions. Use these APIs when you want to control a mocked function result or assert how a macro called xAPI:

// Call assertions use normal Jest mock matchers.
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(...args);
expect(fn).toHaveBeenNthCalledWith(1, ...args);

// Call inspection uses the standard Jest mock fields and helpers.
fn.mock;
fn.mock.calls;
fn.mock.results;
fn.getMockName();
fn.getMockImplementation();

// Reset and naming helpers are available on mocked xAPI functions.
fn.mockClear();
fn.mockReset();
fn.mockRestore();
fn.mockName("descriptive mock name");
fn.mockReturnThis();

// Implementation helpers support temporary command/status/config/event behavior.
fn.mockImplementation(handler);
fn.mockImplementationOnce(handler);
fn.withImplementation(handler, callback);

// Result helpers support sync, resolved, and rejected mock values.
fn.mockReturnValue(value);
fn.mockReturnValueOnce(value);
fn.mockResolvedValue(value);
fn.mockResolvedValueOnce(value);
fn.mockRejectedValue(value);
fn.mockRejectedValueOnce(value);

For new style command paths, call the Jest helper directly on the command path:

xapi.Command.Dial.mockResolvedValueOnce({
  dialed: "[email protected]",
});

Use the same Jest helper names with the old style command path as the first argument:

xapi.command.mockResolvedValueOnce("Dial", {
  dialed: "[email protected]",
});

The standard Jest form is still available on xapi.command too. When called without a path, it applies to the next xapi.command(...) invocation regardless of command path.

Promise-returning APIs resolve or reject like the macro runtime. Subscriptions return an unsubscribe function, and once(...) listeners automatically unsubscribe after the first matching update.

Reset state between tests

Most test suites should reset values, mocks, listeners, and handlers before each test so one scenario does not leak into the next.

import { beforeEach, jest } from "@jest/globals";

beforeEach(async () => {
  jest.resetModules();
  const { default: xapi } = await import("xapi");
  xapi.reset();
});

Use RoomOS API in tests

New style paths keep tests visually close to the xAPI paths used in RoomOS macros. Command paths are Jest mock functions, so tests can use Jest mock helpers for one-off responses. The mock also adds test helpers to new style paths: status paths support .set(...), event paths support .emit(...), and config paths use the normal RoomOS .set(...) API.

import { expect, it, jest } from "@jest/globals";

it("uses new style xAPI helpers", async () => {
  const { default: xapi } = await import("xapi");
  const volumeHandler = jest.fn();

  xapi.Command.Dial.mockImplementationOnce(async (params) => ({
    dialed: params.Number,
  }));
  xapi.Status.Audio.Volume.on(volumeHandler);
  xapi.Status.Audio.Volume.set(30);

  await expect(
    xapi.Command.Dial({ Number: "[email protected]" }),
  ).resolves.toEqual({ dialed: "[email protected]" });
  await expect(xapi.Status.Audio.Volume.get()).resolves.toBe(30);
  expect(volumeHandler).toHaveBeenCalledWith(30);
});

The mock also supports the commonly used lowercase macro APIs. String paths use the same spaced format shown in the public RoomOS documentation, and these calls return promises like the RoomOS runtime.

import { expect, it } from "@jest/globals";

it("uses old style xAPI helpers", async () => {
  const { default: xapi } = await import("xapi");

  xapi.command.mockImplementationOnce("Dial", async (params) => ({
    dialed: params.Number,
  }));
  xapi.setStatus("Audio Volume", 30);

  await expect(
    xapi.command("Dial", { Number: "[email protected]" }),
  ).resolves.toEqual({ dialed: "[email protected]" });
  await expect(xapi.status.get("Audio Volume")).resolves.toBe(30);
});

New style paths avoid string path parsing.

await xapi.Status.Audio.Volume.get();

await xapi.Status.Call[1].Status.get();

Old style helpers normalize paths before lookup and call tracking. Strings normally use spaces, empty path segments are removed, string segments are capitalized, and numeric strings are converted to numbers.

await xapi.status.get("Audio Volume"); // ["Audio", "Volume"]

await xapi.status.get(["Call", "1", "Status"]); // ["Call", 1, "Status"]

The visible examples below use new style syntax. When an old style equivalent is useful, it appears in an expandable details block.

Set values and trigger xAPI updates

The mock lets tests update status and config values while notifying the same listeners a macro would register at runtime.

import { expect, it, jest } from "@jest/globals";

it("sets status values and notifies xStatus listeners", async () => {
  const { default: xapi } = await import("xapi");
  const handler = jest.fn();

  xapi.Status.Audio.Volume.on(handler);

  xapi.Status.Audio.Volume.set(20);

  await expect(xapi.Status.Audio.Volume.get()).resolves.toBe(20);

  expect(handler).toHaveBeenCalledWith(20);
});
xapi.status.on("Audio Volume", handler);
xapi.setStatus("Audio Volume", 20);
await expect(xapi.status.get("Audio Volume")).resolves.toBe(20);

Config paths already have a RoomOS .set(...) API. In the mock, setting a config value also notifies matching config listeners.

import { expect, it, jest } from "@jest/globals";

it("sets config values and notifies config listeners", async () => {
  const { default: xapi } = await import("xapi");
  const handler = jest.fn();

  xapi.Config.Audio.DefaultVolume.on(handler);

  await xapi.Config.Audio.DefaultVolume.set(100);

  await expect(xapi.Config.Audio.DefaultVolume.get()).resolves.toBe(100);

  expect(handler).toHaveBeenCalledWith(100);
});
xapi.config.on("Audio DefaultVolume", handler);
await xapi.config.set("Audio DefaultVolume", 100);
await expect(xapi.config.get("Audio DefaultVolume")).resolves.toBe(100);

Command paths are Jest mock functions, so a test can set a command result directly on the new style command path.

import { expect, it } from "@jest/globals";

it("mocks a command response", async () => {
  const { default: xapi } = await import("xapi");

  xapi.Command.Dial.mockImplementationOnce(async (params) => ({
    dialed: params.Number,
  }));

  await expect(
    xapi.Command.Dial({ Number: "[email protected]" }),
  ).resolves.toEqual({ dialed: "[email protected]" });
});
xapi.command.mockImplementationOnce("Dial", async (params) => ({
  dialed: params.Number,
}));

await expect(
  xapi.command("Dial", { Number: "[email protected]" }),
).resolves.toEqual({ dialed: "[email protected]" });

The same pattern works for the common Jest result helpers:

xapi.Command.Dial.mockResolvedValueOnce({ status: "dialed" });

xapi.Command.Dial.mockRejectedValueOnce({ code: 4, message: "Invalid" });

xapi.Command.Dial.mockReturnValueOnce(Promise.resolve({ status: "queued" }));
xapi.command.mockResolvedValueOnce("Dial", { status: "dialed" });
xapi.command.mockRejectedValueOnce("Dial", {
  code: 4,
  message: "Invalid",
});
xapi.command.mockReturnValueOnce("Dial", { status: "queued" });

Event payloads can be emitted in the same new style shape used to subscribe to them.

import { expect, it, jest } from "@jest/globals";

it("emits event payloads", async () => {
  const { default: xapi } = await import("xapi");
  const handler = jest.fn();

  xapi.Event.UserInterface.Extensions.Panel.Clicked.on(handler);

  xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
    PanelId: "speed-dial-panel",
  });

  expect(handler).toHaveBeenCalledWith({
    PanelId: "speed-dial-panel",
  });
});
xapi.event.on("UserInterface Extensions Panel Clicked", handler);
xapi.emitEvent("UserInterface Extensions Panel Clicked", {
  PanelId: "speed-dial-panel",
});

Read schema docs

xapi.doc(...) returns schema-derived documentation for rooted status, config, command, and event paths. You can use either Config or Configuration for configuration docs.

import { expect, it } from "@jest/globals";

it("reads xapi docs", async () => {
  const { default: xapi } = await import("xapi");

  await expect(xapi.doc("Status Audio Volume")).resolves.toEqual(
    expect.objectContaining({
      ValueSpace: expect.objectContaining({
        type: "Integer",
      }),
    }),
  );

  await expect(xapi.doc("Config SystemUnit Name")).resolves.toEqual(
    expect.objectContaining({
      ValueSpace: expect.objectContaining({
        type: "String",
      }),
    }),
  );
});

Assert xCommand calls

Commands are Jest mocks, so you can assert on them with normal Jest matchers.

import { expect, it } from "@jest/globals";

it("dials the requested destination", async () => {
  const { default: xapi } = await import("xapi");

  await someMacroFunction();

  expect(xapi.Command.Dial).toHaveBeenCalledWith({
    Number: "[email protected]",
  });
});
expect(xapi.command).toHaveBeenCalledWith("Dial", {
  Number: "[email protected]",
});
  • Default command response: valid commands resolve with { status: "OK" }.
  • Invalid command paths: reject with the same code/message shape used by RoomOS.
  • Parameter validation: schema-backed commands validate required parameters and value ranges.
    • String length: MinLength / MaxLength use UTF-8 byte length, matching RoomOS over WebSocket.
    • XML parsing: UserInterface Extensions Panel Save requires one <Panel> inside <Extensions>.
    • RoomOS-style errors: panel XML errors match messages such as Failed to parse xml and Expected a single Panel, got 0.
  • HTTP Client:
    • Config enforcement: honors Config.HttpClient.Mode, AllowHTTP, and AllowInsecureHTTPS.
    • Default mode: RoomOS defaults Mode to Off, so set xapi.Config.HttpClient.Mode to "On" before using default mock responses.
    • Response body defaults: Get returns PlainText; Post, Put, Patch, and Delete default to None.
    • Connection limit: more than three concurrent requests reject with No available http connections.
    • Response helpers: setHttpClientResponse(...) can set status, headers, body, errors, and delayMs.
  • Customise command response: use Jest helpers to set your own response.
    • mockResolvedValueOnce(...)
    • mockImplementationOnce(...)

Mock utilities

Use the mock utilities to prepare state, emit updates, override command responses, and assert how the macro used xAPI with Jest mock matchers.

import { expect, it } from "@jest/globals";

it("prepares state and asserts command calls", async () => {
  const { default: xapi } = await import("xapi");

  xapi.Status.Audio.Volume.set(20);

  await xapi.Config.Audio.DefaultVolume.set(100);

  xapi.Command.Dial.mockImplementationOnce(async (params) => ({
    dialed: params.Number,
  }));

  await xapi.Command.Dial({ Number: "[email protected]" });

  await xapi.Status.Audio.Volume.get();

  expect(xapi.Command.Dial).toHaveBeenCalledWith({
    Number: "[email protected]",
  });
});
xapi.setStatus("Audio Volume", 20);
await xapi.config.set("Audio DefaultVolume", 100);

xapi.command.mockImplementationOnce("Dial", async (params) => ({
  dialed: params.Number,
}));

await xapi.command("Dial", { Number: "[email protected]" });
await xapi.status.get("Audio Volume");

expect(xapi.command).toHaveBeenCalledWith("Dial", {
  Number: "[email protected]",
});

Available helpers:

State and event helpers:

// Update status state and notify matching listeners.
xapi.Status.Audio.Volume.set(20);

// Update config state and notify matching listeners.
await xapi.Config.Audio.DefaultVolume.set(100);

// Emit event payloads to matching listeners.
xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
  PanelId: "speed-dial-panel",
});

// Remove a status branch and emit the RoomOS-style ghost payload.
xapi.Status.Call[7].remove();
xapi.setStatus("Audio Volume", 20);

await xapi.config.set("Audio DefaultVolume", 100);
await xapi.setConfig("Audio DefaultVolume", 100);

xapi.emitEvent("UserInterface Extensions Panel Clicked", {
  PanelId: "speed-dial-panel",
});

xapi.removeStatus("Call 7");

Jest mock function helpers:

// Set command behavior directly on new style command paths.
xapi.Command.Dial.mockImplementationOnce(handler);
xapi.Command.Dial.mockResolvedValueOnce(value);
xapi.Command.Dial.mockRejectedValueOnce(error);
xapi.Command.Dial.mockReturnValueOnce(value);
// Old style calls share cached path-level mocks.
await xapi.command("Dial", params);
expect(xapi.Command.Dial).toHaveBeenCalledWith(params);

await xapi.status.get("Audio Volume");
expect(xapi.Status.Audio.Volume.get).toHaveBeenCalled();

// Old style command helpers use the command path first.
xapi.command.mockImplementationOnce("Dial", handler);
xapi.command.mockResolvedValueOnce("Dial", value);
xapi.command.mockRejectedValueOnce("Dial", error);
xapi.command.mockReturnValueOnce("Dial", value);

Lower-level command helpers:

// Apply one command override to every command call path.
xapi.setCommandResult("Dial", result);
xapi.setCommandHandler("Dial", handler);

// Mock HttpClient responses with RoomOS success/error behavior.
await xapi.Config.HttpClient.Mode.set("On");
xapi.setHttpClientResponse("Get", {
  statusCode: 404,
  delayMs: 25,
  body: "not found",
  headers: { "content-type": "text/plain" },
});

await expect(
  xapi.Command.HttpClient.Get({
    ResultBody: "None",
    Url: "https://example.test/missing",
  }),
).rejects.toEqual({
  code: 1,
  message: "Command returned an error.",
  data: {
    Headers: [{ Key: "content-type", Value: "text/plain", id: "1" }],
    StatusCode: "404",
  },
});

Reset helper:

// Clear values, handlers, listeners, and Jest mock call counts.
xapi.reset();

Use RoomOS runtime globals

The mock installs the RoomOS _main_module_name() global when jest-mock-xapi is loaded. It returns the name of the calling macro file without the source extension, matching the RoomOS behavior used by self-managing macros.

import xapi from "xapi";

const macroName = _main_module_name();

xapi.Command.Macros.Macro.Deactivate({ Name: macroName });
xapi.command("Macros Macro Deactivate", { Name: macroName });

For example, calling _main_module_name() from self-deactivating-macro.js returns "self-deactivating-macro".

Set leaf status and config values

Use new style .set(...) calls to prepare mock device state before importing a macro or invoking a handler. These forms notify matching listeners.

import { expect, it } from "@jest/globals";

it("reads the prepared default volume", async () => {
  const { default: xapi } = await import("xapi");

  await xapi.Config.Audio.DefaultVolume.set(100);

  xapi.Status.Audio.Volume.set(20);

  await expect(xapi.Config.Audio.DefaultVolume.get()).resolves.toBe(100);

  await expect(xapi.Status.Audio.Volume.get()).resolves.toBe(20);

  await xapi.Config.Audio.DefaultVolume.set(0);

  xapi.Status.Audio.Volume.set(25);

  await expect(xapi.Config.Audio.DefaultVolume.get()).resolves.toBe(0);

  await expect(xapi.Status.Audio.Volume.get()).resolves.toBe(25);
});
await xapi.config.set("Audio DefaultVolume", 100);
xapi.setStatus("Audio Volume", 20);
await expect(xapi.config.get("Audio DefaultVolume")).resolves.toBe(100);
await expect(xapi.status.get("Audio Volume")).resolves.toBe(20);

await xapi.config.set("Audio DefaultVolume", 0);
xapi.setStatus("Audio Volume", 25);
await expect(xapi.config.get("Audio DefaultVolume")).resolves.toBe(0);
await expect(xapi.status.get("Audio Volume")).resolves.toBe(25);

Select a RoomOS product

The mock defaults Status.SystemUnit.ProductPlatform to "Desk Pro" and applies Desk Pro product-specific xAPI availability even when a test has not explicitly set a product. Set Status.SystemUnit.ProductPlatform to another public product name when a test should model a different device.

Once a known product is selected, the mock uses the newest bundled RoomOS major-release schema that supports that product. It rejects xAPI paths that are not available on that product and validates product-specific configuration values.

import { expect, it } from "@jest/globals";

it("handles Desk Pro xAPI differences", async () => {
  const { default: xapi } = await import("xapi");

  xapi.Status.SystemUnit.ProductPlatform.set("Desk Pro");

  await xapi.Config.Video.Output.Connector[1].MonitorRole.set("Auto");

  await expect(
    xapi.Config.Video.Output.Connector[3].MonitorRole.set("Auto"),
  ).rejects.toEqual({
    code: 3,
    message: "No match on address expression",
  });

  await expect(
    xapi.Config.Video.Output.Connector[1].MonitorRole.set("PresentationOnly"),
  ).rejects.toEqual({
    code: 4,
    message: "Invalid or missing parameters",
  });
});
xapi.setStatus("SystemUnit ProductPlatform", "Desk Pro");
await xapi.config.set("Video Output Connector 1 MonitorRole", "Auto");

await expect(
  xapi.config.set("Video Output Connector 3 MonitorRole", "Auto"),
).rejects.toEqual({
  code: 3,
  message: "No match on address expression",
});

await expect(
  xapi.config.set("Video Output Connector 1 MonitorRole", "PresentationOnly"),
).rejects.toEqual({
  code: 4,
  message: "Invalid or missing parameters",
});

The selected schema also provides default software statuses. For example, a schema named 26.5.1 April 2026 produces these default values unless the test overrides them with new style .set(...) or setStatus(...):

Status SystemUnit Software DisplayName: "RoomOS 26.5.1.1 123456789"
Status SystemUnit Software Version: "ce26.5.1.1.123456789"

Compatibility note: tests that previously relied on the old unrestricted default may need to set Status.SystemUnit.ProductPlatform to the product they intend to model.

Read full status or config branches

The mock supports aggregate get() calls on root paths and indexed branches, similar to the real xAPI module.

import { expect, it } from "@jest/globals";

it("returns full config branches", async () => {
  const { default: xapi } = await import("xapi");

  await xapi.Config.Video.Output.Connector[1].MonitorRole.set("First");

  await xapi.Config.Video.Output.Connector[2].MonitorRole.set("Second");

  await expect(xapi.Config.get()).resolves.toHaveProperty("Audio");

  await expect(xapi.Config.Video.Output.Connector[1].get()).resolves.toEqual(
    expect.objectContaining({
      id: "1",
      MonitorRole: "First",
    }),
  );

  await expect(xapi.Config.Video.Output.Connector.get()).resolves.toEqual([
    expect.objectContaining({ id: "1", MonitorRole: "First" }),
    expect.objectContaining({ id: "2", MonitorRole: "Second" }),
  ]);

  await expect(xapi.Config.Video.Output.Connector["*"].get()).resolves.toEqual([
    expect.objectContaining({ id: "1", MonitorRole: "First" }),
    expect.objectContaining({ id: "2", MonitorRole: "Second" }),
  ]);
});
await xapi.config.set("Video Output Connector 1 MonitorRole", "First");
await xapi.config.set("Video Output Connector 2 MonitorRole", "Second");

await expect(xapi.config.get()).resolves.toHaveProperty("Audio");

await expect(xapi.config.get("Video Output Connector 1")).resolves.toEqual(
  expect.objectContaining({
    id: "1",
    MonitorRole: "First",
  }),
);

await expect(xapi.config.get("Video Output Connector")).resolves.toEqual([
  expect.objectContaining({ id: "1", MonitorRole: "First" }),
  expect.objectContaining({ id: "2", MonitorRole: "Second" }),
]);

await expect(xapi.config.get("Video Output Connector *")).resolves.toEqual([
  expect.objectContaining({ id: "1", MonitorRole: "First" }),
  expect.objectContaining({ id: "2", MonitorRole: "Second" }),
]);

Emit xEvent payloads

Use emit() on event paths to simulate the same payloads a RoomOS device would send to a macro.

import { expect, it } from "@jest/globals";

it("reacts to a panel press", async () => {
  const { default: xapi } = await import("xapi");

  await import("./my-macro.js");

  xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
    PanelId: "speed-dial-panel",
  });

  expect(xapi.Command.Dial).toHaveBeenCalledWith({
    Number: "[email protected]",
  });
});
xapi.emitEvent("UserInterface Extensions Panel Clicked", {
  PanelId: "speed-dial-panel",
});

expect(xapi.command).toHaveBeenCalledWith("Dial", {
  Number: "[email protected]",
});

Subscribe to leaf updates

Leaf subscriptions behave like the real macro API and receive the updated value directly.

import { expect, it, jest } from "@jest/globals";

it("notifies leaf status listeners", async () => {
  const { default: xapi } = await import("xapi");
  const handler = jest.fn();

  xapi.Status.Audio.Volume.on(handler);

  xapi.Status.Audio.Volume.set(55);

  xapi.Status.Audio.Volume.set(56);

  expect(handler).toHaveBeenNthCalledWith(1, 55);
  expect(handler).toHaveBeenNthCalledWith(2, 56);
});
xapi.status.on("Audio Volume", handler);
xapi.setStatus("Audio Volume", 55);
xapi.setStatus("Audio Volume", 56);

Subscribe to root or branch updates

Root and branch listeners receive a nested payload scoped to the changed branch.

import { expect, it, jest } from "@jest/globals";

it("notifies root listeners with relative path payloads", async () => {
  const { default: xapi } = await import("xapi");
  const statusHandler = jest.fn();
  const configHandler = jest.fn();
  const eventHandler = jest.fn();

  xapi.Status.on(statusHandler);

  xapi.Config.on(configHandler);

  xapi.Event.on(eventHandler);

  xapi.Status.Audio.Volume.set(55);

  await xapi.Config.Audio.DefaultVolume.set(100);

  xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
    PanelId: "speed-dial-panel",
  });

  xapi.Status.Audio.Volume.set(56);

  await xapi.Config.Audio.DefaultVolume.set(0);

  xapi.Event.UserInterface.Extensions.Panel.Clicked.emit({
    PanelId: "speed-dial-panel-2",
  });

  expect(statusHandler).toHaveBeenCalledWith({
    Audio: {
      Volume: 55,
    },
  });
  expect(configHandler).toHaveBeenCalledWith({
    Audio: {
      DefaultVolume: 100,
    },
  });
  expect(eventHandler).toHaveBeenCalledWith({
    UserInterface: {
      Extensions: {
        Panel: {
          Clicked: {
            PanelId: "speed-dial-panel",
          },
        },
      },
    },
  });
  expect(statusHandler).toHaveBeenCalledWith({
    Audio: {
      Volume: 56,
    },
  });
  expect(configHandler).toHaveBeenCalledWith({
    Audio: {
      DefaultVolume: 0,
    },
  });
  expect(eventHandler).toHaveBeenCalledWith({
    UserInterface: {
      Extensions: {
        Panel: {
          Clicked: {
            PanelId: "speed-dial-panel-2",
          },
        },
      },
    },
  });
});
xapi.status.on(statusHandler);
xapi.config.on(configHandler);
xapi.event.on(eventHandler);

xapi.setStatus("Audio Volume", 55);
await xapi.config.set("Audio DefaultVolume", 100);
xapi.emitEvent("UserInterface Extensions Panel Clicked", {
  PanelId: "speed-dial-panel",
});

xapi.setStatus("Audio Volume", 56);
await xapi.config.set("Audio DefaultVolume", 0);
xapi.emitEvent("UserInterface Extensions Panel Clicked", {
  PanelId: "speed-dial-panel-2",
});

Track indexed status branches such as calls

Indexed collection listeners such as xapi.Status.Call.on(...) receive the full branch snapshot plus the branch id.

import { expect, it, jest } from "@jest/globals";

it("notifies call listeners as a call branch changes", async () => {
  const { default: xapi } = await import("xapi");
  const handler = jest.fn();

  xapi.Status.Call.on(handler);

  xapi.Status.Call[42].Direction.set("Outgoing");

  xapi.Status.Call[42].Status.set("Connected");

  expect(handler).toHaveBeenNthCalledWith(1, {
    Direction: "Outgoing",
    id: "42",
  });
  expect(handler).toHaveBeenNthCalledWith(2, {
    Direction: "Outgoing",
    Status: "Connected",
    id: "42",
  });
});
xapi.status.on("Call", handler);
xapi.setStatus("Call 42 Direction", "Outgoing");
xapi.setStatus("Call 42 Status", "Connected");

Remove indexed status branches

Use new style .remove() to simulate an indexed status branch disappearing, such as a call ending.

import { expect, it, jest } from "@jest/globals";

it("emits a ghost payload when a call ends", async () => {
  const { default: xapi } = await import("xapi");
  const handler = jest.fn();

  xapi.Status.Call.on(handler);

  xapi.Status.Call[7].Direction.set("Incoming");

  xapi.Status.Call[7].Status.set("Connected");

  xapi.Status.Call[7].remove();

  expect(handler).toHaveBeenLastCalledWith({
    ghost: "true",
    id: "7",
  });
});
xapi.status.on("Call", handler);
xapi.setStatus("Call 7 Direction", "Incoming");
xapi.setStatus("Call 7 Status", "Connected");
xapi.removeStatus("Call 7");

Demo

Here are some example macros where jest-mock-xapi is used to validate their functions:

Run all examples against the current unreleased parent package build with:

npm run examples:test

Manual RoomOS hardware parity check

This repository also includes a local-only hardware parity script that connects to real RoomOS devices with jsxapi, runs the same representative xAPI calls against each device and a fresh jest-mock-xapi instance, then compares the response formats. The probe includes xapi.doc(...) status, config, command, and event paths using the public spaced path style, plus array path forms. It also checks invalid path errors, invalid command argument errors, and successful command responses. It is not part of npm test or prepublishOnly.

Validated hardware is generated by npm run parity:devices after all live-device checks pass. The table records the public RoomOS schema used for comparison, not the exact software build running on the tested device.

| Hardware | RoomOS major | Tested schema | Result | Last validated | | --- | --- | --- | --- | --- | | Board 70 | RoomOS 11 | RoomOS 11.33.1 | 53/53 passed | 2026-05-05 | | Codec Pro | RoomOS 26 | RoomOS 26.5.1 | 53/53 passed | 2026-05-05 | | Desk Pro | RoomOS 26 | RoomOS 26.5.1 | 53/53 passed | 2026-05-05 | | Room Bar Pro | RoomOS 26 | RoomOS 26.5.1 | 53/53 passed | 2026-05-05 |

Create a local .env from .env.example:

cp .env.example .env

Add the shared credentials and an address array:

ROOMOS_PARITY_USERNAME=admin
ROOMOS_PARITY_PASSWORD=password
ROOMOS_PARITY_ADDRESSES='["192.0.2.10","192.0.2.11"]'
ROOMOS_PARITY_UPDATE_README=true

The script tries wss:// for each address first, then retries with ssh:// if WSS fails. Test output uses the detected SystemUnit ProductPlatform value for each connected device rather than a configured device name. The legacy ROOMOS_PARITY_DEVICES='[...]' JSON array and ROOMOS_PARITY_DEVICE_1_* numbered blocks are still supported for existing local files.

Run the manual check with:

npm run parity:devices

The command probe is enabled by default; it sends Message Send payloads, validates hidden Panel Save XML body cases, saves and removes a hidden UI extension panel, and displays a short alert on each device. Set ROOMOS_PARITY_INCLUDE_COMMAND=false to skip it. ROOMOS_PARITY_INCLUDE_CONFIG_SET=false is the default because that probe writes the current SystemUnit Name value back to the device. Set ROOMOS_PARITY_CONNECT_TIMEOUT_MS and ROOMOS_PARITY_PROBE_TIMEOUT_MS to tune connection and per-probe timeouts. Set ROOMOS_PARITY_UPDATE_README=false when you want to run parity locally without changing the generated hardware validation table.

Update the bundled RoomOS schema

The package uses the same schema resources as roomos.cisco.com. The generated schema files are ignored by git, but local scripts that need them run schema:ensure first and fetch them when missing. Published packages still include the pruned schema catalog in dist, so consumers do not need network access at runtime.

To create the local schema cache only when it is missing, run:

npm run schema:ensure

To force-refresh the local schema cache from the upstream index, run:

npm run schema:update

By default this selects the newest schema for each major RoomOS release line from schemas.json, such as the latest 9.x, 10.x, 11.x, and 26.x schemas. At runtime, jest-mock-xapi uses Status.SystemUnit.ProductPlatform to pick the newest bundled major schema that supports that product. To pin a single upstream schema for debugging, set ROOMOS_SCHEMA_NAME, for example:

ROOMOS_SCHEMA_NAME="26.5.1 April 2026" npm run schema:update

Publishing runs schema:update before building so the published package includes the current pruned schema catalog.

Local development commands

  • npm test ensures the schema cache exists and runs the Jest suite.
  • npm run build ensures the schema cache exists, compiles TypeScript, and copies schemas into dist.
  • npm run schema:ensure fetches schemas only when the local ignored cache is missing.
  • npm run schema:update refreshes the local ignored schema cache from the upstream RoomOS schema index.
  • npm run parity:devices builds the package, runs the live-device parity probe, and updates the README validation table when enabled.

License

All contents are licensed under the MIT license. Please see license for details.

Disclaimer

Everything included is for demo and Proof of Concept purposes only. Use of the site is solely at your own risk. This site may contain links to third party content, which we do not warrant, endorse, or assume liability for. These demos are for Cisco Webex use cases, but are not official Cisco Webex branded demos.

Questions

Please contact the WXSD team at [email protected] for questions. Or, if you're a Cisco internal employee, reach out to us on the Webex App via our bot ([email protected]). In the Engagement Type field, choose API/SDK Proof of Concept Integration Development to make sure you reach our team.