tinyrelayfile
tinyrelay/scripts/chat-activity.test.mjs/

scripts/chat-activity.test.mjs raw
1import test from "node:test";
2import assert from "node:assert/strict";
3import fs from "node:fs";
4import vm from "node:vm";
5
6const source = fs.readFileSync("tinyclient/chat-activity.js", "utf8");
7const actor = "a".repeat(64), author = "b".repeat(64), request = "c".repeat(64);
8
9class Node {
10 constructor(tag = "div") { this.localName = tag; this.childNodes = []; this.attributes = {}; this.parent = null; }
11 get isConnected() { return Boolean(this.parent); }
12 append(...items) { for (const item of items) { if (item instanceof Node) item.parent = this; this.childNodes.push(item); } }
13 replaceChildren(...items) { this.childNodes = []; this.append(...items); }
14 setAttribute(name, value) { this.attributes[name] = String(value); }
15 getAttribute(name) { return this.attributes[name] ?? null; }
16 hasAttribute(name) { return Object.hasOwn(this.attributes, name); }
17 querySelectorAll() { return []; }
18 contains(node) { return node === this || this.childNodes.some(child => child instanceof Node && child.contains(node)); }
19 closest(selector) { return selector === this.localName ? this : this.parent?.closest(selector) || null; }
20 dispatchEvent(event) { this.listeners?.[event.type]?.(event); return true; }
21 addEventListener(name, fn) { (this.listeners ||= {})[name] = fn; }
22 removeEventListener(name) { delete this.listeners?.[name]; }
23}
24
25function setup() {
26 const document = new Node("document"); document.hidden = false; document.activeElement = null;
27 const definitions = {}, events = {};
28 document.addEventListener = (name, fn) => (events[name] ||= []).push(fn);
29 document.removeEventListener = () => {};
30 const signer = {getPublicKey: async () => actor};
31 const published = [];
32 const tiny = {signer: () => signer, localPath: path => path, signing: {publish: async event => { published.push(event); return event; }},
33 ui: {FormElement: class extends Node { report(message) { this.message = message; } connectedCallback() { this.form = {}; } }}};
34 const template = new Node("template");
35 document.createElement = tag => tag === "template" ? template : new Node(tag);
36 const sandbox = {document, HTMLElement: Node, customElements: {define: (name, ctor) => definitions[name] = ctor}, tiny,
37 location: {href: "https://relay.test/rooms/room", origin: "https://relay.test"}, URL, AbortController, setInterval: () => 1,
38 clearInterval: () => {}, setTimeout, clearTimeout, queueMicrotask, CustomEvent: class { constructor(type,opts){this.type=type;Object.assign(this,opts);} }, morph: (target,html)=>{target.morphed=html;}, fetch: async () => ({ok: true, text: async () => ""}), Date, JSON, Error};
39 vm.runInNewContext(source, sandbox);
40 return {document, definitions, tiny, signer, published, template};
41}
42
43test("Fixi refresh preserves a draft focused while the request was in flight", async () => {
44 const {definitions, document} = setup();
45 const host = new (definitions["chat-activity"])(); host.parent = document;
46 const original = new Node("textarea"); host.append(original); host.connectedCallback();
47 const cfg={trigger:{target:host},headers:{},abort(){},response:{ok:true},target:new Node(),text:"new cards"};
48 host.onConfig({target:host,detail:{cfg}});
49 assert.equal(cfg.cache,"no-store");assert.equal(cfg.credentials,"same-origin");assert.equal(cfg.transition,false);
50 document.activeElement=original;cfg.swap();assert.equal(cfg.target.morphed,undefined);
51 document.activeElement=null;cfg.swap();assert.equal(cfg.target.morphed,"new cards");
52 host.disconnectedCallback();
53});
54
55test("Fixi swap preserves details and ignores failed responses and logout", () => {
56 const {definitions,document}=setup(),host=new (definitions["chat-activity"])();host.parent=document;
57 const open={dataset:{task:"first"}},first={dataset:{task:"first"}},second={dataset:{task:"second"}};
58 host.querySelectorAll=()=>[open];
59 const target={querySelectorAll:selector=>selector.startsWith("details")?[first,second]:[]};
60 const cfg={target,text:"cards",response:{ok:false}};host.swap(cfg);assert.equal(target.morphed,undefined);
61 cfg.response.ok=true;host.swap(cfg);assert.equal(first.open,true);assert.equal(second.open,false);
62 host.stopped=true;cfg.text="logout";host.swap(cfg);assert.equal(target.morphed,"cards");
63});
64
65test("decision revalidates the current card before signing and emits a NIP-22 answer", async () => {
66 const {definitions, document, template, published} = setup();
67 const Decision = definitions["chat-decision"], host = new (definitions["chat-activity"])(); host.localName = "chat-activity"; host.parent = document;
68 host.read = async () => "fresh"; host.schedule = () => { host.scheduled = true; };
69 const allowed = new Node("chat-decision"); allowed.setAttribute("event", request); allowed.setAttribute("actor", actor); allowed.setAttribute("pubkey", author);
70 template.content = {querySelectorAll: selector => selector === "chat-decision" ? [allowed] : []};
71 const decision = new Decision(); decision.localName = "chat-decision"; decision.parent = host;
72 for (const [name, value] of [["event", request], ["actor", actor], ["pubkey", author], ["room", "room"], ["kind", "5000"], ["question", ""]]) decision.setAttribute(name, value);
73 decision.submitter = {value: "?"};
74 const form = {elements: {content: {value: " answer "}}};
75 await decision.submit(form);
76 assert.equal(published.length, 1);
77 assert.equal(published[0].kind, 1111);
78 assert.deepEqual(JSON.parse(JSON.stringify(published[0].tags)), [["h", "room"], ["E", request, "", author], ["K", "5000"], ["P", author], ["e", request, "", author], ["k", "5000"], ["p", author]]);
79 assert.equal(published[0].content, "answer");
80});
81
82test("decision refuses a card that disappeared while it was being answered", async () => {
83 const {definitions, document, template} = setup();
84 const Decision = definitions["chat-decision"], host = new (definitions["chat-activity"])(); host.localName = "chat-activity"; host.parent = document;
85 host.read = async () => "gone"; host.schedule = () => { host.scheduled = true; };
86 template.content = {querySelectorAll: () => []};
87 const decision = new Decision(); decision.localName = "chat-decision"; decision.parent = host;
88 for (const [name, value] of [["event", request], ["actor", actor], ["pubkey", author], ["room", "room"], ["kind", "5000"]]) decision.setAttribute(name, value);
89 decision.submitter = {value: "+"};
90 await assert.rejects(decision.submit({elements: {}}), /no longer waiting/);
91 assert.equal(host.scheduled, true);
92});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs