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); }
|