diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/balance-settings-row-test.js | 13 | ||||
| -rw-r--r-- | tools/chat-layout-test.js | 2 | ||||
| -rw-r--r-- | tools/footer-support-test.js | 41 | ||||
| -rw-r--r-- | tools/onboard-backup-test.js | 4 | ||||
| -rw-r--r-- | tools/pos-test.js | 20 | ||||
| -rw-r--r-- | tools/send-lnaddr-advance-test.js | 3 | ||||
| -rw-r--r-- | tools/signup-quick-test.js | 203 |
7 files changed, 265 insertions, 21 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/chat-layout-test.js b/tools/chat-layout-test.js index 7c79858..a0f00ad 100644 --- a/tools/chat-layout-test.js +++ b/tools/chat-layout-test.js @@ -164,7 +164,7 @@ try { console.log('\n[direct messages]'); await page.click('.chat-back'); await sleep(500); - check('footer returns when the conversation closes', await page.$eval('.footer', (el) => getComputedStyle(el).display !== 'none')); + // (no footer to bring back: with a wallet open the pickers live in Settings, 2026-11-15) await click('button', 'New message'); await setInput('input[placeholder*="npub" i]', '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'); await click('button', 'Open'); diff --git a/tools/footer-support-test.js b/tools/footer-support-test.js index a37c070..0de6c9a 100644 --- a/tools/footer-support-test.js +++ b/tools/footer-support-test.js @@ -1,5 +1,6 @@ -// The footer's "Show your support" link opens the hat shop, from any page, -// and only shows once a wallet is open (the shop pays from it). +// "Show your support" lives on Settings → About (2026-11-15: the footer is +// gone once a wallet is open; language and currency top the Settings hub) and opens the hat shop; it shows only +// once a wallet is open (the shop pays from it). // Run: bun tools/footer-support-test.js import puppeteer from './puppeteer.js'; import { generateMnemonic } from '@scure/bip39'; @@ -18,31 +19,43 @@ const browser = await puppeteer.launch({ executablePath: process.env.CHROME || ' const page = await browser.newPage(); const errs = []; page.on('pageerror', (e) => errs.push(String(e))); const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); -const link = () => page.evaluate(() => [...document.querySelectorAll('.footer button')].some((b) => b.textContent.trim() === 'Show your support')); +const link = () => page.evaluate(() => !!document.getElementById('about-support')); +const footerOnly = () => page.evaluate(() => { const f = document.querySelector('.footer'); return f ? { selects: f.querySelectorAll('select').length, other: f.querySelectorAll('button, a').length, text: f.innerText.replace(/\s+/g, ' ').trim() } : null; }); +const openAbout = 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) => /^About/.test(e.textContent.trim())).click()); await sleep(500); +}; try { await page.setViewport({ width: 390, height: 844 }); await page.goto(server.url.origin, { waitUntil: 'domcontentloaded' }); await sleep(600); check('no link before there is a wallet', !(await link())); - 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'); for (let i = 0; i < 80 && !(await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.textContent.trim() === 'Send'))); i++) { await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /^(not now|skip|continue|later)$/i.test(b.textContent.trim()))?.click()); await sleep(400); } await sleep(1500); - check('the footer has "Show your support"', await link()); - // from another page: the feed - await page.evaluate(() => [...document.querySelectorAll('nav button, .bottom-nav button')].find((b) => /feed/i.test(b.textContent))?.click()); await sleep(800); - await page.evaluate(() => [...document.querySelectorAll('.footer button')].find((b) => b.textContent.trim() === 'Show your support').click()); + const f = await footerOnly(); + check('with a wallet open there is no footer', !f || (await page.$eval('.footer', (el) => getComputedStyle(el).display === 'none')), JSON.stringify(f)); + await page.evaluate(() => [...document.querySelectorAll('button')].find((e) => e.getAttribute('aria-label') === 'Settings').click()); await sleep(500); + check('Settings carries the language and currency', await page.evaluate(() => document.querySelectorAll('.settings-prefs select').length) === 2); + if (process.env.SHOT2) await page.screenshot({ path: process.env.SHOT2 }); + await page.evaluate(() => [...document.querySelectorAll('.settings-tile')].find((e) => /^About/.test(e.textContent.trim())).click()); await sleep(500); + const about = await page.evaluate(() => document.querySelector('.about-card')?.innerText || ''); + check('About has the credits, How it works, the theme and the commit', /Adam Soltys/.test(about) && /git\.coinos\.io/.test(about) && /How it works/.test(about) && /(Dark|Light) mode/.test(about) && /Running git commit [0-9a-f]{7}/.test(about), about.replace(/\n/g, ' | ')); + check('...and "Show your support"', await link()); + const th = await page.evaluate(() => document.documentElement.dataset.theme); + await page.click('#about-theme'); await sleep(300); + check('the theme switch works from there', await page.evaluate(() => document.documentElement.dataset.theme) !== th); + await page.click('#about-theme'); await sleep(300); + if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT }); + await page.click('#about-support'); await sleep(1200); const txt = await page.evaluate(() => document.body.innerText); - check('it opens the hat shop', /hat/i.test(txt) && !!(await page.evaluate(() => document.querySelector('.hat-btn') || /Crown|Top hat|hats?/i.test(document.querySelector('.card')?.innerText || ''))), txt.slice(0, 160).replace(/\n/g, ' | ')); - await page.evaluate(() => { document.querySelector('.bottom-nav, nav')?.remove(); window.scrollTo(0, 1e6); }); - await sleep(300); - if (process.env.SHOT) await (await page.$('.footer')).screenshot({ path: process.env.SHOT }); - else await page.screenshot({ path: '/tmp/footer-support.png', fullPage: true }); - // || '/tmp/footer-support.png', fullPage: true }); + check('it opens the hat shop', /hat/i.test(txt), txt.slice(0, 160).replace(/\n/g, ' | ')); check('no page errors', errs.length === 0, errs.join('; ').slice(0, 300)); } catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(); } console.log(ok ? 'PASS' : 'FAIL'); 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/pos-test.js b/tools/pos-test.js index 55349cb..2b6a0b7 100644 --- a/tools/pos-test.js +++ b/tools/pos-test.js @@ -30,7 +30,7 @@ const ctx = { }; const feature = posFeature(ctx); window.__feature = feature; function render() { document.querySelector('#app').replaceChildren(feature.screenView() || h('div', { id: 'closed' }, 'closed')); } -window.test = { open: () => { feature.openPos(); }, init: () => { feature.init(); render(); }, +window.test = { open: () => { feature.openPos(); }, request: () => { feature.openAmountRequest(); }, init: () => { feature.init(); render(); }, cards: () => { document.querySelector('#app').replaceChildren(...feature.posSettingsCards()); }, settle: (ok) => { const w = watch; watch = null; w.cb({ step: ok ? 'done' : 'failed' }); }, get watch() { return watch && watch.id; }, get cancelled() { return cancelled; }, get sales() { return (state.pos || {}).sales || []; }, ui }; render(); `; @@ -128,6 +128,24 @@ try { check("the old device-wide flag is dropped on start (it can't say whose it was)", await page.evaluate(() => (test.ui.pos = null, true)) && (await boot('owner')) && await page.evaluate(() => localStorage.getItem('btc-wallet-pos-mode') === null)); check('another wallet on the same device does not start in point of sale', !(await boot('someone-else'))); check('the owner still does, after anyone else has been in', await boot('owner')); + + console.log('\n[Receive → Amount]'); + await page.evaluate(() => { localStorage.removeItem('btc-wallet-pos-mode:owner'); test.ui.pos = null; test.request(); }); await pause(50); + let rq = await text(); + check('opens as Request an amount, without the till around it', /Request an amount/.test(rq) && !/ask for a tip|Tips today|Sales today/i.test(rq) && !(await page.$('.pos-sale')), rq.slice(0, 200)); + const before = await page.evaluate(() => test.sales.length); + await type('.pos-amount', '2100'); + await page.click('#pos-charge'); await pause(100); + rq = await text(); + check('straight to the invoice, no tip prompt', /Scan to pay/i.test(rq) && /2,100/.test(rq) && !!(await page.$('.pos-qr')), rq.slice(0, 200)); + const req = await page.evaluate(() => test.sales[test.sales.length - 1]); + check('the request is for the amount, no tip, and marked', req.billSat === 2100 && req.tipSat === 0 && req.request === true, JSON.stringify(req)); + await page.evaluate(() => test.settle(true)); await pause(100); + check('paid, with Done back to Receive', /Done/.test(await text()) && !/New sale/.test(await text())); + await click('Done'); await pause(50); + check('Done closes it', !!(await page.$('#closed'))); + await page.evaluate(() => { test.ui.pos = null; test.open(); }); await pause(50); + check("the till's sales list leaves requests out", !(await text()).includes('2,100'), String(before)); check('no browser errors', errors.length === 0); } finally { await browser.close(); server.stop(true); } console.log('\n✅ point of sale'); 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..a269faf --- /dev/null +++ b/tools/signup-quick-test.js @@ -0,0 +1,203 @@ +// 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, no name on it)', s.faces === 1 && !s.carousel && !s.label, JSON.stringify({ faces: s.faces, carousel: s.carousel, label: s.label })); + // asked once, and only with money in the account (2026-11-15): not on an empty one + check('no way-back-in prompt on an empty account', !/add a way back into your account/i.test(s.body) && !(await page.$('.ways-back-prompt'))); + // money arrives in Spending (the ark feature, found on the heap, made to + // report a balance; then a repaint) + const giveBalance = async () => { + const protoH = await page.evaluateHandle(() => Object.prototype); + const all = await page.queryObjects(protoH); + const found = await page.evaluate((objs) => { + const f = objs.find((o) => { try { return o && o.id === 'ark' && typeof o.spendingSat === 'function'; } catch { return false; } }); + if (!f) return false; + f.spendingSat = () => 5000; + window.dispatchEvent(new Event('resize')); + document.querySelector('.tabs button')?.click(); document.querySelector('.tabs button')?.click(); + return true; + }, all); + await all.dispose(); await protoH.dispose(); + await sleep(800); + return found; + }; + check('(test: found the ark feature)', await giveBalance()); + check('with a balance, it asks once for a way back in', !!(await page.$('.ways-back-prompt')), (await text()).slice(0, 300).replace(/\n/g, ' | ')); + await page.evaluate(() => [...document.querySelectorAll('.ways-back-prompt button')].find((b) => /not now/i.test(b.textContent)).click()); await sleep(400); + check('Not now puts it away', !(await page.$('.ways-back-prompt'))); + await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.balance-face', { timeout: 20000 }); await sleep(1500); + await giveBalance(); + check('...and never again, not after a reload', !(await page.$('.ways-back-prompt'))); + for (let i = 0; i < 60 && !(await page.$('#recv-amount')); i++) await sleep(250); + check('Receive has an Amount button', !!(await page.$('#recv-amount'))); + check('no History heading on an empty wallet', !/\bHISTORY\b/.test(await page.evaluate(() => document.body.innerText))); + if (process.env.SHOT3) await page.screenshot({ path: process.env.SHOT3 }); + await page.click('#recv-amount'); await sleep(500); + check('Amount opens the amount pad', !!(await page.$('.pos-amount')) && /request an amount/i.test(await text())); + if (process.env.SHOT4) await page.screenshot({ path: process.env.SHOT4 }); + await click('back'); await sleep(500); + check('Back returns to Receive', !(await page.$('.pos-amount')) && !!(await page.$('#recv-amount'))); + 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('the ways back in read as auth buttons', (await page.$$eval('.ways-back-add', (l) => l.map((e) => e.textContent.trim()))).join('|') === 'Passkey auth|Google auth|Nostr auth|Pen and paper', (await page.$$eval('.ways-back-add', (l) => l.map((e) => e.textContent.trim()))).join('|')); + check('only Spending is listed, with Add a savings account', views.join() === 'spending' && !!(await page.$('#add-savings')), views.join()); + if (process.env.SHOT2) { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); await page.evaluate(() => document.querySelector('.balance-settings-row').closest('.card').scrollIntoView()); await sleep(300); await page.screenshot({ path: process.env.SHOT2 }); await page.evaluate(() => document.documentElement.removeAttribute('data-theme')); } + 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'))); + // the list and the reminder, as the app sees them (record a method directly) + check('the phrase card waits behind Pen and paper', !(await page.$('.recovery-card'))); + await page.evaluate(() => document.querySelector('.ways-back-add[data-way="phrase"]')?.click()); + await sleep(500); + check('Pen and paper opens the recovery phrase card', !!(await page.$('.recovery-card .words'))); + + 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); |
