Class: PluginTestHost<T>
Runs your plugin class in a test, without a Nexus server: use it in every plugin's test suite.
It drives the plugin through the same PluginRuntime that runs it in a worker, so config
validation, sensors, actuators, catalog, panel, actions and webhooks behave as in production, and
it records everything the plugin sends. Always await host.stop() at the end (in finally),
or the plugin's timers keep the test process alive.
Example
Start the smart plug plugin against a fake device, read a sensor, switch it, stop:
const clock = new TestClock()
const host = await PluginTestHost.start(SmartPlugPlugin, { config: { address }, clock })
try {
const power = await host.addSensor("meter", { reading: "power" })
assert.deepEqual(host.values(power), [Value.number(0)])
const button = await host.addActuator("switch")
await host.actuate(button, Value.boolean(true))
await clock.advance(5000) // the next poll
} finally {
await host.stop()
}
Type Parameters
T
T extends PluginClass = PluginClass
Properties
sent
readonlysent: ({type:"hello";version:number;features?:string[]; } | {type:"ready"; } | {type:"failed";error:string; } | {type:"emit";signalId:string;value:Value;causeId?:string; } | {type:"status";status:"running"|"degraded";message?:string; } | {type:"log";level:"error"|"warn"|"info"|"debug";message:string;data?:unknown;causeId?:string; } | {type:"store.set";key:string;value:unknown; } | {type:"store.delete";key:string; } | {type:"config.migrated";config:unknown;version:number; } | {type:"panel.changed"; } | {type:"action.progress";requestId:number;text:string;fraction?:number; } | {type:"response";requestId:number;ok:boolean;result?:unknown;error?:string; } | {type:"signal.error";signalId:string;error:string; } | {type:"signal.recovered";signalId:string; } | {type:"watch.ack";seq:number; } | {type:"pong";seq:number; } | {type:"stopped"; })[] =[]
every message the plugin sent, in order: emits, logs ({ type: "log", level, message, data? }),
status changes, store writes and responses. Assert on it for what no other member shows, such
as a log line.
store
readonlystore:Record<string,unknown>
the store as the host would have persisted it; updated by the plugin's set() and delete(), read it back in assertions
Accessors
migratedConfig
Get Signature
get migratedConfig(): {
config:unknown;version:number; } |undefined
the config init migrated and reported to be stored, if it did
Returns
{ config: unknown; version: number; } | undefined
plugin
Get Signature
get plugin():
InstanceType<T>
the plugin instance, typed as its class, for reaching into it from a test
Returns
InstanceType<T>
status
Get Signature
get status(): {
status:"running"|"degraded";message:string|undefined; } | {message?:undefined;status:"running"; }
the last status the plugin reported, "running" if none
Returns
{ status: "running" | "degraded"; message: string | undefined; } | { message?: undefined; status: "running"; }
Methods
start()
staticstart<T>(type,options?):Promise<PluginTestHost<T>>
Starts the plugin and waits for ready.
Type Parameters
T
T extends PluginClass
Parameters
type
T
options?
TestHostOptions = {}
Returns
Promise<PluginTestHost<T>>
Throws
Error with the plugin's message when init fails (invalid config, start threw)
checkConfig()
staticcheckConfig(type,config,configVersion?,store?):Promise<ConfigCheckResult>
What a host's settings check answers: issues at their fields, and the (migrated) config to
save; the plugin doesn't start. With store, also what the migration wrote to a copy of it.
Parameters
type
config
unknown
configVersion?
number
store?
Record<string, unknown>
Returns
Promise<ConfigCheckResult>
testConfig()
statictestConfig(type,config):Promise<string|undefined>
runs the plugin's Test connection with this config;
Parameters
type
config
unknown
Returns
Promise<string | undefined>
Throws
Error with its message, also for an invalid config
discover()
staticdiscover(type,field,config?):Promise<object[]>
runs the plugin's Find on the network for a config field; config is the form as filled in so far.
Parameters
type
field
string
config?
unknown
Returns
Promise<object[]>
Throws
Error with its failure
describe()
staticdescribe(type):Promise<{config:Record<string,unknown>;sensors:object[];actuators:object[];watches?: {label:string;description?:string; };actions?:object[];testable?:true;panelPush?:true;discoverable?:string[]; }>
the description the UI gets (config and params as JSON Schema), without starting the plugin; throws for invalid static declarations
Parameters
type
Returns
Promise<{ config: Record<string, unknown>; sensors: object[]; actuators: object[]; watches?: { label: string; description?: string; }; actions?: object[]; testable?: true; panelPush?: true; discoverable?: string[]; }>
addSensor()
addSensor(
capability,params?,label?):Promise<string>
Adds and starts a sensor, like the host does when the user configures a signal; the call resolves after the sensor's start(). Ids are generated ("sensor-1", "sensor-2", …).
Parameters
capability
string
params?
unknown = {}
checked against the capability's params schema, defaults applied; default {}
label?
string
the signal's name (this.signal.label); default "" like an older host
Returns
Promise<string>
the new signal's id;
Throws
Error for unknown capabilities, invalid params or a failing start
addActuator()
addActuator(
capability,params?,label?):Promise<string>
Adds an actuator like addSensor() does for a sensor.
Parameters
capability
string
params?
unknown = {}
label?
string
Returns
Promise<string>
the new signal's id; throws like addSensor()
remove()
remove(
id):Promise<void>
disposes the signal's sensor or actuator, as when the user deletes it; unknown ids are ignored
Parameters
id
string
Returns
Promise<void>
values()
values(
id):Value[]
every value the signal emitted so far
Parameters
id
string
Returns
Value[]
next()
next(
id,timeoutMs?):Promise<Value>
waits for the next value emitted after this call (earlier ones are in values());
Parameters
id
string
timeoutMs?
number = 2000
Returns
Promise<Value>
Throws
after timeoutMs
actuate()
actuate(
id,value,causeId?,replayed?):Promise<void>
runs an actuator;
Parameters
id
string
value
causeId?
string
the bus message it answers; emits and logs while it runs carry it
replayed?
boolean = false
delivered again by the host (a force after a restart), as run() sees in command.replayed
Returns
Promise<void>
Throws
Error with the message run() threw
catalog()
catalog():
Promise<CatalogNode[]>
calls Plugin.catalog()
Returns
Promise<CatalogNode[]>
panel()
panel():
Promise<{type:"keyValue";items:object[]; } | {type:"table";columns:string[];rows: (string| {text?:string;tone?:"error"|"ok"|"warn"|"info"|"muted";until?:number;since?:number;at?:number; })[][];rowActions?:object[][];rowCatalog?: (string|null)[]; } | {type:"qr";data:string;caption?:string; } | {type:"json";data:unknown; } | {type:"tiles";tiles:object[]; } | {type:"notice";tone:"error"|"ok"|"warn"|"info"|"muted";title?:string;text:string;link?: {label:string;href:string; }; } | {type:"sections";sections:object[]; } |undefined>
calls Plugin.panel(); rejects a panel the host would refuse, e.g. a javascript: link or 13 tiles
Returns
Promise<{ type: "keyValue"; items: object[]; } | { type: "table"; columns: string[]; rows: (string | { text?: string; tone?: "error" | "ok" | "warn" | "info" | "muted"; until?: number; since?: number; at?: number; })[][]; rowActions?: object[][]; rowCatalog?: (string | null)[]; } | { type: "qr"; data: string; caption?: string; } | { type: "json"; data: unknown; } | { type: "tiles"; tiles: object[]; } | { type: "notice"; tone: "error" | "ok" | "warn" | "info" | "muted"; title?: string; text: string; link?: { label: string; href: string; }; } | { type: "sections"; sections: object[]; } | undefined>
action()
action(
id,input?):Promise<string|undefined>
runs an action like the Run button does; resolves with its message, rejects with its failure
Parameters
id
string
input?
unknown = {}
Returns
Promise<string | undefined>
actionAnswer()
actionAnswer(
id,input?):Promise<string| {message?:string;panel?: {type:"keyValue";items:object[]; } | {type:"table";columns:string[];rows: (string| {text?:string;tone?:"error"|"ok"|"warn"|"info"|"muted";until?:number;since?:number;at?:number; })[][];rowActions?:object[][];rowCatalog?: (string|null)[]; } | {type:"qr";data:string;caption?:string; } | {type:"json";data:unknown; } | {type:"tiles";tiles:object[]; } | {type:"notice";tone:"error"|"ok"|"warn"|"info"|"muted";title?:string;text:string;link?: {label:string;href:string; }; }; } |undefined>
like action(), with the whole answer: a text, or a message with its panel part (on hosts with "action.result")
Parameters
id
string
input?
unknown = {}
Returns
Promise<string | { message?: string; panel?: { type: "keyValue"; items: object[]; } | { type: "table"; columns: string[]; rows: (string | { text?: string; tone?: "error" | "ok" | "warn" | "info" | "muted"; until?: number; since?: number; at?: number; })[][]; rowActions?: object[][]; rowCatalog?: (string | null)[]; } | { type: "qr"; data: string; caption?: string; } | { type: "json"; data: unknown; } | { type: "tiles"; tiles: object[]; } | { type: "notice"; tone: "error" | "ok" | "warn" | "info" | "muted"; title?: string; text: string; link?: { label: string; href: string; }; }; } | undefined>
http()
http(
request):Promise<Required<{status:number;headers?:Record<string,string>;body?:string; }>>
Calls a webhook route; the method defaults to POST. An unknown route answers 404, a throwing handler rejects.
Parameters
request
Partial<{ method: string; path: string; query: Record<string, string>; headers: Record<string, string>; body: string; }> & Pick<{ method: string; path: string; query: Record<string, string>; headers: Record<string, string>; body: string; }, "path">
Returns
Promise<Required<{ status: number; headers?: Record<string, string>; body?: string; }>>
unhandledRejection()
unhandledRejection(
reason):void
Hands reason to the plugin as the worker would for an unhandled promise rejection. Real ones
only reach the plugin with forwardRejections; otherwise node:test keeps handling them.
Parameters
reason
unknown
Returns
void
logLevel()
logLevel(
level):Promise<void>
the lowest log level the host keeps, as when the user switches debug logging on or off; lines below it aren't sent
Parameters
level
"error" | "warn" | "info" | "debug"
Returns
Promise<void>
watchSignals()
watchSignals(
signals):Promise<void>
the signals the user chose, for a plugin that declares watches; replaces the previous list
Parameters
signals
object[]
Returns
Promise<void>
pushValues()
pushValues(
values,dropped?):Promise<void>
Delivers values of watched signals like the host does; resolves once the plugin acknowledged
the batch, i.e. its watched() finished. ts and kind default to now and "state".
Parameters
values
(Omit<{ signalId: string; ts: number; kind: "state" | "command"; value: Value; forced?: true; initial?: true; }, "kind" | "ts"> & Partial<Pick<{ signalId: string; ts: number; kind: "state" | "command"; value: Value; forced?: true; initial?: true; }, "kind" | "ts">>)[]
dropped?
number = 0
Returns
Promise<void>
stop()
stop():
Promise<void>
stops the plugin like the host would; resolves when it reported stopped
Returns
Promise<void>