aboutsummaryrefslogtreecommitdiffstats
path: root/tools/live-chat-actions-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-18 23:46:18 -0700
committerAdam Soltys <[email protected]>2026-10-18 23:46:18 -0700
commitaa3fecd4f0dc39550c6ad44a19736f820201d0a3 (patch)
treefac2819d695f881fbeb73264907445aed1e3e596 /tools/live-chat-actions-test.js
parent1f0b1cd2af2c2391d6706f6194bdb65c85f14ca6 (diff)
downloadcoinosv3-aa3fecd4f0dc39550c6ad44a19736f820201d0a3.tar.gz
coinosv3-aa3fecd4f0dc39550c6ad44a19736f820201d0a3.zip
A stream's chat is the app's chat: each comment is a bubble like a DM's or a group's — tap for the emoji row, reply, zap or copy; reactions (kind 7, tagged with the stream and the comment, one per person, deletable) and zap totals show on the comment for every viewer; the zap goes to the commenter, for that comment, on that stream. The composer is the one the other chats use (drafts kept per stream). A reaction no relay took is taken back. (Begun by Codex; finished here.)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'tools/live-chat-actions-test.js')
-rw-r--r--tools/live-chat-actions-test.js74
1 files changed, 74 insertions, 0 deletions
diff --git a/tools/live-chat-actions-test.js b/tools/live-chat-actions-test.js
new file mode 100644
index 0000000..4e4d4ff
--- /dev/null
+++ b/tools/live-chat-actions-test.js
@@ -0,0 +1,74 @@
+// Real live-chat controls with local relays and a fake payment handler.
+import assert from 'node:assert/strict';
+import puppeteer from './puppeteer.js';
+const app = await Bun.file('src/app.js').text();
+const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
+const entry = `
+import {messagesFeature} from './src/features/messages.js';
+import {getPublicKey} from 'nostr-tools/pure';
+import {liveReactions, zapAddrOf} from './src/live.js';
+${dom}
+const sk = new Uint8Array(32).fill(1), me = getPublicKey(sk), fan = '2'.repeat(64);
+const ev = {id:'3'.repeat(64),pubkey:fan,kind:1311,created_at:1,content:'A viewer comment',tags:[]};
+const w = {a:'30311:'+'4'.repeat(64)+':stream',lines:new Map([[ev.id,ev]]),actions:new Map(),reacting:new Set(),relays:['wss://fixture'],stops:[],sheet:null};
+const ui = {}, calls = [], toasts = [];
+window.__published=[]; window.__publishOK=true;
+let feature;
+const wallet = {mnemonic:'test',nostr:{pk:me,sk},nostrRelays:()=>[],loadFeatureState:()=>({}),saveFeatureState(){},registerCacheExtension(){}};
+function render(){if(feature) document.querySelector('#app').replaceChildren(feature.testLine(ev,w,liveReactions([...w.actions.values()])), ...(w.sheet?[feature.testSheet(w)]:[]));}
+feature = messagesFeature({h,ui,wallet,render,toast:s=>toasts.push(s),zapDefaultSat:()=>21,zapSound:()=>false,
+ hook(name,...args){if(name==='arkReady')return true;if(name==='zapNpub'){calls.push(args);return true;}return null;}});
+feature.testStart(w,ev); render();
+window.test={ui,w,ev,me,fan,calls,toasts,render,addr:()=>zapAddrOf(ev.id),self:()=>{ev.pubkey=me;render();},other:()=>{ev.pubkey=fan;render();},settle:()=>feature.zapSettled(ev.id,true,21)};
+`;
+const built = await Bun.build({entrypoints:['test-entry'],target:'browser',plugins:[{name:'test',setup(b){
+ b.onResolve({filter:/^test-entry$/},()=>({path:'entry',namespace:'test'}));
+ b.onLoad({filter:/.*/,namespace:'test'},()=>({contents:entry,loader:'js',resolveDir:process.cwd()}));
+ b.onLoad({filter:/src\/features\/messages\.js$/},async({path})=>({loader:'js',contents:(await Bun.file(path).text()).replace("id: 'messages',", "id: 'messages', testLine: liveLine, testSheet: liveMessageSheet, testStart(w,e){liveWatch=w;setZapAddr(e.id,w.a);},")}));
+ b.onLoad({filter:/src\/nostr\.js$/},async({path})=>({loader:'js',contents:(await Bun.file(path).text())
+ .replace(/export async function publishOn\(relays, evt, \{[^}]*\} = \{\}\) \{/, (m) => m + ' window.__published.push(evt); return window.__publishOK;')
+ .replace('export function subscribeOn(relays, filter, onEvent, extra = {}) {','export function subscribeOn(relays, filter, onEvent, extra = {}) { return () => {};')
+ .replace('export async function queryOn(relays, filter, maxWait = 1500) {','export async function queryOn(relays, filter, maxWait = 1500) { return [];') }));
+}}]});
+assert(built.success,built.logs.join('\n'));
+const html = '<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>'+await Bun.file('src/style.css').text()+'</style><div id="app"></div><script type="module">'+await built.outputs[0].text()+'</script>';
+const server=Bun.serve({port:0,fetch:()=>new Response(html,{headers:{'content-type':'text/html'}})});
+const browser=await puppeteer.launch({executablePath:'/usr/bin/google-chrome',headless:true,args:['--no-sandbox']});
+const page=await browser.newPage(), errors=[];
+page.on('pageerror',e=>errors.push(e.message));
+await page.setRequestInterception(true);
+page.on('request',r=>r.url().startsWith(server.url.origin)?r.continue():r.abort());
+const pause=ms=>new Promise(r=>setTimeout(r,ms));
+try {
+ await page.setViewport({width:390,height:844});
+ await page.goto(server.url.href);
+ await page.waitForSelector('.chat-zap', {timeout:5000}).catch(async e => { throw new Error(errors.join(' | ') || e.message); });
+ await page.click('.chat-zap');
+ assert(await page.evaluate(()=>test.calls.length===1 && test.calls[0][0]===test.fan && test.calls[0][2]===test.ev.id && test.addr()===test.w.a),'zap must target the commenter, comment and stream');
+ await page.evaluate(()=>{test.settle();test.self();});
+ assert(!(await page.$('.chat-zap')),'no self-zap button');
+ await page.evaluate(()=>test.other());
+ await page.click('.chat-bubble');
+ await page.waitForSelector('.msg-sheet-emojis');
+ await page.evaluate(()=>[...document.querySelectorAll('.msg-sheet-emojis button')].find(b=>b.textContent==='❤️').click());
+ await page.waitForSelector('.chat-react.on:not(.zap-tally)');
+ const reaction=await page.evaluate(()=>window.__published.find(e=>e.kind===7));
+ assert(reaction.tags.some(t=>t[0]==='k'&&t[1]==='1311'));
+ await page.click('.chat-react.on:not(.zap-tally)'); await pause(100);
+ assert(!(await page.$('.chat-react.on:not(.zap-tally)')),'tapping again removes our reaction');
+ await page.click('.chat-bubble');
+ await page.evaluate(()=>[...document.querySelectorAll('.msg-sheet-emojis button')].find(b=>b.textContent==='❤️').click());
+ await page.waitForSelector('.chat-react.on:not(.zap-tally)');
+ assert(await page.evaluate(()=>new Set(window.__published.filter(e=>e.kind===7).map(e=>e.id)).size===2),'immediately re-adding a removed reaction must have a new event id');
+ await page.click('.chat-react.on:not(.zap-tally)'); await pause(100);
+ await page.evaluate(()=>window.__publishOK=false);
+ await page.click('.chat-bubble');
+ await page.evaluate(()=>[...document.querySelectorAll('.msg-sheet-emojis button')].find(b=>b.textContent==='❤️').click());
+ await page.waitForFunction(() => !document.querySelector('.chat-react.on:not(.zap-tally)'), {timeout:3000});
+ assert(await page.evaluate(()=>test.toasts.length>0));
+ const box = await (await page.$('.chat-zap')).boundingBox();
+ await page.mouse.move(box.x+box.width/2,box.y+box.height/2); await page.mouse.down(); await pause(650); await page.mouse.up();
+ assert(await page.evaluate(()=>test.ui.zapSetup.pk===test.fan && test.ui.zapSetup.eventId===test.ev.id && test.ui.zapSetup.edit),'hold opens the commenter’s zap amount settings');
+ assert.deepEqual(errors,[]);
+ console.log('✓ Live chat zap recipient/event/stream, no self-zaps, emoji actions, toggle/re-add and rejection rollback');
+} finally {await browser.close();server.stop(true);}