// 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 = '
'; 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);}