diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/balance-settings-row-test.js | 13 | ||||
| -rw-r--r-- | tools/onboard-backup-test.js | 4 | ||||
| -rw-r--r-- | tools/send-lnaddr-advance-test.js | 3 | ||||
| -rw-r--r-- | tools/signup-quick-test.js | 167 |
4 files changed, 182 insertions, 5 deletions
diff --git a/tools/balance-settings-row-test.js b/tools/balance-settings-row-test.js index 89e68d9..38d34b3 100644 --- a/tools/balance-settings-row-test.js +++ b/tools/balance-settings-row-test.js @@ -2,7 +2,9 @@ // balance card's gear opens. "I simply cannot find this option" (Nils, // 2026-11-12) — the Spending gear was the only way in. // Run: bun tools/balance-settings-row-test.js (NO_BUILD=1 to reuse dist/) -import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js'; +import puppeteer from './puppeteer.js'; +import { generateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; 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; }; @@ -32,13 +34,16 @@ try { await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'mainnet')); await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); - await click('create a new wallet'); await sleep(600); - await seedWords(page); - await skipSeedCheck(page); + // on mainnet Create makes a Spending-only account (signup-quick-test); a + // seed wallet with Savings alone comes in through the seed-phrase door + await page.waitForSelector('#onb-signin'); await page.click('#onb-signin'); await sleep(400); + await page.click('#onb-restore'); await sleep(400); + await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('open wallet'); if (await waitText('claim a username', 8000)) { await click('not now'); await sleep(800); } await waitText('receive'); await sleep(1500); check('savings-only wallet lists Savings alone', await (async () => { await openWalletPage(); const v = await page.$$eval('.balance-settings-row', (l) => l.map((e) => e.dataset.view)); await page.goBack(); await page.goBack(); await sleep(500); return v.join() === 'savings'; })()); + await waitText('set up a spending account', 30000); await click('set up a spending account'); await sleep(800); if (await waitText('claim a username', 5000)) { await click('not now'); await sleep(800); } await waitText('receive'); await sleep(800); diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js index 8fc1071..946f342 100644 --- a/tools/onboard-backup-test.js +++ b/tools/onboard-backup-test.js @@ -64,6 +64,10 @@ try { const page = await browser.newPage(); await page.setViewport({ width: 390, height: 844 }); await page.goto('http://localhost:5291/', { waitUntil: 'domcontentloaded' }); + // regtest: the classic phrase flow. Where a name can be claimed (mainnet, + // mutinynet) Create is the quick sign-up instead — signup-quick-test.js. + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(1500); // Two choices and nothing else (2026-11-06): a new wallet, or one you // already have — the sign-in methods wait behind the second. diff --git a/tools/send-lnaddr-advance-test.js b/tools/send-lnaddr-advance-test.js index fd6c1a1..14640ca 100644 --- a/tools/send-lnaddr-advance-test.js +++ b/tools/send-lnaddr-advance-test.js @@ -43,7 +43,8 @@ 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('#onb-signin'); await page.click('#onb-signin'); await sleep(400); + await page.click('#onb-restore'); await sleep(400); await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('open wallet'); // mainnet onboarding: skip the username step (and anything after it) for (let i = 0; i < 80 && !(await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.textContent.trim() === 'Send'))); i++) { diff --git a/tools/signup-quick-test.js b/tools/signup-quick-test.js new file mode 100644 index 0000000..c80547d --- /dev/null +++ b/tools/signup-quick-test.js @@ -0,0 +1,167 @@ +// Signing up is a name and a look, nothing else (Adam, 2026-11-15): "i'd like +// to move away from requiring people to backup and verify a seed phrase to +// sign up. we should just have them claim a name and pick their avatar/cover +// and they should be in. we should also not create a savings account by +// default but let them add it later … adding additional auth methods like +// passkey, nostr, or google should be possible once they've created their +// account." +// +// Mainnet, with the Ark server's info read for real and the names registrar +// mocked (nothing is claimed): Create an account → no twelve words, no check +// → Claim your name (no "Not now") → Choose your look → in, on a Spending-only +// wallet (no carousel, no Savings face); the wallet line asks for a way back +// in; Settings → Wallet shows "Ways back in" (passkey via a CDP virtual +// authenticator → listed, the line goes away) and "Add a savings account" +// (→ the Savings face appears); a reload keeps both. +// Run: bun tools/signup-quick-test.js (NO_BUILD=1 to reuse dist/) +import puppeteer from './puppeteer.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; }; +if (!process.env.NO_BUILD && Bun.spawnSync(['bun', 'build.js']).exitCode !== 0) throw new Error('build failed'); +const server = Bun.serve({ port: 0, fetch: async (req) => { + const path = new URL(req.url).pathname; + const f = Bun.file('dist' + path); + return path.includes('.') && await f.exists() ? new Response(f) : new Response(Bun.file('dist/index.html')); +} }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +const errs = []; +const registered = []; +const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type,authorization', 'access-control-allow-methods': 'GET,POST,DELETE,OPTIONS' }; +await page.setRequestInterception(true); +page.on('request', (r) => { + const u = new URL(r.url()); + if (u.origin === server.url.origin || u.hostname === 'ark.coinos.io') return r.continue(); + if (u.hostname !== 'names.coinos.io') return r.abort(); + if (r.method() === 'OPTIONS') return r.respond({ status: 200, headers: cors, body: '{}' }); + const reply = (j, status = 200) => r.respond({ status, headers: { ...cors, 'content-type': 'application/json' }, body: JSON.stringify(j) }); + if (u.pathname === '/register' && r.method() === 'POST') { const j = JSON.parse(r.postData() || '{}'); registered.push(j.name); return reply({ ok: true, name: j.name }); } + if (u.pathname.startsWith('/name/')) return reply({ taken: false }); + if (u.pathname.startsWith('/pubkey/')) return reply({}); + return reply({}); +}); +page.on('pageerror', (e) => errs.push(String(e))); +const click = (x) => page.evaluate((s) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(s)); if (e) e.click(); return !!e; }, x); +const text = () => page.evaluate(() => document.body.innerText); +const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await text()).toLowerCase().includes(x)) return true; await sleep(250); } return false; }; +const openWalletPage = async () => { + await page.evaluate(() => [...document.querySelectorAll('button')].find((e) => e.getAttribute('aria-label') === 'Settings').click()); await sleep(500); + await page.evaluate(() => [...document.querySelectorAll('.settings-tile')].find((e) => /^Wallet/.test(e.textContent.trim())).click()); await sleep(600); +}; +try { + await page.setViewport({ width: 390, height: 844 }); + const cdp = await page.createCDPSession(); + await cdp.send('WebAuthn.enable'); + await cdp.send('WebAuthn.addVirtualAuthenticator', { options: { + protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, hasPrf: true, automaticPresenceSimulation: true } }); + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'mainnet')); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#onb-create', { timeout: 15000 }); + check('the front door says Create an account', (await page.$eval('#onb-create', (e) => e.textContent.trim())) === 'Create an account'); + await page.click('#onb-create'); + check('no seed screens: straight to Claim your name', await waitText('claim your name', 20000)); + const t1 = await text(); + check('no twelve words or check on the way', !(await page.$('.words')) && !/which word/i.test(t1)); + check('no "Not now" on a sign-up\'s name step', !/not now/i.test(t1)); + await page.waitForSelector('.onb input[type=text]', { timeout: 20000 }); + await page.type('.onb input[type=text]', 'quicktester'); + await click('claim'); + check('the name goes to the registrar', await (async () => { for (let i = 0; i < 80 && !registered.length; i++) await sleep(250); return registered.includes('quicktester'); })(), registered.join()); + check('then the look: avatar and cover', await (async () => { for (let i = 0; i < 80; i++) { if (await page.$('.onb-cover')) return true; await sleep(250); } return false; })()); + await click('continue'); await sleep(800); + if (await waitText("you're all set", 8000)) await click('enter'); + await page.waitForSelector('.balance-face', { timeout: 20000 }); + await sleep(1500); + const shape = async () => page.evaluate(() => ({ + faces: document.querySelectorAll('.balance-face').length, + carousel: !!document.querySelector('.bal-carousel'), + label: document.querySelector('.balance-face .small.faint')?.textContent.trim(), + body: document.body.innerText, + })); + let s = await shape(); + check('in, on Spending alone (no Savings face)', s.faces === 1 && !s.carousel && /spending/i.test(s.label || ''), JSON.stringify({ faces: s.faces, carousel: s.carousel, label: s.label })); + check('a quiet line asks for a way back in', /add a way back into your account/i.test(s.body)); + const acct = await page.evaluate(() => JSON.parse(sessionStorage.getItem('btc-wallet-accounts') || '{}')); + const a0 = (acct.accounts || [])[0] || {}; + check('the account: spending-only, phrase unseen, saved to the device', a0.kind === 'spending' && a0.savingsLater && a0.seedSeen === false && a0.persisted, JSON.stringify({ kind: a0.kind, later: a0.savingsLater, seen: a0.seedSeen, persisted: a0.persisted })); + + await openWalletPage(); + check('Settings → Wallet opens with Ways back in', !!(await page.$('.ways-back')) && /ways back in/i.test(await text())); + const views = await page.$$eval('.balance-settings-row[data-view]', (l) => l.map((e) => e.dataset.view)); + check('only Spending is listed, with Add a savings account', views.join() === 'spending' && !!(await page.$('#add-savings')), views.join()); + if (process.env.SHOT) { await page.evaluate(() => document.querySelector('.ways-back').scrollIntoView()); await page.screenshot({ path: process.env.SHOT }); } + // a passkey: the relays are stubbed, so the publish fails offline — the + // error says so and nothing is listed + await page.evaluate(() => document.querySelector('.ways-back-add[data-way="passkey"]')?.click()); + await sleep(4000); + const pk = await page.evaluate(() => ({ rows: [...document.querySelectorAll('.ways-back-row')].map((e) => e.dataset.way), err: document.querySelector('.ways-back .notice.err')?.textContent || '' })); + check('adding a passkey lists it', pk.rows.includes('passkey') && !pk.err, JSON.stringify(pk)); + const backups = await page.evaluate(() => (window.__relayGuard.events || []).filter((e) => e.kind === 30078 && e.tags.some((t) => t[0] === 'd' && t[1] === 'coinos:wallet:v1'))); + check('the reminder line is gone once there is a way back in', await (async () => { await page.goBack(); await page.goBack(); await sleep(600); const gone = !/add a way back into your account/i.test(await text()); await openWalletPage(); return gone; })()); + // the list and the reminder, as the app sees them (record a method directly) + await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === 'Show')?.click()); + await sleep(500); + check('Show opens the recovery phrase card', /recovery phrase/i.test(await text())); + + await page.evaluate(() => document.getElementById('add-savings')?.click()); await sleep(800); + const views2 = await page.$$eval('.balance-settings-row[data-view]', (l) => l.map((e) => e.dataset.view)); + check('Add a savings account adds Savings beside Spending', views2.join() === 'spending,savings' && !(await page.$('#add-savings')), views2.join()); + await page.goBack(); await page.goBack(); await sleep(800); + s = await shape(); + check('the wallet now has both faces', s.carousel && s.faces === 2, JSON.stringify({ faces: s.faces, carousel: s.carousel })); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForSelector('.balance-face', { timeout: 20000 }); await sleep(1500); + s = await shape(); + check('a reload keeps Savings', s.carousel && s.faces === 2); + + // A new device: everything here wiped, the relays holding only what the + // passkey published. Sign in → Passkey opens THIS account, as itself. + const before = await page.evaluate(() => { + const st = JSON.parse(sessionStorage.getItem('btc-wallet-accounts') || '{}'); + const a = (st.accounts || [])[0] || {}; + return { mnemonic: a.mnemonic, nostrPk: a.nostrPk }; + }); + before.backups = backups; + check('the passkey published one encrypted seed copy', before.backups.length >= 1 && !before.backups[0].content.includes(before.mnemonic.split(' ')[0] + ' '), String(before.backups.length)); + const backup = before.backups[before.backups.length - 1]; + await page.evaluateOnNewDocument((ev) => { + const send = WebSocket.prototype.send; + WebSocket.prototype.send = function (d) { + try { + const m = JSON.parse(d); + if (m[0] === 'REQ' && m.slice(2).some((f) => (f.kinds || []).includes(30078) && (f.authors || []).includes(ev.pubkey))) { + const ws = this; + setTimeout(() => ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', m[1], ev]) })), 1); + } + } catch {} + return send.call(this, d); + }; + }, backup); + await page.evaluate(async () => { + localStorage.clear(); sessionStorage.clear(); + for (const d of (await indexedDB.databases?.() || [])) if (d.name) indexedDB.deleteDatabase(d.name); + localStorage.setItem('btc-wallet-network', 'mainnet'); + }); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#onb-signin', { timeout: 15000 }); + await page.click('#onb-signin'); await sleep(500); + await click('passkey'); + for (let i = 0; i < 120 && !(await page.$('.balance-face')); i++) await sleep(250); + if (!(await page.$('.balance-face'))) console.log((await text()).slice(0, 800), errs); + const after = await page.evaluate(() => { + const st = JSON.parse(sessionStorage.getItem('btc-wallet-accounts') || '{}'); + const a = (st.accounts || [])[0] || {}; + const login = Object.keys(localStorage).concat([]).filter((k) => /:nostrlogin$/.test(k)).map((k) => localStorage.getItem(k)); + return { mnemonic: a.mnemonic, nostrPk: a.nostrPk, kind: a.kind, login, onb: localStorage.getItem('btc-wallet-onb-step') }; + }); + check('the passkey opened the same account', after.mnemonic === before.mnemonic); + check('as itself: same identity, the passkey key not adopted', after.nostrPk === before.nostrPk && !after.login.some((v) => v && v.includes(backup.pubkey)), JSON.stringify({ was: before.nostrPk?.slice(0, 8), now: after.nostrPk?.slice(0, 8) })); + check('no wizard on the way back in', !after.onb && !(await page.$('.onb'))); + check('no page errors', errs.length === 0, errs.join(' | ')); +} finally { + await browser.close(); server.stop(true); +} +console.log(ok ? 'PASS' : 'FAIL'); +process.exit(ok ? 0 : 1); |
