proactiveEvent() built every proactive event without a context, so doorbellPress() and
device.raise(DoorbellEventSource, "DoorbellPress") left out the "context": {} that the example of
alexa-doorbelleventsource.html has. The Event of Alexa.SimpleEventSource is printed without one, so
the event descriptor says which it is (emptyContext) and raise() passes it on to the builder.
A raised DoorbellPress is now { event, context: {} }, 13 bytes longer; the Event of a button is
unchanged. The tests compare the keys and the context of the built message with the fixture of the
page, for the builder, the descriptor and raise(). 267 tests pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
142 lines
9.3 KiB
JavaScript
142 lines
9.3 KiB
JavaScript
"use strict";
|
|
// Scenes, event sources and the interfaces declared with them: what the examples of their pages do not show. The
|
|
// examples themselves are run by descriptors.test.js, device/discovery.test.js and dispatch/interfaces.test.js.
|
|
const { test } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const {
|
|
AlexaInterfaceType, DeclarationError, DoorbellEventSource, InventoryLevelSensor, SceneController, SimpleEventSource,
|
|
TimeHoldController, WakeOnLANController, asset, registry, text,
|
|
} = require("alex2node");
|
|
const { endpoint } = require("../helpers/endpoint.js");
|
|
const { doc } = require("../helpers/fixtures.js");
|
|
|
|
function scene(name = "Evening", categories = ["SCENE_TRIGGER"], description = "Evening scene by Alex2Node") {
|
|
const device = endpoint("scene-1", name, categories);
|
|
device.setDescription(description);
|
|
return device;
|
|
}
|
|
|
|
const refused = (device, descriptor, options, problem) => assert.throws(
|
|
() => device.add(descriptor, options),
|
|
(err) => err instanceof DeclarationError && err.problem === problem
|
|
);
|
|
|
|
test("the events of the pages fit the payload of their descriptor, and come back as they are", () => {
|
|
const printed = [
|
|
["Alexa.SceneController", "alexa-scenecontroller", "ActivationStarted", "response"],
|
|
["Alexa.SceneController", "alexa-scenecontroller", "DeactivationStarted", "response"],
|
|
["Alexa.DoorbellEventSource", "alexa-doorbelleventsource", "DoorbellPress", "proactive"],
|
|
["Alexa.SimpleEventSource", "alexa-simpleeventsource", "Event", "proactive"],
|
|
["Alexa.WakeOnLANController", "alexa-wakeonlancontroller", "WakeUp", "response"],
|
|
];
|
|
for (const [namespace, page, name, topic] of printed) {
|
|
const example = doc(page, `${name}.event`);
|
|
const { header, payload } = example.event;
|
|
const descriptor = registry.get(namespace);
|
|
const event = descriptor.events[name];
|
|
// raise() sends a context where the page prints one, and the pages print an empty one or none
|
|
if (topic === "proactive") {
|
|
assert.equal(event.emptyContext === true, "context" in example, name);
|
|
assert.deepEqual(example.context ?? {}, {}, name);
|
|
}
|
|
assert.deepEqual([header.namespace, header.name, header.payloadVersion], [namespace, event.name, descriptor.version]);
|
|
assert.equal(event.topic, topic, name);
|
|
assert.equal("correlationToken" in header, topic === "response", name);
|
|
assert.deepEqual(event.payload.parse(payload, "payload"), payload);
|
|
}
|
|
const names = registry.list().filter((descriptor) => descriptor.events).map((descriptor) => descriptor.namespace);
|
|
assert.deepEqual(names, [...new Set(printed.map(([namespace]) => namespace))].sort());
|
|
});
|
|
|
|
test("SceneController: a scene declared without options is announced as 1.5.2 announced it", () => {
|
|
const OF_1X = { type: "AlexaInterface", interface: "Alexa.SceneController", version: "3", supportsDeactivation: true, proactivelyReported: false };
|
|
assert.deepEqual(scene().addCapability(AlexaInterfaceType.SCENE_CONTROLLER).getJSON(), OF_1X);
|
|
assert.deepEqual(scene().add(SceneController).toJSON(), OF_1X);
|
|
assert.deepEqual(scene().add(SceneController, { supportsDeactivation: true }).toJSON(), {
|
|
type: "AlexaInterface", interface: "Alexa.SceneController", version: "3", supportsDeactivation: true,
|
|
});
|
|
});
|
|
|
|
test("SceneController: the endpoint of a scene has a category of scenes, the word scene in its description and a name of up to 128", () => {
|
|
const device = scene("Evening", ["ACTIVITY_TRIGGER", "OTHER"], "Evening Scene");
|
|
device.add(SceneController);
|
|
assert.deepEqual(device.check(), []);
|
|
|
|
refused(scene("Evening", ["LIGHT"]), SceneController, {}, "a scene has the display category SCENE_TRIGGER, or ACTIVITY_TRIGGER when the order of its steps matters");
|
|
refused(
|
|
scene("Evening", ["SCENE_TRIGGER"], "All lights off"),
|
|
SceneController,
|
|
{},
|
|
'the description of a scene has the word "scene" in it, like "Party scene connected by Alex2Node"'
|
|
);
|
|
refused(scene("a".repeat(129)), SceneController, {}, "the name of a scene takes up to 128 characters, this one has 129");
|
|
|
|
// Declared the 1.x way the scene is announced, and check() says what Alexa may not take
|
|
const old = scene("Evening", ["SCENE_TRIGGER"], "All lights off");
|
|
old.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
|
|
assert.deepEqual(old.check(), ['scene-1: Alexa.SceneController: the description of a scene has the word "scene" in it, like "Party scene connected by Alex2Node"']);
|
|
});
|
|
|
|
test("DoorbellEventSource: proactivelyReported on the capability, DOORBELL among the categories and CAMERA before it", () => {
|
|
const announced = { type: "AlexaInterface", interface: "Alexa.DoorbellEventSource", version: "3", proactivelyReported: true };
|
|
const bell = endpoint("door-1", "Door", ["DOORBELL"]);
|
|
assert.deepEqual(bell.add(DoorbellEventSource, { proactivelyReported: false }).toJSON(), announced);
|
|
assert.deepEqual(endpoint("door-1", "Door", ["DOORBELL"]).addCapability(AlexaInterfaceType.DOORBELL_EVENT_SOURCE).getJSON(), announced);
|
|
refused(endpoint("door-1", "Door", ["CAMERA"]), DoorbellEventSource, {}, "a doorbell has the display category DOORBELL");
|
|
refused(endpoint("door-1", "Door", ["DOORBELL", "CAMERA"]), DoorbellEventSource, {}, "a video doorbell lists the display category CAMERA before DOORBELL");
|
|
});
|
|
|
|
test("SimpleEventSource: version 1.0, an instance for each button with its events, no properties object", () => {
|
|
const remote = () => endpoint("remote-1", "Remote", ["REMOTE"]);
|
|
const single = { id: "Button.SinglePush.1", friendlyNames: [asset("Alexa.Button.SinglePush")] };
|
|
const double = { id: "Button.DoublePush.1", friendlyNames: [asset("Alexa.Button.DoublePush"), text("Twice")] };
|
|
const top = { instance: "Remote.TopButton", friendlyNames: [asset("Alexa.Button.TopButton")] };
|
|
|
|
assert.deepEqual(remote().add(SimpleEventSource, { ...top, supportedEvents: [single, double] }).toJSON(), {
|
|
type: "AlexaInterface",
|
|
interface: "Alexa.SimpleEventSource",
|
|
instance: "Remote.TopButton",
|
|
version: "1.0",
|
|
capabilityResources: { friendlyNames: top.friendlyNames },
|
|
configuration: { supportedEvents: [single, double] },
|
|
});
|
|
refused(remote(), SimpleEventSource, { supportedEvents: [single] }, "needs an instance name, like Blind.Lift");
|
|
refused(remote(), SimpleEventSource, { ...top, supportedEvents: [] }, "supportedEvents: expected a list with 1 or more entries, got []");
|
|
refused(remote(), SimpleEventSource, { ...top, supportedEvents: [single, single] }, "the event Button.SinglePush.1 is listed twice");
|
|
refused(
|
|
remote(),
|
|
SimpleEventSource,
|
|
{ ...top, supportedEvents: [single, { ...double, friendlyNames: single.friendlyNames }] },
|
|
"two events have Alexa.Button.SinglePush as their first friendly name, the Alexa app shows an event by it"
|
|
);
|
|
});
|
|
|
|
test("TimeHoldController, InventoryLevelSensor and WakeOnLANController: what a declaration has to say", () => {
|
|
const device = () => endpoint("device-1", "Device", ["OTHER"]);
|
|
refused(device(), TimeHoldController, {}, "allowRemoteResume: expected true or false, got nothing");
|
|
|
|
const ink = { instance: "Ink.Cyan", friendlyNames: [text("Cyan ink")], replenishment: { "@type": "DashReplenishmentId", value: "id-1" } };
|
|
refused(device(), InventoryLevelSensor, { ...ink, measurement: { "@type": "Volume" } }, "measurement.unit: expected the unit of the volume, like MILLILITER or GRAM, got nothing");
|
|
refused(device(), InventoryLevelSensor, { ...ink, measurement: { "@type": "Percentage", unit: "PERCENT" } }, 'measurement.unit: expected no unit for a percentage, got "PERCENT"');
|
|
refused(device(), InventoryLevelSensor, { ...ink, measurement: { "@type": "Count" }, replenishment: undefined }, "replenishment: expected an object with @type, value, got nothing");
|
|
assert.deepEqual(InventoryLevelSensor.properties.level.value.parse({ "@type": "Weight", value: 0.5, unit: "KILOGRAM" }), { "@type": "Weight", value: 0.5, unit: "KILOGRAM" });
|
|
|
|
assert.equal(WakeOnLANController.deferrable, true);
|
|
assert.deepEqual(device().add(WakeOnLANController, { macAddresses: ["00:14:22:01:23:45"] }).toJSON().configuration, { MACAddresses: ["00:14:22:01:23:45"] });
|
|
refused(device(), WakeOnLANController, {}, "macAddresses: expected a list with 1 or more entries, got nothing");
|
|
refused(device(), WakeOnLANController, { macAddresses: ["0014.2201.2345"] }, 'macAddresses[0]: expected a MAC address like 00-14-22-01-23-45, got "0014.2201.2345"');
|
|
refused(device(), WakeOnLANController, { macAddresses: ["00-14-22-01-23-ab", "00:14:22:01:23:AB"] }, "00-14-22-01-23-AB is listed twice");
|
|
|
|
// Declared the 1.x way none of the three can say it: they are announced, and check() lists what is missing
|
|
const old = device();
|
|
old.addCapability(AlexaInterfaceType.TIME_HOLD_CONTROLLER);
|
|
old.addCapability(AlexaInterfaceType.WAKE_ON_LAN_CONTROLLER);
|
|
assert.deepEqual(old.getJSON().capabilities.slice(0, 2), [
|
|
{ type: "AlexaInterface", interface: "Alexa.TimeHoldController", version: "3", properties: { supported: [{ name: "holdStartTime" }, { name: "holdEndTime" }], proactivelyReported: false, retrievable: true } },
|
|
{ type: "AlexaInterface", interface: "Alexa.WakeOnLANController", version: "3", properties: {} },
|
|
]);
|
|
assert.deepEqual(old.check(), [
|
|
"device-1: Alexa.TimeHoldController: allowRemoteResume: expected true or false, got nothing",
|
|
"device-1: Alexa.WakeOnLANController: macAddresses: expected a list with 1 or more entries, got nothing",
|
|
]);
|
|
});
|