aboutsummaryrefslogtreecommitdiffstats
path: root/tools/account-access-test.js
blob: c9af32e4b5a2e015a7d722d868d14faa642a7b98 (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
// A watch-only account without remembered Nostr metadata must still offer
// account switching and logout. Uses an unfunded, generated public key only.
// Run: bun tools/account-access-test.js
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { accountXpubFor, cacheKeyFor } from '../src/wallet.js';
import { buildHtml } from '../build.js';

const xpub = accountXpubFor({ mnemonic: generateMnemonic(wordlist) });
const html = await buildHtml({ minify: false, pwa: false });
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();
const errors = [];
page.on('pageerror', (e) => errors.push(e.stack));
await page.setRequestInterception(true);
page.on('request', (r) => r.url().startsWith(server.url.origin) ? r.continue() : r.abort());
const check = (name, value) => { assert(value, name); console.log(' ✓ ' + name); };
const click = (text) => page.evaluate((label) => {
  const button = [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === label);
  if (!button) throw new Error('Missing button: ' + label);
  button.click();
}, text);
try {
  await page.goto(server.url.href);
  await page.evaluate((xpub) => {
    localStorage.setItem('btc-wallet-watch', JSON.stringify([{ id: 'test-watch', label: 'Saved wallet', xpub }]));
  }, xpub);
  await page.reload();
  await page.waitForSelector('.header-avatar');
  check('fresh local account defaults to mainnet', await page.evaluate(() => JSON.parse(localStorage.getItem('btc-wallet-watch'))[0].network === 'mainnet'));
  check('anonymous avatar has an accessible label', await page.$eval('.header-avatar', (e) => e.getAttribute('aria-label') === 'Account settings'));
  await page.click('.header-avatar');
  check('avatar opens the wallet settings with sign-in and logout', await page.evaluate(() => ['Security', 'Sign into another account', 'Log out'].every((s) => document.body.innerText.includes(s))));
  const saved = await page.evaluate(() => localStorage.getItem('btc-wallet-watch'));
  await click('Sign into another account');
  check('sign-in opens on Create new, not the seed import box', await page.evaluate(() =>
    document.querySelector('.tabs .active')?.textContent.includes('Create new') && !document.querySelector('.card textarea')));
  // Expand the Nostr login card without supplying a private key.
  await click('Sign in with Nostr');
  await page.waitForSelector('input[placeholder="nsec, private key, or bunker://…"]');
  check('Nostr key sign-in is reachable without deleting the old wallet', await page.evaluate((saved) => localStorage.getItem('btc-wallet-watch') === saved, saved));
  await click('← Back');
  await page.waitForSelector('.header-avatar');
  await page.click('.header-avatar');
  await click('Log out');
  check('logout asks before ending the session', await page.evaluate(() => document.body.innerText.includes('Your accounts stay saved on this device.')));
  await click('Log out');
  check('logout reaches the sign-in screen and retains the saved wallet', await page.evaluate((saved) => !document.querySelector('.header-avatar') && document.body.innerText.includes('Sign in with Nostr') && localStorage.getItem('btc-wallet-watch') === saved, saved));
  // Remember a public login identity on the watch-only account so every
  // header destination is available, without supplying a private key.
  await page.evaluate(({ key }) => {
    localStorage.setItem(key + ':nostrlogin', JSON.stringify({ pubkey: 'a'.repeat(64), type: 'extension' }));
    history.replaceState(null, '');
  }, { key: cacheKeyFor(xpub) });
  await page.reload();
  await page.waitForSelector('[aria-label="Messages"]');
  // the balance card's gear: a page like any other under the header
  await page.click('.balance-gear');
  const onSettings = () => page.evaluate(() => history.state.nav.screen === 'accountSettings' && [...document.querySelectorAll('h3')].some(e => e.textContent === 'Security'));
  for (const label of ['Search', 'Messages', 'Your profile', 'Settings']) {
    check(label + ' starts on the wallet settings', await onSettings());
    await page.click('[aria-label="' + label + '"]');
    check(label + ' opens from the wallet settings', await page.evaluate(label => {
      const nav = history.state.nav;
      return nav.screen === 'wallet'
        && (label === 'Search' ? !!document.querySelector('.user-search-input')
          : label === 'Messages' ? nav.chatOpen && nav.msgView === 'home'
          : label === 'Your profile' ? nav.profilePk === 'a'.repeat(64)
          : nav.tab === 'settings');
    }, label));
    await page.goBack();
    await page.waitForFunction(() => history.state.nav.screen === 'accountSettings');
    check('Back returns from ' + label + ' to the wallet settings', await onSettings());
  }
  await page.click('[aria-label="Lock wallet"]');
  check('Lock opens its password prompt from the wallet settings', await page.evaluate(() => !!document.querySelector('input[type="password"]')));
  await click('Not now');
  await page.click('.brand');
  check('the logo returns to the wallet', await page.evaluate(() => history.state.nav.screen === 'wallet' && !!document.querySelector('.balance-gear')));
  assert.deepEqual(errors, []);
  check('no browser errors', true);
} finally { await browser.close(); server.stop(true); }