"use strict"; // The 1.x way of declaring capabilities, on the classes 2.0 generates discovery from. const { test } = require("node:test"); const assert = require("node:assert/strict"); const alex2node = require("alex2node"); const { ActionMapping, AlexaActions, AlexaInterface, AlexaInterfaceType, Capability, DeclarationError, EndpointHealth, PowerController, PowerState, registry, } = alex2node; const { endpoint } = require("../helpers/endpoint.js"); test("every export of 1.5.2 is there", () => { const exported = [ "ActionMapping", "Alex2MQTT", "AlexaActions", "AlexaErrorResponse", "AlexaErrorType", "AlexaInterface", "AlexaInterfaceType", "AlexaStatusMessage", "DEFAULT_HOST", "Device", "DisplayCategory", "EndpointHealth", "PowerController", "TemperatureSensorScale", "ThermostatMode", ]; assert.deepEqual(exported.filter((name) => alex2node[name] === undefined), []); }); test("PowerController and EndpointHealth are the enums of 1.x and the descriptors of 2.0", () => { assert.deepEqual([PowerController.ON, PowerController.OFF], ["ON", "OFF"]); assert.deepEqual([EndpointHealth.OK, EndpointHealth.UNREACHABLE], ["OK", "UNREACHABLE"]); assert.equal(PowerController, registry.get("Alexa.PowerController")); assert.equal(EndpointHealth, registry.get(AlexaInterfaceType.ENDPOINT_HEALTH)); assert.deepEqual(PowerState, { ON: "ON", OFF: "OFF" }); }); test("AlexaInterface: the constructor and the getters of 1.x", () => { const capability = new AlexaInterface(AlexaInterfaceType.THERMOSTAT_CONTROLLER, false, true); assert.ok(capability instanceof Capability); assert.equal(capability.type, "Alexa.ThermostatController"); assert.equal(capability.getType(), AlexaInterfaceType.THERMOSTAT_CONTROLLER); assert.equal(capability.getTypeString(), "Alexa.ThermostatController"); assert.equal(capability.getVersion(), "3.2"); assert.deepEqual(capability.getProps(), ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]); assert.deepEqual([capability.retrievable, capability.proactivelyReported, capability.instance], [false, true, ""]); assert.deepEqual(capability.getJSON(), capability.toJSON()); const toggle = new AlexaInterface("Alexa.ToggleController", true, false, "Fan.Oscillate"); toggle.setInstance("Fan.Swing"); toggle.addFriendlyName("Swing", "en-GB"); assert.equal(toggle.getJSON().instance, "Fan.Swing"); assert.deepEqual(toggle.getJSON().capabilityResources, { friendlyNames: [{ "@type": "text", value: { text: "Swing", locale: "en-GB" } }] }); }); test("addCapability takes the enum member or the namespace, and throws for a name that is not an interface", () => { const lamp = endpoint(); const byMember = lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER, { proactivelyReported: true }); const byName = lamp.addCapability("Alexa.BrightnessController"); assert.ok(byMember instanceof AlexaInterface); assert.equal(byMember.descriptor, PowerController); assert.deepEqual(lamp.getCapabilities(), [byMember, byName]); for (const name of ["Alexa.PowerControler", AlexaInterfaceType.UNKNOWN, undefined]) { assert.throws( () => lamp.addCapability(name), (err) => err instanceof DeclarationError && err.message === `lamp-1: ${JSON.stringify(name)} is not an interface alex2node knows` ); } assert.equal(lamp.getCapabilities().length, 2); assert.throws(() => new AlexaInterface("Alexa.PowerControler"), DeclarationError); }); test("getJSON replaced on a capability, as 1.x callers did to correct it, is what discovery lists", () => { const lock = endpoint("lock-1", "Front Door", ["SMARTLOCK"]); const capability = lock.addCapability(AlexaInterfaceType.LOCK_CONTROLLER); const original = capability.getJSON.bind(capability); capability.getJSON = () => ({ ...original(), properties: { ...original().properties, supported: [{ name: "lockState" }] } }); assert.deepEqual(lock.getJSON().capabilities[0].properties.supported, [{ name: "lockState" }]); }); test("ActionMapping: the payload is an object and reaches discovery as one", () => { const open = new ActionMapping([AlexaActions.Open, AlexaActions.Raise], "SetRangeValue", { rangeValue: 100 }); assert.deepEqual(open.toJSON(), { "@type": "ActionsToDirective", actions: ["Alexa.Actions.Open", "Alexa.Actions.Raise"], directive: { name: "SetRangeValue", payload: { rangeValue: 100 } }, }); assert.equal(open.deprecation, undefined); // An empty payload is kept: the ToggleController examples of Amazon have "payload": {} assert.deepEqual(new ActionMapping([AlexaActions.Close], "TurnOff", {}).toJSON().directive, { name: "TurnOff", payload: {} }); // None at all, and the "" that 1.x took for none assert.deepEqual(new ActionMapping([AlexaActions.Close], "TurnOff").toJSON().directive, { name: "TurnOff" }); assert.deepEqual(new ActionMapping([AlexaActions.Close], "TurnOff", "").toJSON().directive, { name: "TurnOff" }); }); test("ActionMapping: a JSON string is parsed and noted as deprecated, any other string throws", () => { const blinds = endpoint("blinds-1", "Blinds", ["INTERIOR_BLIND"]); const lift = blinds.addCapability(AlexaInterfaceType.RANGE_CONTROLLER, { instance: "Blind.Lift" }); lift.addActionMapping(new ActionMapping([AlexaActions.Close], "SetRangeValue", JSON.stringify({ rangeValue: 0 }))); lift.addActionMapping(new ActionMapping([AlexaActions.Open], "SetRangeValue", { rangeValue: 100 })); // 1.5.2 put the string itself into discovery: "payload": "{\"rangeValue\":0}" assert.deepEqual(lift.getJSON().semantics.actionMappings.map((mapping) => mapping.directive.payload), [{ rangeValue: 0 }, { rangeValue: 100 }]); assert.deepEqual( blinds.check().filter((line) => line.includes("JSON string")), ['blinds-1: Alexa.RangeController "Blind.Lift": the payload of the action mapping for SetRangeValue is a JSON string, pass the object'] ); for (const text of ["rangeValue: 0", "0", "[0]", "null", '"closed"']) { assert.throws( () => new ActionMapping([AlexaActions.Close], "SetRangeValue", text), (err) => err instanceof DeclarationError && err.message === `the payload of the action mapping for SetRangeValue is an object, got the string ${JSON.stringify(text)}` ); } });