aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/balance-settings-row-test.js13
-rw-r--r--tools/onboard-backup-test.js4
-rw-r--r--tools/send-lnaddr-advance-test.js3
-rw-r--r--tools/signup-quick-test.js167
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);