"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", ]); });