aboutsummaryrefslogtreecommitdiffstats
path: root/tools/send-enter-keys-test.js
blob: aac95f8a7090427c34da0fd9dc92478c345c5a73 (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
// Keyboard path through the wallet: swiping the carousel to Savings opens its
// Receive pane (the on-chain address, no extra tap); on Send, Enter in the
// recipient field picks the top suggestion, and Enter after an amount
// presses the screen's main button (Review), Enter again presses the next
// one (confirm). A held Enter (auto-repeat) presses nothing.
// Uses the live registrar for the 'adam' lookup. Spends nothing: the wallet
// is fresh and empty, and the confirm step is checked on a stand-in button.
// Run: bun tools/send-enter-keys-test.js
import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
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 html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: (req) => {
  const p = new URL(req.url).pathname;
  if (p.startsWith('/punks') || p === '/verify-worker.js' || p.startsWith('/locales') || /\.js$/.test(p)) { const f = Bun.file('dist' + p); return f.size ? new Response(f) : new Response('', { status: 404 }); }
  return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
const browser = await puppeteer.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
const errs = []; 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().includes(x)); if (e) { e.click(); return true; } return false; }, t);
const lit = () => page.evaluate(() => document.querySelector('.tabs .active, .tabs [aria-selected=true]')?.textContent.trim() || '');
const addrShown = () => page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim() || '');
// swipe the carousel to a card the way a finger does: scroll the strip, let it settle
const swipeTo = (cls) => page.evaluate((c) => {
  const el = document.querySelector('.bal-carousel'); const k = el && el.querySelector('.' + c);
  if (!k) return false;
  el.scrollTo({ left: k.offsetLeft + k.offsetWidth / 2 - el.clientWidth / 2, behavior: 'auto' });
  el.dispatchEvent(new Event('scroll'));
  return true;
}, cls);
// Replace the main button's action with a recorder, so a press is visible
// and nothing is really sent.
const armPrimary = () => page.evaluate(() => {
  const b = document.querySelector('[data-send-pane] .btn-primary.btn-block:not(:disabled), [data-send-pane] .btn-primary.grow:not(:disabled)');
  if (!b) return null;
  window.__pressed = null;
  b.onclick = () => { window.__pressed = b.textContent.trim(); };
  return b.textContent.trim();
});
try {
  await page.setViewport({ width: 390, height: 844 });
  await page.goto(server.url.origin, { waitUntil: 'domcontentloaded' });
  await sleep(400);
  await click('create a new wallet'); await sleep(500); await click('to import'); await sleep(400);
  await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('open wallet');
  for (let i = 0; i < 80 && !(await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.textContent.trim() === 'Send'))); i++) {
    await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /^(not now|skip|continue|later)$/i.test(b.textContent.trim()))?.click());
    await sleep(400);
  }
  await sleep(3000); // features (names, zaps, ark) load

  console.log('[carousel → Savings]');
  // a fresh wallet is Savings only: set up its Spending side for the carousel
  for (let i = 0; i < 60 && !(await click('set up a spending account')); i++) await sleep(500);
  for (let i = 0; i < 60 && !(await page.$('.bal-carousel .face-savings')); i++) {
    await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /^(not now|skip|continue|later|done)$/i.test(b.textContent.trim()))?.click());
    await sleep(500);
  }
  check('the carousel is up', !!(await page.$('.bal-carousel .face-savings')));
  await swipeTo('face-spending'); await sleep(1200);
  check('on Spending, its history (no pane open)', !(await lit()), await lit());
  await swipeTo('face-savings'); await sleep(1200);
  let a = await addrShown();
  check('Savings opens on Receive', /receive/i.test(await lit()), await lit());
  check('...showing the on-chain deposit address', /^(bc1|tb1|bcrt1)/.test(a), a);
  await swipeTo('face-spending'); await sleep(1200);
  check('back on Spending, history again', !(await lit()), await lit());

  console.log('[recipient: Enter picks the top suggestion]');
  // a REAL click: Chrome focuses the button clicked, and the recipient field
  // must still take the cursor from it
  const openSend = async () => {
    for (const b of await page.$$('button')) if ((await b.evaluate((n) => n.textContent.trim())) === 'Send') { await b.click(); return; }
  };
  await openSend();
  await page.waitForSelector('textarea.send-dest', { timeout: 10000 });
  await sleep(300);
  check('desktop: Send opens with the cursor in the recipient field', await page.evaluate(() => document.activeElement?.matches('textarea.send-dest')),
    await page.evaluate(() => document.activeElement?.outerHTML.slice(0, 80)));
  // away and back, two ways: the cursor lands again each time
  await page.evaluate(() => document.activeElement?.blur());
  await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Receive')?.click()); await sleep(600);
  await openSend(); await sleep(600);
  check('...again after Receive and back to Send', await page.evaluate(() => document.activeElement?.matches('textarea.send-dest')),
    await page.evaluate(() => document.activeElement?.outerHTML.slice(0, 80)));
  await page.evaluate(() => document.activeElement?.blur());
  await swipeTo('face-savings'); await sleep(1200);
  await swipeTo('face-spending'); await sleep(1200);
  await openSend(); await sleep(600);
  check('...and after swiping to Savings and back', await page.evaluate(() => document.activeElement?.matches('textarea.send-dest')),
    await page.evaluate(() => document.activeElement?.outerHTML.slice(0, 80)));
  await page.focus('textarea.send-dest');
  await page.keyboard.type('adam', { delay: 80 });
  let top = '';
  for (let i = 0; i < 40 && !top; i++) { await sleep(250); top = await page.evaluate(() => { const s = document.querySelector('.send-suggest'); return s && s.style.display !== 'none' ? s.querySelector('.item .muted')?.textContent.trim() || s.querySelector('.item')?.textContent.trim() || '' : ''; }); }
  check('suggestions show for "adam"', !!top, top || await page.evaluate(() => document.querySelector('[data-send-pane]')?.innerText.slice(0, 200)));
  await page.keyboard.press('Enter');
  let amt = null;
  for (let i = 0; i < 40 && !amt; i++) { await sleep(250); amt = await page.$('[data-send-pane] input[type=number]'); }
  const field = await page.evaluate(() => document.querySelector('textarea.send-dest')?.value ?? document.querySelector('[data-send-pane] h3')?.textContent);
  check('Enter picked it: the amount step is up', !!amt, field);
  check('no line break went into the field', !/\n/.test(field || ''), JSON.stringify(field));
  await sleep(300);
  check('desktop: the amount field has the cursor already', await page.evaluate(() => !matchMedia('(pointer: coarse)').matches && document.activeElement?.matches('[data-send-pane] input[type=number]')),
    await page.evaluate(() => document.activeElement?.outerHTML.slice(0, 80)));
  // left alone after that: a re-render (unit toggle) doesn't pull it back
  await page.evaluate(() => document.activeElement.blur());
  await page.evaluate(() => document.querySelector('[data-send-pane] button[title]')?.click()); await sleep(300);
  await page.evaluate(() => document.querySelector('[data-send-pane] button[title]')?.click()); await sleep(300);
  check('...only once: a later render leaves focus alone', await page.evaluate(() => document.activeElement === document.body),
    await page.evaluate(() => document.activeElement?.outerHTML.slice(0, 80)));

  console.log('[amount: Enter presses the main button]');
  await page.focus('[data-send-pane] input[type=number]');
  await page.keyboard.type('1000');
  const label = await armPrimary();
  check('there is a main button to press', !!label, label);
  await page.keyboard.down('Enter'); await page.keyboard.up('Enter');
  check('Enter in the amount pressed it', await page.evaluate(() => window.__pressed) === label, label);
  check('...and let go of the field', await page.evaluate(() => document.activeElement === document.body));

  console.log('[nothing focused: Enter presses the next main button]');
  const label2 = await armPrimary();
  await page.keyboard.press('Enter');
  check('Enter with nothing focused pressed it', await page.evaluate(() => window.__pressed) === label2, label2);

  console.log('[held Enter presses nothing]');
  await armPrimary();
  await page.evaluate(() => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', repeat: true, bubbles: true })));
  check('an auto-repeat Enter is ignored', await page.evaluate(() => window.__pressed) === null);

  console.log('[the real thing: adam ⏎ 1000 ⏎ goes to the next screen]');
  await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Send')?.click()); await sleep(400);
  await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Send')?.click());
  await page.waitForSelector('textarea.send-dest', { timeout: 10000 });
  await page.$eval('textarea.send-dest', (e) => { e.value = ''; e.dispatchEvent(new Event('input', { bubbles: true })); }); await sleep(300);
  await page.focus('textarea.send-dest');
  await page.keyboard.type('adam', { delay: 80 });
  await sleep(2500);
  await page.keyboard.press('Enter');
  await page.waitForSelector('[data-send-pane] input[type=number]', { timeout: 10000 });
  await page.focus('[data-send-pane] input[type=number]');
  await page.keyboard.type('1000');
  const before = await page.evaluate(() => document.querySelector('[data-send-pane]')?.innerText || '');
  await page.keyboard.press('Enter');
  await sleep(2500);
  const after = await page.evaluate(() => document.querySelector('[data-send-pane]')?.innerText || '');
  console.log('   screen after ⏎:', JSON.stringify(after.slice(0, 200)));
  check('the screen moved on to the review', after !== before && /Send$/.test(after.trim()), after.slice(-40));
  const confirmLabel = await armPrimary();
  await page.keyboard.press('Enter');
  check('⏎ again presses Send (stand-in: nothing is sent)', confirmLabel === 'Send' && await page.evaluate(() => window.__pressed) === 'Send', confirmLabel);
  check('no page errors', errs.length === 0, errs.join('; ').slice(0, 300));
} catch (e) {
  console.error(e); ok = false;
} finally {
  await browser.close(); server.stop();
}
console.log(ok ? 'PASS' : 'FAIL');
process.exit(ok ? 0 : 1);