Ada Extensions
Guides

Testing

Test your extension in Node, on Adanotch's own runtime, without touching your Mac.

The adanotch init template includes a test kit: adanotch/testing.js. It bundles your extension with esbuild and runs it in Node on the same runtime Adanotch uses. You supply what Adanotch would answer (system data, fetch, media…).

npm test
test/index.test.ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { loadExtension } from "../adanotch/testing.js";

test("CPU shows beside the notch", async () => {
  const ext = await loadExtension({
    mocks: { "system.stats": () => ({ cpuPercent: 42, ramPercent: 50, usedMemoryGB: 8, totalMemoryGB: 16 }) },
  });
  assert.equal(ext.view.compact?.trailing?.value, "42%");
});

loadExtension starts the extension and waits for setup and the first update. It rejects if either throws.

Options

Prop

Type

Driving the extension

MethodWhat it does
update()Runs update
action(name, value?)onAction, then update
setSettings(values)onSettingsChange, then update
emit(event, payload?)Sends an event; fails if the extension doesn't listen to it
command(name, data?)Like adanotch ext <id> <name> [data]
advance(ms)Moves the fake clock forward; timers that come due fire
settle()Waits for pending promises, requests and renders
stop()Stops it

Reading the result

PropertyHolds
viewThe last view: { compact, expanded }
texts(node?)Every text in the view
find(type, where?)The first matching node, or null
storageWhat storage holds now
requestsRequests made to Adanotch: { method, params }
notifications, activitiesnotify.island and island.show calls
logs, errorsConsole lines, and errors from events and timers
opened, clipboardWhat open() opened, what the clipboard holds now

Examples

fetch

test("shows the temperature", async () => {
  const ext = await loadExtension({
    settings: { city: "Ankara" },
    mocks: {
      fetch: (req) => {
        assert.match(req.url, /api\.open-meteo\.com/);
        return { body: { current: { temperature_2m: 18.4 } } };
      },
    },
  });
  assert.ok(ext.texts().includes("18°"));
});

test("shows the last value on a network error", async () => {
  const ext = await loadExtension({
    storage: { last: { temp: 21 } },
    mocks: {
      fetch: () => {
        const error = new Error("offline");
        error.name = "NetworkError";
        throw error;
      },
    },
  });
  assert.ok(ext.texts().includes("21°"));
});

When a mock throws, the error's name picks the class the extension sees: NetworkError, TimeoutError, PermissionError.

Timers

test("notifies after 25 minutes", async () => {
  const ext = await loadExtension();
  await ext.action("start");
  await ext.advance(25 * 60_000);
  assert.equal(ext.notifications.at(-1)?.text, "Time for a break");
});

Events and permissions

test("the title follows the track", async () => {
  let track = { title: "A", artist: "X", album: "", duration: 200, elapsed: 0, isPlaying: true, app: "com.spotify.client" };
  const ext = await loadExtension({ mocks: { "media.nowPlaying": () => track } });
  track = { ...track, title: "B" };
  await ext.emit("media.change", track);
  await ext.settle();
  assert.ok(ext.texts().includes("B"));
});

test("explains when media access is off", async () => {
  const ext = await loadExtension({ permissions: [] });
  assert.ok(ext.texts().includes("Media access is off"));
});

Error lines

Stacks in tests go through the source map too, so they read src/index.ts:12:5.

On this page