aboutsummaryrefslogtreecommitdiffstats
path: root/tools/cordn-ui-test.js
blob: 8fc707a2623f18663f4c37738920f97f5c0d37f5 (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
// Cordn in the app, end to end, against the public coordinator: a fresh
// wallet makes a Cordn group from Messages → New group, invites Alice (the
// official @cordn/cli), sends; Alice's CLI accepts the Welcome, reads it and
// answers; the app shows her answer. Needs the CLI rig (see
// cordn-interop-test.js).
//
// Run: bun tools/cordn-ui-test.js [screenshot.png]
import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { npubEncode } from 'nostr-tools/nip19';
import { buildHtml } from '../build.js';

const CLI_DIR = process.env.CORDN_CLI_DIR || '/tmp/claude-1000/-home-adam-coinosv3/77efc6a1-2602-46f7-b851-5580b0742cf1/scratchpad/cordn/cli';
const CLI = `${CLI_DIR}/node_modules/.bin/cordn`;
const ALICE_STATE = `${CLI_DIR}/alice.json`;
const shotPath = process.argv[2] || '/tmp/cordn-ui-test.png';
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; return c; };
function alice(cmd) {
  const p = Bun.spawnSync([CLI, '--state-file', ALICE_STATE, '--command', cmd], { timeout: 90_000, stdout: 'pipe', stderr: 'pipe' });
  const out = (p.stdout.toString() + p.stderr.toString()).replace(/\x1b\[[0-9;]*m/g, '').trim();
  console.log(`   [alice] ${cmd}\n      ${out.split('\n').slice(-4).join('\n      ')}`);
  return out;
}
const apk = /stablePubkey:\s*([0-9a-f]{64})/.exec(alice('status'))?.[1];
const aliceRef = /ref=([0-9a-f]{64})/.exec(alice('gen-kp kp' + Date.now().toString(36)))?.[1];

const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5234, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
const browser = await puppeteer.launch({ relays: 'live', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
const errs = [];
page.on('pageerror', (e) => { errs.push(e.message); console.log('  [pageerror]', e.message); });
const click = (sel, t) => page.evaluate((s, x) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase() === x.toLowerCase()) || [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(); return true; } return false; }, sel, t);
const waitFor = async (fn, ms = 25000, arg) => { for (let i = 0; i < ms / 250; i++) { if (await page.evaluate(fn, arg)) return true; await sleep(250); } return false; };
const bodyText = () => page.evaluate(() => document.body.innerText);

try {
  await page.setViewport({ width: 420, height: 860 });
  await page.goto('http://localhost:5234/', { waitUntil: 'domcontentloaded' });
  await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
  await page.reload({ waitUntil: 'domcontentloaded' });
  await sleep(400);
  await click('button', 'Create a new wallet'); await sleep(300);
  await click('button', 'Have an existing seed'); await sleep(300);
  await page.waitForSelector('textarea');
  await page.type('textarea', generateMnemonic(wordlist));
  await click('button', 'Open wallet');
  await waitFor(() => /receive/i.test(document.body.innerText), 15000);
  await page.evaluate(() => [...document.querySelectorAll('.app-nav-button')].find((b) => /message/i.test(b.textContent))?.click());
  check('messages screen', await waitFor(() => /New group/.test(document.body.innerText), 15000));

  // New group → Cordn → Alice → create
  await click('button', 'New group'); await sleep(300);
  check('protocol choice shown', await waitFor(() => [...document.querySelectorAll('.seg button')].some((b) => b.textContent.trim() === 'Cordn'), 5000));
  await click('.seg button', 'Cordn'); await sleep(200);
  await page.type('input[placeholder="Group name"]', 'UI test group');
  await page.type('input[placeholder^="Add people"]', npubEncode(apk));
  await page.keyboard.press('Enter');
  check('alice chip shows reachable', await waitFor(() => [...document.querySelectorAll('.chat-react.on')].some((c) => !/checking|can't/.test(c.textContent)), 30000), (await bodyText()).match(/checking…|can't join[^\n]*/)?.[0]);
  await click('button', 'Create group');
  check('group screen opened', await waitFor(() => !!document.querySelector('.chat-title') && /UI test group/.test(document.querySelector('.chat-title').textContent), 60000));
  await page.type('#msg-draft', 'hello from the coinos app');
  await click('button', 'Send');
  check('our message is on screen', await waitFor(() => [...document.querySelectorAll('.chat-bubble')].some((b) => b.textContent.includes('hello from the coinos app')), 10000));

  // Alice's side
  alice('fetch-welcomes');
  const id = alice('welcomes').split('\n').map((l) => /([0-9a-f]{64}:([0-9a-f]{64}):\d+)/.exec(l)).filter(Boolean).sort((a, b) => Number(b[1].split(':')[2]) - Number(a[1].split(':')[2]))[0]?.[1]; // the newest: the one just stored
  check('alice got the Welcome', !!id);
  const alias = 'ui' + Date.now().toString(36);
  if (id) {
    alice(`accept-welcome ${id} ${alias}`);
    alice(`sync ${alias}`);
    check("alice reads the app's message", alice(`messages ${alias}`).includes('hello from the coinos app'));
    alice(`send-to ${alias} hello app, from the cli`);
    check("the app shows alice's reply", await waitFor(() => [...document.querySelectorAll('.chat-bubble')].some((b) => b.textContent.includes('hello app, from the cli')), 30000));
    check('member list shows both', await waitFor(() => /2 members/.test(document.body.innerText), 5000));
  }
  await sleep(500);
  await page.screenshot({ path: shotPath });
  // back home: the group row is in the list with the last message
  await page.evaluate(() => document.querySelector('.chat-head button')?.click());
  check('home lists the group', await waitFor(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => /UI test group/.test(r.textContent)), 5000));
  check('no page errors', errs.length === 0, errs.join(' | ').slice(0, 300));
  console.log(ok ? '\nUI OK' : '\nFAILED');
} finally { await browser.close(); server.stop(); process.exit(ok ? 0 : 1); }