Alex2Node/test/readme.test.js
David dd071548bc 2.0.0
The interface registry, generated and checked discovery, typed dispatch with
automatic error answers, message builders, proactive events and typed
helpers for every error type; the 1.x API is kept and the examples of 1.5.2
run unchanged. docs/wire-changes.md lists what a 1.x caller can observe.

On 2026-09-28 a real Alexa account drove 33 test devices on this API through
the public Alex2MQTT service: Alexa accepted the discovery of all of them,
50 of 58 cases passed, 4 had no action in the control call and 4 failed on
what the check could see, none on an answer of the library. The readme table
names the interfaces that run covered. Voice commands were not tested yet.

307 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 22:55:23 +00:00

88 lines
4.9 KiB
JavaScript

"use strict";
// readme.md says what the code does: its table of interfaces is the one the registry gives, a program it shows is
// the text of a file that a test runs, and the other code blocks compile.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { run, ROOT } = require("./helpers/examples.js");
const { table, update, shown, BEGIN, END, DRIVEN, README } = require("../scripts/capability-table.js");
const alex2node = require("..");
const readme = fs.readFileSync(README, "utf8");
const AsyncFunction = (async () => {}).constructor;
// The js code blocks, each with the file its marker line names
const blocks = [...readme.matchAll(/(?:<!-- file: (\S+) -->\n)?```js\n([\s\S]*?)\n```/g)]
.map(([, file, code]) => ({ file, code }));
const loads = (code) => /require\("alex2node"\)|from "alex2node"/.test(code);
test("the table of interfaces and the files in the readme are the ones of the code: npm run docs", () => {
assert.equal(update(readme), readme);
});
test("the table has a row for every described interface, and a line for the others", () => {
const text = table();
for (const { namespace, tier } of alex2node.registry.list()) {
const row = text.split("\n").filter((line) => line.startsWith(`| [${namespace}](`));
assert.equal(row.length, tier === 3 ? 0 : 1, namespace);
assert.ok(text.includes(`[${namespace}](`), namespace);
}
assert.ok(readme.indexOf(BEGIN) < readme.indexOf(END));
});
test("an interface is said to be driven through Alexa only when a run is recorded for it", () => {
// The run of 2026-09-28 with 33 test devices on the 2.0 API. A name is added here with the record of its run.
assert.deepEqual(Object.keys(DRIVEN).sort(), [
"Alexa", "Alexa.BrightnessController", "Alexa.ColorController", "Alexa.ColorTemperatureController",
"Alexa.ContactSensor", "Alexa.DoorbellEventSource", "Alexa.InventoryLevelSensor", "Alexa.LockController",
"Alexa.ModeController", "Alexa.MotionSensor", "Alexa.PercentageController", "Alexa.PowerController",
"Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.SceneController",
"Alexa.SecurityPanelController", "Alexa.SimpleEventSource", "Alexa.TemperatureSensor",
"Alexa.ThermostatController", "Alexa.ThermostatController.Schedule", "Alexa.ToggleController",
]);
const rows = table().split("\n").filter((line) => line.startsWith("| [Alexa"));
for (const line of rows) {
const cells = line.split(" | ");
const namespace = cells[0].slice(3, cells[0].indexOf("]"));
assert.equal(cells[cells.length - 1].replace(/ \|$/, ""), DRIVEN[namespace] ?? "-", namespace);
}
});
test("a program in the readme is a file, and a test runs the file", () => {
assert.deepEqual(shown(readme), ["examples/lamp.js", "examples/testing/lamp.test.js", "examples/legacy/ExamplePowerController.js"]);
// More than the line that loads the library: a program, which has to be one of the files
const programs = blocks.filter(({ code }) => loads(code) && code.split("\n").length > 1);
assert.deepEqual(programs.map(({ file }) => file), shown(readme));
const examples = fs.readFileSync(path.join(__dirname, "examples.test.js"), "utf8");
assert.ok(examples.includes('start("lamp.js"'));
assert.ok("ExamplePowerController.js" in require("./fixtures/legacy-examples/discovery-1.5.2.json"));
// examples/testing/lamp.test.js: the next test
});
test("examples/testing/lamp.test.js passes, without a broker", async () => {
// LOOPBACK_PORT 1, where nothing listens: a connection would fail
const example = await run(path.join("testing", "lamp.test.js"));
assert.equal(await example.exited, 0, example.output());
assert.match(example.output(), /pass 4\n/);
assert.match(example.output(), /fail 0\n/);
});
test("the other code blocks of the readme compile, and load what the library exports", () => {
const fragments = blocks.filter(({ file }) => !file);
assert.ok(fragments.length >= 8);
for (const { code } of fragments) {
for (const [, names] of code.matchAll(/(?:const|import) \{([^}]+)\} (?:= require\("alex2node"\)|from "alex2node")/g)) {
for (const name of names.split(",").map((part) => part.trim())) assert.ok(name in alex2node, `${name} is not an export`);
}
const body = code.split("\n").filter((line) => !/^import .* from "alex2node";$/.test(line)).join("\n");
assert.doesNotThrow(() => new AsyncFunction(body), `this block does not compile:\n${code}`);
}
});
test("what the readme calls on AlexaErrors and on the registry is there", () => {
for (const [, helper] of readme.matchAll(/AlexaErrors\.(\w+)\(/g)) assert.equal(typeof alex2node.AlexaErrors[helper], "function", helper);
for (const [, namespace] of readme.matchAll(/registry\.get\("([^"]+)"\)/g)) assert.ok(alex2node.registry.has(namespace), namespace);
assert.equal(Object.keys(require("../dist/cjs/registry/catalog.js").ERROR_TYPES).length, 73);
});