"use strict"; // The interfaces of a TV and a speaker: 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 { Alex2MQTT, AlexaInterfaceType, DeclarationError, EqualizerController, InputController, Inputs, MemoryPublisher, PlaybackController, PlaybackStateReporter, Speaker, StepSpeaker, } = require("alex2node"); const { endpoint } = require("../helpers/endpoint.js"); const { directive } = require("../helpers/harness.js"); const tv = () => endpoint("tv-1", "TV", ["TV"]); const refused = (descriptor, options, message) => assert.throws( () => tv().add(descriptor, options), (err) => err instanceof DeclarationError && err.message === `tv-1: ${descriptor.namespace}: ${message}` ); test("1.x declarations: the version and the properties are those of the pages, where 1.5.2 had version 1 and no property", () => { const T = AlexaInterfaceType; const announced = (type) => { const { version, properties } = tv().addCapability(type).getJSON(); return [version, properties && properties.supported]; }; assert.deepEqual(announced(T.SPEAKER), ["3", [{ name: "volume" }, { name: "muted" }]]); assert.deepEqual(announced(T.STEP_SPEAKER), ["3", undefined]); assert.deepEqual(announced(T.EQUALIZER_CONTROLLER), ["3", [{ name: "bands" }, { name: "mode" }]]); assert.deepEqual(announced(T.PLAYBACK_CONTROLLER), ["3", undefined]); assert.deepEqual(announced(T.PLAYBACK_STATE_REPORTER), ["3", [{ name: "playbackState" }]]); assert.deepEqual(announced(T.INPUT_CONTROLLER), ["3", [{ name: "input" }]]); assert.deepEqual(announced(T.CHANNEL_CONTROLLER), ["3", [{ name: "channel" }]]); }); test("1.x declarations of an equalizer and of inputs have no way to say what the device has: check() says it", () => { const device = tv(); device.addCapability(AlexaInterfaceType.EQUALIZER_CONTROLLER); device.addCapability(AlexaInterfaceType.INPUT_CONTROLLER); device.addCapability(AlexaInterfaceType.SPEAKER); assert.deepEqual(device.check(), [ "tv-1: Alexa.EqualizerController: needs bands, modes or both: declare what the equalizer has", "tv-1: Alexa.InputController: inputs: expected a list with 1 or more entries, got nothing", ]); }); test("Speaker and StepSpeaker: neither has options, and the StepSpeaker has no properties object", () => { assert.deepEqual(tv().add(StepSpeaker).toJSON(), { type: "AlexaInterface", interface: "Alexa.StepSpeaker", version: "3" }); refused(Speaker, { maximum: 11 }, "maximum: not an option of the interface"); assert.deepEqual(Object.keys(PlaybackStateReporter.directives), []); }); test("EqualizerController: a device with bands, with modes or with both lists the property of what it has", () => { const bands = { supported: ["BASS", "TREBLE"], range: { minimum: -10, maximum: 10 } }; const onlyBands = tv().add(EqualizerController, { bands }).toJSON(); assert.deepEqual(onlyBands.properties.supported, [{ name: "bands" }]); assert.deepEqual(onlyBands.configurations, { bands: { supported: [{ name: "BASS" }, { name: "TREBLE" }], range: bands.range } }); const onlyModes = tv().add(EqualizerController, { modes: ["NIGHT", "TV"] }).toJSON(); assert.deepEqual(onlyModes.properties.supported, [{ name: "mode" }]); assert.deepEqual(onlyModes.configurations, { modes: { supported: [{ name: "NIGHT" }, { name: "TV" }] } }); assert.equal("configuration" in onlyModes, false, "the interface spells it configurations"); refused(EqualizerController, {}, "needs bands, modes or both: declare what the equalizer has"); refused(EqualizerController, { bands: { ...bands, range: { minimum: 6, maximum: -6 } } }, "bands.range.maximum -6 is not above bands.range.minimum 6"); refused(EqualizerController, { bands: { ...bands, supported: ["BASS", "BASS"] } }, "BASS is listed twice"); refused(EqualizerController, { modes: ["CINEMA"] }, 'modes[0]: expected MOVIE | MUSIC | NIGHT | SPORT | TV, got "CINEMA"'); }); test("PlaybackController: all eight operations unless the declaration lists some, and only those reach the handler", async () => { assert.deepEqual(tv().add(PlaybackController).toJSON().supportedOperations, Object.keys(PlaybackController.directives)); refused( PlaybackController, { supportedOperations: ["Eject"] }, 'supportedOperations[0]: expected Play | Pause | Stop | Next | Previous | FastForward | Rewind | StartOver, got "Eject"' ); const sent = new MemoryPublisher(); const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: () => {} }); const player = bridge.addDevice({ endpointId: "player-1", name: "Player", categories: ["STREAMING_DEVICE"] }); const state = player.add(PlaybackStateReporter); const seen = []; player.add(PlaybackController, { supportedOperations: ["Play", "Pause"] }).on("*", (ctx) => { seen.push(ctx.name); return ctx.respond((s) => s.set(state, "playbackState", { state: ctx.name === "Play" ? "PLAYING" : "PAUSED" })); }); for (const [name, token] of [["Play", "ct-1"], ["Rewind", "ct-2"], ["Pause", "ct-3"]]) { await bridge.receive("root/player-1/alexaDirective", JSON.stringify(directive("Alexa.PlaybackController", name, "player-1", token))); } assert.deepEqual(seen, ["Play", "Pause"]); const answers = sent.published.map(({ message: { event, context } }) => [ event.header.name, context ? context.properties.filter(({ name }) => name === "playbackState").map(({ value }) => value.state) : event.payload.type, ]); assert.deepEqual(answers, [["Response", ["PLAYING"]], ["ErrorResponse", "INVALID_DIRECTIVE"], ["Response", ["PAUSED"]]]); }); test("InputController: the inputs are on the capability, each name and friendly name stands for one input", () => { assert.equal(Inputs.length, 61); assert.equal(new Set(Inputs).size, 61); const inputs = [{ name: "HDMI 1", friendlyNames: ["Console"] }, { name: "TUNER" }]; assert.deepEqual(tv().add(InputController, { inputs }).toJSON().inputs, inputs); refused(InputController, { inputs: [] }, "inputs: expected a list with 1 or more entries, got []"); refused(InputController, { inputs: [{ name: "SCART" }] }, 'inputs[0].name: expected an input like HDMI 1, got "SCART"'); refused(InputController, { inputs: [{ name: "TV" }, { name: "TV" }] }, "TV is listed twice, a name stands for one input"); refused( InputController, { inputs: [{ name: "HDMI 1", friendlyNames: ["Console"] }, { name: "HDMI 2", friendlyNames: ["Console"] }] }, "Console is listed twice, a name stands for one input" ); });