aboutsummaryrefslogtreecommitdiffstats
path: root/tools/nip29-group-test.js
blob: 2ff1192ace839ce8645ef22b6f9894eafb60eaa9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// NIP-29 relay groups in Messages: New group → Relay group → paste an
// address → Join. Reads the real "pyramid runners" group off
// pyramid.fiatjaf.com (relays: 'read' — nothing published leaves the page:
// the join request, the kind 10009 list and the test message are answered
// OK in place and checked here).
// Run: bun tools/nip29-group-test.js
import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
const ADDR = process.env.GROUP || "https://chachi.chat/pyramid.fiatjaf.com/Tnq7x2ZTgrPZWFrC";
const GID = 'Tnq7x2ZTgrPZWFrC';

const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
const errs = [];
page.on('console', (m) => { if (/nip29/.test(m.text())) console.log('  [page]', m.text()); });
page.on('pageerror', (e) => errs.push(String(e)));
const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase() === x); if (e) { e.click(); return true; } return false; }, t);
const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; };
const published = () => page.evaluate(() => window.__relayGuard.events);

try {
  await page.setViewport({ width: 390, height: 844 });
  await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
  await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
  await page.reload({ waitUntil: 'domcontentloaded' });
  await sleep(400);
  await page.evaluate(() => [...document.querySelectorAll('button')].find((n) => /create a new wallet/i.test(n.textContent))?.click()); await sleep(600);
  await seedWords(page);
  await skipSeedCheck(page);
  await waitText('receive', 20000);
  await page.click('[aria-label="Messages"]');
  await waitText('new group', 10000);
  await click('new group'); await sleep(300);
  check('New group offers a relay group', await click('relay group'));
  await sleep(200);
  await page.type('.nip29-address', 'not an address');
  await click('join'); await sleep(500);
  check('a bad address is refused', await waitText("isn't a group address", 3000));
  await page.evaluate(() => { const i = document.querySelector('.nip29-address'); i.value = ''; i.dispatchEvent(new Event('input', { bubbles: true })); });
  await page.type('.nip29-address', ADDR);
  await click('join');
  check('the group opens, named by its relay', await waitText('pyramid runners', 20000));
  check('its messages arrive', await waitText('paff.club', 20000));
  const evs = await published();
  const join = evs.find((e) => e.kind === 9021);
  check('a join request went to the group', !!join && join.tags.some((x) => x[0] === 'h' && x[1] === GID));
  // the 10009 write reads the newest list first, everywhere (a first write)
  let list = null;
  for (let i = 0; i < 80 && !list; i++) { list = (await published()).filter((e) => e.kind === 10009).pop(); if (!list) await sleep(250); }
  check('the group is added to the kind 10009 list', !!list && list.tags.some((x) => x[0] === 'group' && x[1] === GID && /pyramid\.fiatjaf\.com/.test(x[2])), JSON.stringify(list?.tags));

  // say something (answered OK in place, never sent)
  await page.click('#msg-draft');
  await page.type('#msg-draft', 'hello from the nip29 test');
  await page.keyboard.press('Enter');
  await sleep(1500);
  const msg = (await published()).find((e) => e.kind === 9 && e.content === 'hello from the nip29 test');
  check('a message is a kind 9 with the group h tag', !!msg && msg.tags.some((x) => x[0] === 'h' && x[1] === GID), JSON.stringify(msg?.tags));
  check('...and previous tags from the timeline', !!msg && msg.tags.some((x) => x[0] === 'previous' && /^[0-9a-f]{8}$/.test(x[1])));
  check('it shows in the group', await waitText('hello from the nip29 test', 3000));

  // members: the relay's list
  await page.evaluate(() => document.querySelector('.chat-head .clickable')?.click()); await sleep(800);
  check('the member panel says it is a relay group', await waitText('not encrypted', 3000));
  const n = await page.evaluate(() => document.querySelectorAll('.chat-members .chat-thread-row').length);
  check('members come from the relay', n > 10, n + ' members');

  // back home: a row of its own; reload keeps it
  await page.evaluate(() => document.querySelector('.chat-head button')?.click()); await sleep(600);
  check('the group is listed in Messages', await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => r.textContent.includes('pyramid runners'))));
  await page.reload({ waitUntil: 'domcontentloaded' });
  await waitText('receive', 20000);
  await page.click('[aria-label="Messages"]');
  let kept = false;
  for (let i = 0; i < 40 && !kept; i++) { kept = await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => r.textContent.includes('pyramid runners'))); if (!kept) await sleep(250); }
  check('...and still is after a reload', kept);

  // leave: two taps, a 9022 to the relay, and off the list
  await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].find((r) => r.textContent.includes('pyramid runners'))?.click()); await sleep(800);
  await page.click('[aria-label="Leave group"]'); await sleep(200);
  await page.click('[aria-label="Leave group"]');
  let left = null, relist = null;
  for (let i = 0; i < 80 && !(left && relist); i++) {
    const evs = await published();
    left = evs.find((e) => e.kind === 9022);
    relist = evs.filter((e) => e.kind === 10009).find((e) => !e.tags.some((x) => x[0] === 'group'));
    if (!(left && relist)) await sleep(250);
  }
  check('leaving tells the relay (9022)', !!left && left.tags.some((x) => x[0] === 'h' && x[1] === GID));
  check('...and takes it off the 10009 list', !!relist);
  check('...and out of Messages', !(await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => r.textContent.includes('pyramid runners')))));
  check('no page errors', !errs.length, errs.join(' | '));
} catch (e) { console.error(e); ok = false; } finally {
  await browser.close(); server.stop();
}
console.log(ok ? '\nPASS' : '\nFAIL');
process.exit(ok ? 0 : 1);