summaryrefslogtreecommitdiffstats
path: root/tools/ark/ui-test.js
blob: 232e742e97a25cfe5fb0883a43696bf360915e87 (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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Browser-level e2e of the Ark UI wiring, driven with puppeteer against the
// real dev server + the ark regtest stack:
//
//   import wallet -> ark connects -> receive over Ark (alice pays the address
//   shown in the Receive tab) -> send over Ark from the Send form -> board
//   from the Settings card -> refresh/consolidate -> balances line up.
//
// Usage: bun tools/ark/ui-test.js   (regtest stack up + nothing on :5199)

import { spawn, execSync } from 'node:child_process';
import puppeteer from 'puppeteer-core';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';

const PORT = 5199;
const APP = `http://localhost:${PORT}`;
const BARK = `${process.env.HOME}/bark/target/debug/bark`;
const ALICE = `${BARK} --datadir ${process.env.HOME}/ark-regtest/alice`;
const BCLI = `bitcoin-cli -regtest -datadir=${process.env.HOME}/ark-regtest/bitcoind -rpcport=18543 -rpcwallet=miner`;

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const sh = (cmd) => execSync(cmd, { shell: '/bin/bash' }).toString().trim();
const mine = (n = 1) => sh(`${BCLI} generatetoaddress ${n} $(${BCLI} getnewaddress) >/dev/null`);
const aliceBalance = () => JSON.parse(sh(`${ALICE} -q balance`)).spendable_sat;

let ok = true;
const check = (name, cond, detail = '') => {
  console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ' — ' + detail : ''}`);
  if (!cond) ok = false;
};

// --- dev server ---
const dev = spawn('bun', ['run', 'dev'], { cwd: process.env.HOME + '/halwallet', env: { ...process.env, PORT: String(PORT) }, stdio: 'ignore' });
for (let i = 0; i < 30; i++) {
  try { if ((await fetch(APP)).ok) break; } catch {}
  await sleep(500);
}

const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
page.on('pageerror', (e) => console.log('  [pageerror]', e.message));

// helpers — NB innerText respects CSS text-transform, so match case-insensitively
const bodyText = () => page.evaluate(() => document.body.innerText);
const clickText = async (sel, text) => {
  const found = await page.evaluate((sel, text) => {
    const el = [...document.querySelectorAll(sel)].find((e) => e.textContent.trim().toLowerCase().includes(text.toLowerCase()));
    if (el) { el.click(); return true; }
    return false;
  }, sel, text);
  if (!found) throw new Error(`clickText: no ${sel} with "${text}"`);
};
const waitText = async (text, ms = 20000) => {
  for (let i = 0; i < ms / 250; i++) {
    if ((await bodyText()).toLowerCase().includes(text.toLowerCase())) return true;
    await sleep(250);
  }
  return false;
};
// The number shown next to a label like "ark balance" (sats mode, comma groups)
const labeledNumber = async (label) => {
  const txt = await bodyText();
  const esc = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const m = txt.match(new RegExp(esc + String.raw`[\s\n]*([\d,]+)`, 'i'));
  return m ? Number(m[1].replaceAll(',', '')) : null;
};
const waitLabeledNumber = async (label, want, ms = 30000) => {
  for (let i = 0; i < ms / 500; i++) {
    if (await labeledNumber(label) === want) return true;
    await sleep(500);
  }
  return false;
};

try {
  // --- configure: regtest, esplora on the ark chain, ark local ---
  await page.goto(APP, { waitUntil: 'domcontentloaded' });
  await page.evaluate(() => {
    localStorage.setItem('btc-wallet-network', 'regtest');
    localStorage.setItem('btc-wallet-source:regtest', JSON.stringify({ id: 'custom', url: 'http://localhost:30002' }));
    localStorage.setItem('btc-wallet-ark-provider:regtest', 'local');
  });
  await page.reload({ waitUntil: 'domcontentloaded' });
  await sleep(500);

  // --- import a fresh wallet ---
  console.log('\n[1] import wallet + ark connects');
  const mnemonic = generateMnemonic(wordlist);
  await clickText('button', 'Import existing');
  await sleep(200);
  await page.type('textarea', mnemonic);
  await clickText('button', 'Open wallet');
  check('wallet opened', await waitText('receive', 15000));

  check('ark connected (receive option present)', await (async () => {
    for (let i = 0; i < 40; i++) {
      const has = await page.evaluate(() => [...document.querySelectorAll('option')].some((o) => o.value === 'ark'));
      if (has) return true;
      await sleep(500);
    }
    return false;
  })());

  // --- receive over ark ---
  console.log('\n[2] receive over Ark');
  await page.select('select', 'ark');
  check('lazy connect on first use', await waitText('tark1', 15000));
  const arkAddr = await page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim() || '');
  check('ark address shown', /^tark1[a-z0-9]+$/.test(arkAddr), arkAddr.slice(0, 24) + '…');
  sh(`${ALICE} -q send ${arkAddr} "25000 sat"`);
  check('ark balance shows 25,000', await waitLabeledNumber('ark balance', 25000, 30000), String(await labeledNumber('ark balance')));
  check('payment celebration shown', await waitText('Payment received', 10000));
  await clickText('.card', 'Payment received'); // tap to acknowledge
  await sleep(400);
  check('back to receive card after tap', await waitText('tark1', 5000));

  // --- send over ark ---
  console.log('\n[3] send over Ark');
  const aliceAddr = sh(`${ALICE} -q address`);
  const before = aliceBalance();
  await clickText('.tabs button', 'Send');
  await sleep(300);
  await page.type('.mono-input', aliceAddr);
  await sleep(400); // destReady re-render reveals the amount input
  await page.type('input[type="number"]', '7000');
  const noFeeUi = await page.evaluate(() => {
    const txt = document.body.innerText.toLowerCase();
    return !txt.includes('fee rate') && !txt.includes('coin control') && txt.includes('no mining fee');
  });
  check('no fee/coin controls for ark dest', noFeeUi);
  await clickText('button.btn-primary', 'Review transaction');
  check('ark review shown', await waitText('Send over Ark', 5000));
  await clickText('button.btn-primary', 'Send');
  check('sent view', await waitText('Sent!', 15000));
  check('alice +7000', aliceBalance() === before + 7000, String(aliceBalance() - before));
  await clickText('.card', 'Sent!'); // tap anywhere to proceed
  await sleep(300);

  // history shows both ark movements
  await clickText('.tabs button', 'History');
  await sleep(400);
  const hist = await bodyText();
  check('history shows ark receive', /received\s*\n?\s*ark/i.test(hist));
  check('history shows ark send', /sent\s*\n?\s*ark/i.test(hist));

  // rows open a detail view
  await clickText('.item', 'sent');
  await sleep(300);
  check('ark detail view', await waitText('Coin (VTXO)', 5000));
  check('detail shows destination', (await bodyText()).includes('tark1'));
  await clickText('button', 'Back');
  await sleep(300);

  // --- board from settings ---
  console.log('\n[4] board');
  await clickText('.tabs button', 'Receive');
  await sleep(300);
  await page.select('select', 'address');
  await sleep(300);
  const onchainAddr = await page.evaluate(() => document.querySelector('.addr-box').textContent.trim());
  sh(`${BCLI} sendtoaddress ${onchainAddr} 0.001`);
  mine(1);
  check('onchain funds arrive', await waitText('100,000', 30000));
  // the on-chain receive celebration takes over the tab — tap through it
  if ((await bodyText()).toLowerCase().includes('payment received')) {
    await clickText('.card', 'Payment received');
    await sleep(400);
  }
  await page.select('select', 'ark');
  await sleep(400);
  const preBoard = await labeledNumber('ark balance');
  const boardInput = await page.evaluateHandle(() =>
    [...document.querySelectorAll('input')].find((i) => (i.placeholder || '').toLowerCase().startsWith('board amount')));
  check('board form on receive/ark', !!(await boardInput.asElement()));
  await boardInput.asElement().type('30000');
  await clickText('button', 'Board');
  check('boarding success screen', await waitText('Boarding!', 20000));
  check('boarding txid link shown', await page.evaluate(() =>
    !!document.querySelector('.addr-box') && /^[0-9a-f]{64}$/.test(document.querySelector('.addr-box').textContent.trim())));
  await clickText('button.btn-primary', 'Done');
  await sleep(300);
  mine(2);
  check('board vtxo lands (+29,670)', await waitLabeledNumber('ark balance', preBoard + 29670, 40000),
    String(await labeledNumber('ark balance')));

  // --- refresh / consolidate ---
  console.log('\n[5] refresh');
  await clickText('.tabs button', 'Settings');
  await sleep(300);
  await clickText('button', 'Refresh / consolidate');
  check('refresh submitted', await waitText('Refresh submitted', 10000));
  const done = await (async () => {
    for (let i = 0; i < 45; i++) {
      mine(1);
      await sleep(2000);
      if (await labeledNumber('coins (vtxos)') === 1 && !(await bodyText()).toLowerCase().includes('in progress')) return true;
    }
    return false;
  })();
  check('refresh consolidated to 1 coin', done, String(await labeledNumber('coins (vtxos)')));
  const finalBal = await labeledNumber('ark balance');
  console.log(`   final ark balance: ${finalBal} sats`);

  // --- reload: a wallet holding vtxos reconnects on its own ---
  console.log('\n[6] reload auto-connects (has vtxos)');
  await page.reload({ waitUntil: 'domcontentloaded' });
  check('ark balance back without interaction', await waitLabeledNumber('ark balance', finalBal, 20000),
    String(await labeledNumber('ark balance')));

  console.log(ok ? '\n✅ SUCCESS: Ark UI — receive, send, board, refresh all work in the browser'
                : '\n❌ some checks failed');
} finally {
  await browser.close();
  dev.kill();
}
process.exit(ok ? 0 : 1);