// Tests for composer key handling. node test/ui.test.mjs import assert from "node:assert/strict"; import { shouldSend, fileExt, fileAllowed, fileMime, fileIcon, normalizeTheme, normalizeAppearance, colorScheme, composerToken, pinnedFirst, ownMessage, sameSender, memberCountLabel, onlineFilterKeeps, chatCategory, presenceLabel, displayNameFor, renameNotice, translateMentions, DEFAULT_THEME, DEFAULT_APPEARANCE, sanitizeActions, capAllows, appOpenPlan, normalizeTextSize, TEXT_SIZES, DEFAULT_TEXT_SIZE, appReloadMessage, appSchemeFrom, } from "../web/static/ui.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " β€” " + (e && e.message)); failures++; } } await test("desktop: Enter sends", () => { assert.equal(shouldSend({ key: "Enter", shiftKey: false }, false), true); }); await test("desktop: Shift+Enter is a newline (does not send)", () => { assert.equal(shouldSend({ key: "Enter", shiftKey: true }, false), false); }); await test("mobile: Enter is a newline (does not send)", () => { assert.equal(shouldSend({ key: "Enter", shiftKey: false }, true), false); }); await test("mobile: Shift+Enter is a newline (does not send)", () => { assert.equal(shouldSend({ key: "Enter", shiftKey: true }, true), false); }); await test("other keys never send", () => { assert.equal(shouldSend({ key: "a", shiftKey: false }, false), false); assert.equal(shouldSend({ key: "Tab", shiftKey: false }, false), false); }); await test("fileExt: lowercased extension without the dot", () => { assert.equal(fileExt("CLAUDE.local.md"), "md"); assert.equal(fileExt("Report.PDF"), "pdf"); assert.equal(fileExt("archive.tar.gz"), "gz"); assert.equal(fileExt("noext"), ""); assert.equal(fileExt(""), ""); assert.equal(fileExt(undefined), ""); }); await test("fileAllowed: every file type may be attached (2026-07-09)", () => { for (const name of ["a.pdf", "notes.txt", "CLAUDE.local.md", "data.csv", "x.json", "deck.pptx", "evil.exe", "run.sh", "app.js", "lib.so", "noext", ""]) { assert.equal(fileAllowed(name), true, name); } }); await test("fileMime: browser type wins, else mapped from extension", () => { assert.equal(fileMime("x.bin", "image/png"), "image/png"); // browser-provided type wins assert.equal(fileMime("CLAUDE.local.md", ""), "text/markdown"); // .md often has empty type assert.equal(fileMime("data.csv", undefined), "text/csv"); assert.equal(fileMime("mystery.xyz", ""), "application/octet-stream"); // unknown ext }); await test("fileIcon: mapped emoji, πŸ“Ž fallback", () => { assert.equal(fileIcon("a.pdf"), "πŸ“•"); assert.equal(fileIcon("b.csv"), "πŸ“Š"); assert.equal(fileIcon("c.docx"), "πŸ“˜"); assert.equal(fileIcon("d.unknown"), "πŸ“Ž"); }); await test("normalizeTheme: valid passes through, junk falls back", () => { for (const ok of ["litroom", "lagoon", "sunny", "plum", "linen", "classic"]) { assert.equal(normalizeTheme(ok), ok); } assert.equal(normalizeTheme("neon"), DEFAULT_THEME); assert.equal(normalizeTheme(null), DEFAULT_THEME); assert.equal(normalizeTheme(undefined), DEFAULT_THEME); assert.equal(normalizeTheme(""), DEFAULT_THEME); }); await test("normalizeAppearance: valid passes through, junk falls back", () => { for (const ok of ["light", "dark", "system"]) { assert.equal(normalizeAppearance(ok), ok); } assert.equal(normalizeAppearance("sepia"), DEFAULT_APPEARANCE); assert.equal(normalizeAppearance(null), DEFAULT_APPEARANCE); }); await test("colorScheme: system maps to 'light dark' so light-dark() follows the OS", () => { assert.equal(colorScheme("light"), "light"); assert.equal(colorScheme("dark"), "dark"); assert.equal(colorScheme("system"), "light dark"); assert.equal(colorScheme("anything-else"), "light dark"); }); await test("normalizeTextSize: valid stops pass through (stored strings too), junk falls back", () => { for (const ok of TEXT_SIZES) { assert.equal(normalizeTextSize(ok), ok); assert.equal(normalizeTextSize(String(ok)), ok); // localStorage returns strings } assert.equal(normalizeTextSize(15), DEFAULT_TEXT_SIZE); // not a slider stop assert.equal(normalizeTextSize("huge"), DEFAULT_TEXT_SIZE); assert.equal(normalizeTextSize(null), DEFAULT_TEXT_SIZE); assert.equal(normalizeTextSize(undefined), DEFAULT_TEXT_SIZE); assert.equal(normalizeTextSize(""), DEFAULT_TEXT_SIZE); }); await test("text size default sits one from the top of the ladder", () => { assert.equal(DEFAULT_TEXT_SIZE, 15); // one stop above the default and no more β€” bigger sizes read as shouting assert.equal(TEXT_SIZES[TEXT_SIZES.length - 2], DEFAULT_TEXT_SIZE); // strictly increasing β€” the slider steps through them by index for (let i = 1; i < TEXT_SIZES.length; i++) assert.ok(TEXT_SIZES[i] > TEXT_SIZES[i - 1]); }); await test("composerToken: detects an emoji shortcode in progress", () => { const t = composerToken("hey :tad", 8); assert.equal(t?.kind, "emoji"); assert.equal(t.query, "tad"); assert.equal(t.start, 4); // index of the ":" }); await test("composerToken: detects a mention (and @-only opens it)", () => { assert.equal(composerToken("hi @so", 6)?.kind, "mention"); assert.equal(composerToken("hi @so", 6)?.query, "so"); assert.equal(composerToken("@", 1)?.kind, "mention"); // bare "@" browses everyone assert.equal(composerToken("@", 1)?.query, ""); }); await test("composerToken: ignores non-boundary and closed tokens", () => { assert.equal(composerToken("10:30", 5), null); // time, not an emoji assert.equal(composerToken("email@host", 10), null); // email, not a mention assert.equal(composerToken("see https://x", 13), null); // URL scheme colon assert.equal(composerToken(":tada:", 6), null); // completed shortcode assert.equal(composerToken("hi :", 4), null); // a bare ":" is punctuation }); await test("composerToken: only the token before the caret counts", () => { const t = composerToken(":tada: and :par", 15); assert.equal(t?.kind, "emoji"); assert.equal(t.query, "par"); }); await test("composerToken: a leading / opens the command menu", () => { const t = composerToken("/tod", 4); assert.equal(t?.kind, "command"); assert.equal(t.query, "tod"); assert.equal(t.start, 0); assert.equal(composerToken("/", 1)?.kind, "command"); // bare "/" browses all commands assert.equal(composerToken("/", 1)?.query, ""); }); await test("composerToken: / only triggers on the command name, at the start", () => { assert.equal(composerToken("/todo buy milk", 10), null); // caret in args β†’ no menu assert.equal(composerToken("hey /todo", 9), null); // "/" mid-line isn't a command assert.equal(composerToken("/todo", 5)?.query, "todo"); // caret at end of the name }); await test("pinnedFirst: floats pinned chats, keeping order within each band", () => { const list = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]; const pins = { b: true, d: true }; assert.deepEqual( pinnedFirst(list, (t) => pins[t.id]).map((t) => t.id), ["b", "d", "a", "c"], ); }); await test("pinnedFirst: no pins leaves the list unchanged", () => { const list = [{ id: "a" }, { id: "b" }]; assert.deepEqual(pinnedFirst(list, () => false), list); }); await test("pinnedFirst: orders each band most-recent-first by lastOf", () => { // Out-of-recency-order input (the pinned d is older than pinned b; unpinned c // newer than a) β€” each band is re-sorted newest-first, pinned band on top. const list = [ { id: "a", at: 10 }, { id: "b", at: 40 }, { id: "c", at: 30 }, { id: "d", at: 20 }, ]; const pins = { b: true, d: true }; assert.deepEqual( pinnedFirst(list, (t) => pins[t.id], (t) => t.at).map((t) => t.id), ["b", "d", "c", "a"], ); // Ties keep input order (stable) β€” so a same-second burst doesn't jump around. const tied = [{ id: "x", at: 5 }, { id: "y", at: 5 }, { id: "z", at: 9 }]; assert.deepEqual( pinnedFirst(tied, () => false, (t) => t.at).map((t) => t.id), ["z", "x", "y"], ); }); await test("ownMessage: matches by uid when both sides have one", () => { assert.equal(ownMessage({ uid: "u1", name: "Paul" }, "u1", "Paul"), true); assert.equal(ownMessage({ uid: "u2", name: "Paul" }, "u1", "Paul"), false); // same name, other user }); await test("ownMessage: legacy payloads (no uid) fall back to the screen name", () => { assert.equal(ownMessage({ name: "Paul" }, "u1", "Paul"), true); assert.equal(ownMessage({ name: "Maud" }, "u1", "Paul"), false); }); await test("ownMessage: never matches on empty identity", () => { assert.equal(ownMessage(null, "u1", "Paul"), false); assert.equal(ownMessage({ name: "" }, "u1", ""), false); assert.equal(ownMessage({}, "u1", "Paul"), false); }); await test("sameSender: uid wins over the name snapshot", () => { // Same person across a rename (snapshots differ) β€” still one run. assert.equal(sameSender({ uid: "u1", name: "Dermo" }, { uid: "u1", name: "Dermot" }), true); // Two people, even with identical snapshots (both nameless), never group. assert.equal(sameSender({ uid: "u1", name: "" }, { uid: "u2", name: "" }), false); }); await test("sameSender: uid-less legacy messages fall back to the name", () => { assert.equal(sameSender({ name: "Al" }, { name: "Al" }), true); assert.equal(sameSender({ name: "Al" }, { name: "Bo" }), false); // A nameless legacy pair says nothing about who sent them β€” no grouping. assert.equal(sameSender({ name: "" }, { name: "" }), false); assert.equal(sameSender(null, { name: "Al" }), false); }); await test("sanitizeActions: whitelists types and validates params", () => { const chips = sanitizeActions([ { type: "set_theme", theme: "plum" }, { type: "set_theme", theme: "neon" }, // not a real theme { type: "set_appearance", appearance: "dark", label: " Go dark " }, { type: "delete_everything" }, // unknown type { type: "open_profile" }, { type: "open_tutorial" }, // the welcome DM's "Take the tour" chip "junk", ]); assert.deepEqual(chips.map((c) => c.type), ["set_theme", "set_appearance", "open_profile", "open_tutorial"]); assert.equal(chips[0].theme, "plum"); assert.equal(chips[1].label, "Go dark"); }); await test("sanitizeActions: caps the count and tolerates garbage", () => { assert.deepEqual(sanitizeActions(null), []); assert.deepEqual(sanitizeActions("x"), []); const many = Array.from({ length: 9 }, () => ({ type: "open_theme_picker" })); assert.equal(sanitizeActions(many).length, 4); const longLabel = sanitizeActions([{ type: "open_profile", label: "x".repeat(99) }]); assert.equal(longLabel[0].label.length, 40); }); await test("memberCountLabel: people, bots, and AI agents counted separately", () => { assert.equal(memberCountLabel(2, 2, 1), "2 members, 2 bots, 1 AI agent"); assert.equal(memberCountLabel(1, 1, 2), "1 member, 1 bot, 2 AI agents"); assert.equal(memberCountLabel(2, 3), "2 members, 3 bots"); // agents defaults to 0 }); await test("memberCountLabel: zero categories are dropped", () => { assert.equal(memberCountLabel(5, 0, 0), "5 members"); assert.equal(memberCountLabel(1, 0, 0), "1 member"); assert.equal(memberCountLabel(0, 0, 0), "0 members"); assert.equal(memberCountLabel(2, 0, 1), "2 members, 1 AI agent"); // bots dropped, agents kept }); await test("memberCountLabel: all helpers β†’ no '0 members'", () => { assert.equal(memberCountLabel(0, 2, 0), "2 bots"); assert.equal(memberCountLabel(0, 0, 3), "3 AI agents"); assert.equal(memberCountLabel(0, 1, 1), "1 bot, 1 AI agent"); }); await test("onlineFilterKeeps: people decide; pure-bot chats fall back to the bot", () => { // A DM with an online person stays; away or offline drops. assert.equal(onlineFilterKeeps([{ agent: false, presence: "online" }]), true); assert.equal(onlineFilterKeeps([{ agent: false, presence: "away" }]), false); assert.equal(onlineFilterKeeps([{ agent: false, presence: "offline" }]), false); // A chat with ONLY bots keeps iff the bot is online (sidecar health). assert.equal(onlineFilterKeeps([{ agent: true, presence: "online" }]), true); assert.equal(onlineFilterKeeps([{ agent: true, presence: "offline" }]), false); // In a mixed room only the PEOPLE count β€” an always-on bot must not make // every room it sits in read "someone's online". assert.equal( onlineFilterKeeps([ { agent: true, presence: "online" }, { agent: false, presence: "offline" }, ]), false, ); assert.equal( onlineFilterKeeps([ { agent: true, presence: "offline" }, { agent: false, presence: "online" }, ]), true, ); // Notes/app threads (no other members) never match; missing entries are safe. assert.equal(onlineFilterKeeps([]), false); assert.equal(onlineFilterKeeps([null, { agent: false, presence: "" }]), false); }); await test("chatCategory: apps, then all-bot chats, else people", () => { assert.equal(chatCategory(true, []), "apps"); assert.equal(chatCategory(true, [{ agent: true, presence: "online" }]), "apps"); // app wins assert.equal(chatCategory(false, [{ agent: true, presence: "offline" }]), "bots"); assert.equal( chatCategory(false, [ { agent: true, presence: "online" }, { agent: true, presence: "online" }, ]), "bots", ); // Any person in the room makes it a people chat, bots present or not. assert.equal( chatCategory(false, [ { agent: true, presence: "online" }, { agent: false, presence: "offline" }, ]), "people", ); assert.equal(chatCategory(false, [{ agent: false, presence: "online" }]), "people"); assert.equal(chatCategory(false, []), "people"); // your own Notes }); await test("presenceLabel: all three states read out; unknown reads blank", () => { assert.equal(presenceLabel("online"), "Online"); assert.equal(presenceLabel("away"), "Away"); assert.equal(presenceLabel("offline"), "Offline"); assert.equal(presenceLabel(""), ""); assert.equal(presenceLabel(undefined), ""); }); await test("displayNameFor: my name for someone wins over their published one", () => { assert.equal(displayNameFor({ name: "Dermo", seen: "Dermo" }, "Dermot"), "Dermo"); assert.equal(displayNameFor(null, "Dermot"), "Dermot"); assert.equal(displayNameFor(undefined, ""), ""); assert.equal(displayNameFor({ name: "", seen: "" }, "Dermot"), "Dermot"); // empty nickname = no nickname }); await test("renameNotice: fires once per unacknowledged rename, never without a nickname", () => { // The scenario from #577: invited as Dermo, renames to Dermot β†’ I get the choice. assert.deepEqual(renameNotice({ name: "Dermo", seen: "Dermo" }, "Dermot"), { from: "Dermo", to: "Dermot" }); // Acknowledged (kept my name): seen catches up, no more prompting. assert.equal(renameNotice({ name: "Dermo", seen: "Dermot" }, "Dermot"), null); // …until they rename AGAIN. assert.deepEqual(renameNotice({ name: "Dermo", seen: "Dermot" }, "Diarmuid"), { from: "Dermo", to: "Diarmuid" }); // They renamed to exactly what I call them β€” nothing to ask. assert.equal(renameNotice({ name: "Dermo", seen: "x" }, "Dermo"), null); // No nickname / no published name yet β†’ never a notice. assert.equal(renameNotice(null, "Dermot"), null); assert.equal(renameNotice({ name: "Dermo", seen: "Dermo" }, ""), null); }); await test("translateMentions: rewrites published @names to the viewer's, same boundaries as the renderer", () => { const renames = { Robert: "Bobby", "Anna Lee": "Mum" }; assert.equal(translateMentions("@Robert good to see you", renames), "@Bobby good to see you"); assert.equal(translateMentions("ping @Anna Lee now", renames), "ping @Mum now"); // multi-word label assert.equal(translateMentions("@Roberta is someone else", renames), "@Roberta is someone else"); // no trailing word char assert.equal(translateMentions("mail me@Robert.com", renames), "mail me@Robert.com"); // email stays assert.equal(translateMentions("hi @Robert and @Robert", renames), "hi @Bobby and @Bobby"); // every occurrence assert.equal(translateMentions("no mentions here", renames), "no mentions here"); assert.equal(translateMentions("@Robert", {}), "@Robert"); // no renames β†’ untouched assert.equal(translateMentions("", renames), ""); }); await test("sanitizeActions: service_research validates services and goal", () => { const ok = sanitizeActions([ { type: "service_research", services: ["github", "dnsimple", "github"], goal: " which domains point at Pages? ", label: "Look" }, ]); assert.equal(ok.length, 1); assert.deepEqual(ok[0].services, ["github", "dnsimple"]); // deduped assert.equal(ok[0].goal, "which domains point at Pages?"); // trimmed const bad = sanitizeActions([ { type: "service_research", services: [], goal: "x" }, // no services { type: "service_research", services: ["github"], goal: " " }, // blank goal { type: "service_research", services: ["GH!"], goal: "x" }, // bad id { type: "service_research", services: ["a", "b", "c", "d", "e"], goal: "x" }, // too many { type: "service_research", services: ["github"], goal: "x".repeat(301) }, // goal too long ]); assert.equal(bad.length, 0); }); await test("sanitizeActions: run_command takes one clean app command, label is the command", () => { const ok = sanitizeActions([{ type: "run_command", command: " /remind pick up the kids at 4pm ", label: "Open theme picker" }]); assert.equal(ok.length, 1); assert.equal(ok[0].command, "/remind pick up the kids at 4pm"); // trimmed assert.equal(ok[0].label, undefined); // a free label could lie about the tap β€” always render the command const bad = sanitizeActions([ { type: "run_command", command: "remind me at 4" }, // no slash { type: "run_command", command: "/" }, // empty verb { type: "run_command", command: "/todo @here buy milk" }, // group side effect from a tap { type: "run_command", command: "/todo a\nb" }, // multi-line { type: "run_command", command: "/" + "x".repeat(300) }, // too long { type: "run_command", command: "/todo a\x07b" }, // control chars ]); assert.equal(bad.length, 0); }); await test("capAllows: a connection's cap is the grant ceiling", () => { // Reads pass under every cap (and under no cap at all). assert.equal(capAllows("read", "read"), true); assert.equal(capAllows("write", "read"), true); assert.equal(capAllows(undefined, "read"), true); assert.equal(capAllows("read", undefined), true); // access defaults to read // Writes need a read+write connection… assert.equal(capAllows("write", "write"), true); assert.equal(capAllows("read", "write"), false); // …except legacy cap-less connections, which keep their old behaviour. assert.equal(capAllows(undefined, "write"), true); assert.equal(capAllows(null, "write"), true); }); await test("appReloadMessage names what actually changed, not a blanket 'new app'", () => { assert.equal(appReloadMessage("created"), "A new app is available."); assert.equal(appReloadMessage("updated"), "App updated."); assert.equal(appReloadMessage("removed"), "Apps changed."); assert.equal(appReloadMessage(undefined), "Apps changed."); // older server, no kind sent }); await test("appSchemeFrom: the shipped scheme and dev alts pass, junk falls back", () => { assert.equal(appSchemeFrom("elevenmessenger"), "elevenmessenger"); assert.equal(appSchemeFrom("elevenmessenger.alt-dek"), "elevenmessenger.alt-dek"); assert.equal(appSchemeFrom("elevenmessenger.alt-dev2"), "elevenmessenger.alt-dev2"); for (const junk of ["", "https", "evil", "elevenmessengerx", "elevenmessenger.alt-", "elevenmessenger.alt-UPPER", "elevenmessenger.alt-" + "x".repeat(33), null, undefined, 7]) { assert.equal(appSchemeFrom(junk), "elevenmessenger", `junk ${JSON.stringify(junk)} falls back`); } }); await test("appOpenPlan routes App Studio's Open by the app's places", () => { // No group place β†’ open straight into where the app lives. assert.deepEqual(appOpenPlan(["you"]), { ask: false, direct: "you" }); assert.deepEqual(appOpenPlan(["you", "space"]), { ask: false, direct: "you" }); assert.deepEqual(appOpenPlan(["space"]), { ask: false, direct: "space" }); // Group-only β†’ nothing to open until it's added somewhere: ask which group. assert.deepEqual(appOpenPlan(["chat"]), { ask: "group", direct: null }); // Group + another place β†’ ask where to open it. assert.deepEqual(appOpenPlan(["you", "chat"]), { ask: "how", direct: "you" }); assert.deepEqual(appOpenPlan(["chat", "space"]), { ask: "how", direct: "space" }); assert.deepEqual(appOpenPlan(["you", "chat", "space"]), { ask: "how", direct: "you" }); // Legacy/empty manifests read as personal-only, like registerMemberApp's default. assert.deepEqual(appOpenPlan(undefined), { ask: false, direct: "you" }); assert.deepEqual(appOpenPlan([]), { ask: false, direct: "you" }); }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);