Add channel-nine-only notification builder preset

This commit is contained in:
mikecarper committed 2026-10-05 18:41:27 -07:00
1 parent 46e6bb7b71
commit 1db09f41ac
6 files changed
+195 -9

No files matched your search

+36
View File
@@ -16,6 +16,10 @@ for (const config of Object.values(builder.EXAMPLES)) {
const complete = {...config, id: config.kind === "contact" ? "01".repeat(32) : config.id};
const commands = builder.commands(complete);
assert.ok(commands.every(cmd => cmd.length + 4 <= 175));
if (config.quietOthers === "on") {
assert.throws(() => builder.notificationText(complete), /CLI|USB/);
continue;
}
const dm = builder.notificationText(complete);
assert.ok(dm.startsWith("!notify "));assert.ok(!dm.includes("gpio="));assert.ok(dm.length <= 159);
}
@@ -28,6 +32,38 @@ assert.equal(builder.notificationText(longPost).length, 151);
assert.throws(() => builder.notificationText({...longPost, kind: "room"}), /150 characters for a room post/);
assert.throws(() => builder.commands({...builder.EXAMPLES.food, remote: "on"}));
assert.throws(() => builder.commands({...vip, id: "1234"}));
// The channel-only example installs a quiet default before its exception.
const quietPrefix = [
"set notify.enabled on",
"set notify.vibration all off", "set notify.sound all off",
"set notify.led all off", "set notify.screen all off", "set notify.gpio all off",
"set notify.repeat all 1", "set notify.gap all 500", "set notify.stop all button"
];
const channel9 = builder.EXAMPLES.channel9;
assert.equal(channel9.kind, "channel");assert.equal(channel9.id, "9");
assert.equal(channel9.when, "any");assert.equal(channel9.quietOthers, "on");
assert.equal(channel9.sound, "ch9:d=8,o=5,b=180:c,e,g");
assert.equal(channel9.repeat, "1");assert.equal(channel9.gap, "500");assert.equal(channel9.stop, "button");
for (const name of ["vibration", "led", "screen", "gpio"]) assert.equal(channel9[name], "off");
const exceptionOnly = builder.commands({...channel9, quietOthers: "off"});
const onlyChannel9 = builder.commands(channel9);
assert.deepEqual(onlyChannel9, [...quietPrefix, ...exceptionOnly]);
assert.deepEqual(exceptionOnly, builder.commands({...channel9, quietOthers: undefined}));
assert.deepEqual(exceptionOnly, [
"set notify.vibration channel:9 off", "set notify.sound on",
"set notify.sound channel:9 ch9:d=8,o=5,b=180:c,e,g",
"set notify.led channel:9 off", "set notify.screen channel:9 off",
"set notify.gpio channel:9 off", "set notify.repeat channel:9 1",
"set notify.gap channel:9 500", "set notify.stop channel:9 button"
]);
assert.deepEqual(builder.commands({...channel9, id: "8"}), [
...quietPrefix, ...exceptionOnly.map(command => command.replace("channel:9", "channel:8"))
]);
for (const kind of ["all", "contact", "room"]) {
assert.throws(() => builder.commands({...channel9, kind, id: "01".repeat(32)}), /channel/i);
}
assert.throws(() => builder.notificationText(channel9), /CLI|USB/);
assert.ok(builder.notificationText({...channel9, quietOthers: "off"}).startsWith("!notify "));
// Local examples must play before choosing a recipient. Saved rules still need a key.
assert.equal(builder.parseOutputs(builder.EXAMPLES.vip).notes.length, 4);
assert.throws(() => builder.commands(builder.EXAMPLES.vip), /full 64-digit/);
+118 -3
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env python3
"""Optional browser checks for notification audio. Requires Playwright and its browser.
"""Optional browser checks for notification audio/editor. Requires Playwright and its browser.
Use MESHCORE_PREVIEW_BROWSER=webkit to also check the Safari engine, or set
Use MESHCORE_PREVIEW_BROWSER=webkit to also check the Safari engine, set
MESHCORE_PREVIEW_EXECUTABLE to use an installed browser executable, or set
MESHCORE_NOTIFICATION_PREVIEW_URL to check the deployed editor.
"""
@@ -57,7 +58,9 @@ class NotificationAudioTest(unittest.TestCase):
cls.playwright = sync_playwright().start()
engine = getattr(cls.playwright, os.environ.get("MESHCORE_PREVIEW_BROWSER", "chromium"))
try:
cls.browser = engine.launch(headless=True)
executable = os.environ.get("MESHCORE_PREVIEW_EXECUTABLE")
options = {"executable_path": executable} if executable else {}
cls.browser = engine.launch(headless=True, **options)
except Exception:
cls.playwright.stop()
raise
@@ -92,6 +95,118 @@ class NotificationAudioTest(unittest.TestCase):
# Inspect decoded audio at the speaker output, not just a successful play() call.
self.page.wait_for_function("previewAudioStats.rms > 0.05")
def test_only_channel_9_button_sets_quiet_default_and_editable_exception(self):
self.page.locator('[data-example="channel9"]').click()
expected = {
"kind": "channel", "id": "9", "when": "any", "vibration": "off",
"led": "off", "sound": "ch9:d=8,o=5,b=180:c,e,g", "screen": "off",
"gpio": "off", "repeat": "1", "gap": "500", "stop": "button",
}
for name, value in expected.items():
self.assertEqual(self.page.locator('[name="' + name + '"]').input_value(), value)
self.assertTrue(self.page.locator('[name="quietOthers"]').is_checked())
self.assertTrue(self.page.locator('[data-role="quiet-warning"]').is_visible())
self.assertEqual(self.page.locator('[data-role="error"]').inner_text(), "")
commands = self.page.locator('[data-role="commands"]').inner_text().splitlines()
self.assertEqual(commands[:9], self.quiet_prefix())
self.assertIn("set notify.sound channel:9 ch9:d=8,o=5,b=180:c,e,g", commands)
self.assertNotIn("!notify ", self.page.locator('[data-role="dm"]').inner_text())
self.assertRegex(self.page.locator('[data-role="dm"]').inner_text(), "CLI|USB")
self.page.locator('[name="id"]').fill("8")
edited = self.page.locator('[data-role="commands"]').inner_text().splitlines()
self.assertEqual(edited[:9], self.quiet_prefix())
self.assertIn("set notify.sound channel:8 ch9:d=8,o=5,b=180:c,e,g", edited)
self.assertNotIn("channel:9", "\n".join(edited))
self.page.locator('[name="quietOthers"]').uncheck()
normal = self.page.locator('[data-role="commands"]').inner_text()
self.assertNotIn(" all ", normal)
self.assertNotIn("set notify.enabled on", normal)
self.assertTrue(self.page.locator('[data-role="dm"]').inner_text().startswith("!notify "))
self.assertFalse(self.page.locator('[data-role="quiet-warning"]').is_visible())
self.page.locator('[data-example="channel9"]').click()
self.page.locator('[name="kind"]').select_option("all")
self.assertFalse(self.page.locator('[name="quietOthers"]').is_checked())
self.assertTrue(self.page.locator('[name="quietOthers"]').is_disabled())
self.assertNotIn("set notify.enabled on", self.page.locator('[data-role="commands"]').inner_text())
self.assertEqual(self.page.locator('[data-role="error"]').inner_text(), "")
def test_switching_example_clears_quiet_default(self):
for example in ("food", "find", "vip"):
self.page.locator('[data-example="channel9"]').click()
self.assertTrue(self.page.locator('[name="quietOthers"]').is_checked())
self.page.locator('[data-example="' + example + '"]').click()
self.assertFalse(self.page.locator('[name="quietOthers"]').is_checked())
if example == "vip":
self.page.locator('[name="id"]').fill("01" * 32)
commands = self.page.locator('[data-role="commands"]').inner_text()
self.assertNotIn("set notify.enabled on", commands)
self.assertNotIn("set notify.sound all off", commands)
self.assertEqual(self.page.locator('[data-role="error"]').inner_text(), "")
self.assertTrue(self.page.locator('[data-role="dm"]').inner_text().startswith("!notify "))
@staticmethod
def quiet_prefix():
return [
"set notify.enabled on", "set notify.vibration all off",
"set notify.sound all off", "set notify.led all off",
"set notify.screen all off", "set notify.gpio all off",
"set notify.repeat all 1", "set notify.gap all 500", "set notify.stop all button",
]
def mock_usb(self, reject_channel=False):
self.page.evaluate("""rejectChannel => {
let incoming, active = 0;
window.notificationUsbMock = { commands: [], maxActive: 0, replies: 0 };
const port = {
readable: new ReadableStream({ start(controller) { incoming = controller; } }),
writable: new WritableStream({ write(bytes) {
const text = new TextDecoder().decode(bytes.slice(4));
const tag = text.slice(0, 2), command = text.slice(3);
notificationUsbMock.commands.push(command);
notificationUsbMock.maxActive = Math.max(notificationUsbMock.maxActive, ++active);
setTimeout(() => {
let value = command === 'get notify' ? 'supported=31' : 'OK';
if (command.startsWith('get notify.sound channel:'))
value = rejectChannel ? 'Error: channel slot is not configured' : 'inherit';
const body = new TextEncoder().encode(tag + '|' + value);
active--; notificationUsbMock.replies++;
incoming.enqueue(Uint8Array.from([62, body.length + 1, 0, 0x1d, ...body]));
}, 10);
} }),
async open() {}, async setSignals() {}, async close() {},
};
Object.defineProperty(navigator, 'serial', { configurable: true, value: {
async requestPort() { return port; }
} });
}""", reject_channel)
def test_only_channel_9_usb_save_sends_default_then_exception_sequentially(self):
self.mock_usb()
self.page.locator('[data-example="channel9"]').click()
expected = self.page.locator('[data-role="commands"]').inner_text().splitlines()
self.assertEqual(expected[:9], self.quiet_prefix())
self.click("connect")
self.page.wait_for_function("notificationUsbMock.replies === 2 && !document.querySelector('[data-action=apply]').disabled")
self.click("apply")
self.page.wait_for_function("notificationUsbMock.replies === " + str(len(expected) + 3) + " && !document.querySelector('[data-action=apply]').disabled")
self.assertEqual(self.page.evaluate("notificationUsbMock.commands"), ["get notify", "get notify.gpio.pins", "get notify.sound channel:9"] + expected)
self.assertEqual(self.page.evaluate("notificationUsbMock.maxActive"), 1)
self.assertEqual(self.page.locator('[data-role="error"]').inner_text(), "")
self.click("disconnect")
self.page.wait_for_function("document.querySelector('[data-role=device-status]').textContent === 'Device disconnected'")
def test_unconfigured_channel_is_rejected_before_quiet_default_is_written(self):
self.mock_usb(reject_channel=True)
self.page.locator('[data-example="channel9"]').click()
self.click("connect")
self.page.wait_for_function("notificationUsbMock.replies === 2 && !document.querySelector('[data-action=apply]').disabled")
self.click("apply")
self.page.wait_for_function("notificationUsbMock.replies === 3 && !document.querySelector('[data-action=apply]').disabled")
self.assertEqual(self.page.evaluate("notificationUsbMock.commands"), ["get notify", "get notify.gpio.pins", "get notify.sound channel:9"])
self.assertIn("channel slot is not configured", self.page.locator('[data-role="error"]').inner_text())
self.click("disconnect")
self.page.wait_for_function("document.querySelector('[data-role=device-status]').textContent === 'Device disconnected'")
def test_examples_play_audible_melodies_once_without_a_recipient(self):
for index, example in enumerate(("food", "find", "vip"), 1):
self.page.locator('[data-example="' + example + '"]').click()