diff options
| author | Adam Soltys <[email protected]> | 2026-09-18 23:12:18 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-18 23:12:18 -0700 |
| commit | 703ee6e1281e72ec00f65768d7de7a015c242abb (patch) | |
| tree | 8a643d0f1ad6d6e49db210aec45341a7b119e5ab /tools/account-access-test.js | |
| parent | 9e0f9528cd6726836386658c4f38ba8a42bea3ac (diff) | |
| download | coinosv3-703ee6e1281e72ec00f65768d7de7a015c242abb.tar.gz coinosv3-703ee6e1281e72ec00f65768d7de7a015c242abb.zip | |
Accounts from the header, snapshot-first Spending restore, name ownership grants.
Header buttons always leave the current feature screen; an account with
no remembered nostr identity opens Accounts instead of nothing. Logout
confirms in place. A scan finishing after logout or an account switch no
longer drops the new session into offline mode (stateGeneration guards in
sync + ark). Spending restores from the snapshot first. A migration grant
names the identity that owns an address; its manager may register it
without becoming that identity (names/ownership.js). Browser tests for
account access, profile retry, spending restore, thread reply focus,
custom-emoji reactions and registry ownership.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools/account-access-test.js')
| -rw-r--r-- | tools/account-access-test.js | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/tools/account-access-test.js b/tools/account-access-test.js new file mode 100644 index 0000000..ad6ce3f --- /dev/null +++ b/tools/account-access-test.js @@ -0,0 +1,82 @@ +// 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-core'; +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 Accounts label', await page.$eval('.header-avatar', (e) => e.getAttribute('aria-label') === 'Accounts')); + await page.click('.header-avatar'); + check('avatar opens Accounts with sign-in and logout', await page.evaluate(() => ['Accounts', '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'); + // 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"]'); + await click('Accounts'); + const onAccounts = () => page.evaluate(() => [...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts')); + for (const label of ['Search', 'Messages', 'Your profile', 'Settings']) { + check(label + ' starts on Accounts', await onAccounts()); + await page.click('[aria-label="' + label + '"]'); + check(label + ' opens from Accounts', await page.evaluate(label => { + const nav = history.state.nav; + return nav.screen === 'wallet' && ![...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts') + && (label === 'Search' ? !!document.querySelector('.user-search-input') + : label === 'Messages' ? nav.chatOpen && nav.msgView === 'home' + : label === 'Your profile' ? !!document.querySelector('.npub-box') && nav.profilePk === 'a'.repeat(64) + : nav.tab === 'settings'); + }, label)); + await page.goBack(); + await page.waitForFunction(() => [...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts')); + check('Back returns from ' + label + ' to Accounts', await onAccounts()); + } + await page.click('[aria-label="Lock wallet"]'); + check('Lock opens its password prompt from Accounts', 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.querySelectorAll('h3')].some(e => e.textContent === 'Accounts'))); + assert.deepEqual(errors, []); + check('no browser errors', true); +} finally { await browser.close(); server.stop(true); } |
