diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 23:54:55 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 23:54:55 -0700 |
| commit | 5682cc88e54f00ce01fdd2ef01e5bb1a207b1be2 (patch) | |
| tree | 0c151cbf72d4b2e63053416b738fb82f0612ec82 /tools/lock-wallet-test.js | |
| parent | 9a9e2a7e64e02d080d121e1f3079573d00ab28cf (diff) | |
| download | coinosv3-5682cc88e54f00ce01fdd2ef01e5bb1a207b1be2.tar.gz coinosv3-5682cc88e54f00ce01fdd2ef01e5bb1a207b1be2.zip | |
Call it 'Lock wallet', because that is what it does
'Logout' promises a return to a fresh app with nothing of yours left on
it. This closes the open wallet and keeps everything saved, which is why
it read as interchangeable with 'Clear all' — the one that really does
send you back to the beginning.
Renamed via a new key rather than rewording the old one: nineteen locale
tables carry their own word for 'Logout' and t() prefers the locale's
value, so reusing the key would have kept the wrong meaning in every
language but English. The dead key is dropped from those files; they
fall back to English until translated.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'tools/lock-wallet-test.js')
| -rw-r--r-- | tools/lock-wallet-test.js | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/tools/lock-wallet-test.js b/tools/lock-wallet-test.js new file mode 100644 index 0000000..4bc56b1 --- /dev/null +++ b/tools/lock-wallet-test.js @@ -0,0 +1,107 @@ +// "Lock wallet" locks; only "Clear all" deletes. +// +// Wallets save themselves to this device under an empty password unless the +// user sets one, so the unlock screen was demanding a password nobody had +// chosen — which made the action look like it had done something drastic. It +// hasn't: locking keeps every saved wallet. +// +// Run: bun tools/logout-test.js +import puppeteer from 'puppeteer-core'; +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 ? ' — ' + String(d).slice(0, 160) : ''}`); if (!c) ok = false; }; + +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 5235, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +const body = () => page.evaluate(() => document.body.innerText); +const click = (sel, t) => page.evaluate((s, x) => { const e = [...document.querySelectorAll(s)].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(); return true; } return false; }, sel, t); +const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await body()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; +const logout = async () => { + await page.evaluate(() => document.querySelector('.header-avatar')?.click()); + await sleep(400); + await click('.icon-select-item', 'Lock wallet'); + await sleep(1500); +}; +const vaultSize = () => page.evaluate(() => (localStorage.getItem('btc-wallet-vault') || '').length); + +try { + await page.goto('http://localhost:5235/', { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await page.reload({ waitUntil: 'domcontentloaded' }); + await sleep(400); + await click('button', 'I already have a wallet'); + await sleep(300); + await click('button', 'Import existing'); + await sleep(300); + await page.waitForSelector('textarea'); + await page.type('textarea', generateMnemonic(wordlist)); + await click('button', 'Open wallet'); + check('wallet open', await waitText('receive', 15000)); + await sleep(2500); + const savedBytes = await vaultSize(); + check('it saved itself to the device', savedBytes > 0, `${savedBytes} bytes of vault`); + + console.log('\n[locking with no password set]'); + await logout(); + let txt = await body(); + check('no password is demanded', !/enter your password|unlock saved wallets/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); + check('the saved wallet is still there', (await vaultSize()) === savedBytes); + check('and it is offered back', /wallet 1|wallets/i.test(txt), txt.slice(0, 140).replace(/\n+/g, ' | ')); + + const listed = await page.evaluate(() => [...document.querySelectorAll('button, .item, .acct')].map((e) => e.textContent.trim()).filter((x) => /Wallet 1/.test(x))); + check('and only once — no phantom duplicate', listed.length === 1, JSON.stringify(listed)); + + console.log('\n[getting back in]'); + await page.evaluate(() => { const e = [...document.querySelectorAll('*')].find((n) => n.children.length === 0 && /Wallet 1/.test(n.textContent) && n.closest('button, .item, .acct')); if (e) e.closest('button, .item, .acct').click(); }); + await sleep(2000); + check('one tap returns to the wallet', await waitText('receive', 15000)); + + console.log('\n[now with a password]'); + // "Change password" lives on the Wallets screen, behind the header selector + await page.evaluate(() => { const b = [...document.querySelectorAll('.row.between button')].find((e) => /Wallet/.test(e.textContent)); if (b) b.click(); }); + await sleep(1200); + check('on the wallets screen', /wallets/i.test(await body())); + await click('button', 'Change password'); + await sleep(1000); + const fields = await page.$$('input[type=password]'); + check('the change-password form is up', fields.length >= 3, `${fields.length} field(s)`); + // current (empty) / new / confirm + await fields[fields.length - 2].type('hunter22'); + await fields[fields.length - 1].type('hunter22'); + await click('button', 'Save'); + const pwSet = await waitText('password changed', 8000); + check('a password is actually set', pwSet, (await body()).slice(0, 120).replace(/\n+/g, ' | ')); + + // back into the wallet — the Wallets screen has no avatar menu to log out from + await click('button', 'Back'); + await sleep(1500); + check('back in the wallet', await waitText('receive', 15000)); + await logout(); + txt = await body(); + check('now it does ask', /unlock saved wallets|enter your password/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); + check('the wallet is still saved', (await vaultSize()) > 0); + + console.log('\n[clear all is the destructive one]'); + await click('button', 'Use another wallet'); + await sleep(1500); + await page.evaluate(() => { const b = [...document.querySelectorAll('.row.between button')].find((e) => /Wallet/.test(e.textContent)); if (b) b.click(); }); + await sleep(1200); + await click('button', 'Clear all'); + await sleep(800); + const txt2 = await body(); + check('it warns before wiping', /removes every wallet/i.test(txt2), txt2.slice(0, 140).replace(/\n+/g, ' | ')); + check('and it has not wiped anything yet', (await vaultSize()) > 0); +} catch (e) { + check('run completed', false, e.message); +} finally { + await browser.close(); + server.stop(true); +} +console.log(ok ? '\n✅ locking keeps your wallets' : '\n❌ failures above'); +process.exit(ok ? 0 : 1); |
