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 testimport 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
| Method | What 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
| Property | Holds |
|---|---|
view | The last view: { compact, expanded } |
texts(node?) | Every text in the view |
find(type, where?) | The first matching node, or null |
storage | What storage holds now |
requests | Requests made to Adanotch: { method, params } |
notifications, activities | notify.island and island.show calls |
logs, errors | Console lines, and errors from events and timers |
opened, clipboard | What 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.