diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/balance-settings-row-test.js | 65 | ||||
| -rw-r--r-- | tools/nip29-group-test.js | 102 |
2 files changed, 167 insertions, 0 deletions
diff --git a/tools/balance-settings-row-test.js b/tools/balance-settings-row-test.js new file mode 100644 index 0000000..89e68d9 --- /dev/null +++ b/tools/balance-settings-row-test.js @@ -0,0 +1,65 @@ +// Settings → Wallet lists Spending and Savings; each row opens the page the +// 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'; +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 = []; +// mainnet so Spending turns on (the Ark server's info is read); every other +// outside request is refused, so nothing is claimed or published +await page.setRequestInterception(true); +page.on('request', (r) => { const u = new URL(r.url()); (u.origin === server.url.origin || u.hostname === 'ark.coinos.io') ? r.continue() : r.abort(); }); +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 waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).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(500); +}; +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + 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); + 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 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); + if (process.env.DEBUG) console.log((await page.evaluate(() => document.body.innerText)).slice(0, 600)); + const gears = await page.$$eval('.balance-gear', (l) => l.length); + await openWalletPage(); + if (process.env.SHOT) { await page.evaluate(() => document.querySelector('.balance-settings-row').closest('.card').scrollIntoView()); await page.screenshot({ path: process.env.SHOT }); } + const views = await page.$$eval('.balance-settings-row', (l) => l.map((e) => e.dataset.view)); + check('Settings → Wallet lists the balances', views.length >= 1, views.join(',')); + check('Spending is listed when the card shows it', views.includes('spending'), `gears on the wallet: ${gears}`); + await page.evaluate(() => document.querySelector('.balance-settings-row[data-view="spending"]')?.click()); await sleep(600); + const st = await page.evaluate(() => ({ screen: history.state.nav.screen, view: history.state.nav.settingsView, h3: [...document.querySelectorAll('h3')].map((e) => e.textContent) })); + check('the Spending row opens the Spending settings', st.screen === 'accountSettings' && st.view === 'spending' && st.h3.includes('Spending'), JSON.stringify(st.h3.slice(0, 4))); + await page.goBack(); await sleep(600); + check('Back returns to Settings → Wallet', !!(await page.$('.balance-settings-row'))); + await page.evaluate(() => document.querySelector('.balance-settings-row[data-view="savings"]')?.click()); await sleep(600); + const sv = await page.evaluate(() => ({ screen: history.state.nav.screen, view: history.state.nav.settingsView })); + check('the Savings row opens the Savings settings', sv.screen === 'accountSettings' && sv.view === 'savings'); + 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); diff --git a/tools/nip29-group-test.js b/tools/nip29-group-test.js new file mode 100644 index 0000000..2ff1192 --- /dev/null +++ b/tools/nip29-group-test.js @@ -0,0 +1,102 @@ +// NIP-29 relay groups in Messages: New group → Relay group → paste an +// address → Join. Reads the real "pyramid runners" group off +// pyramid.fiatjaf.com (relays: 'read' — nothing published leaves the page: +// the join request, the kind 10009 list and the test message are answered +// OK in place and checked here). +// Run: bun tools/nip29-group-test.js +import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js'; +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 ? ' — ' + d : ''}`); if (!c) ok = false; }; +const ADDR = process.env.GROUP || "https://chachi.chat/pyramid.fiatjaf.com/Tnq7x2ZTgrPZWFrC"; +const GID = 'Tnq7x2ZTgrPZWFrC'; + +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +const errs = []; +page.on('console', (m) => { if (/nip29/.test(m.text())) console.log(' [page]', m.text()); }); +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() === x); if (e) { e.click(); return true; } return false; }, t); +const waitText = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; }; +const published = () => page.evaluate(() => window.__relayGuard.events); + +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await page.reload({ waitUntil: 'domcontentloaded' }); + await sleep(400); + await page.evaluate(() => [...document.querySelectorAll('button')].find((n) => /create a new wallet/i.test(n.textContent))?.click()); await sleep(600); + await seedWords(page); + await skipSeedCheck(page); + await waitText('receive', 20000); + await page.click('[aria-label="Messages"]'); + await waitText('new group', 10000); + await click('new group'); await sleep(300); + check('New group offers a relay group', await click('relay group')); + await sleep(200); + await page.type('.nip29-address', 'not an address'); + await click('join'); await sleep(500); + check('a bad address is refused', await waitText("isn't a group address", 3000)); + await page.evaluate(() => { const i = document.querySelector('.nip29-address'); i.value = ''; i.dispatchEvent(new Event('input', { bubbles: true })); }); + await page.type('.nip29-address', ADDR); + await click('join'); + check('the group opens, named by its relay', await waitText('pyramid runners', 20000)); + check('its messages arrive', await waitText('paff.club', 20000)); + const evs = await published(); + const join = evs.find((e) => e.kind === 9021); + check('a join request went to the group', !!join && join.tags.some((x) => x[0] === 'h' && x[1] === GID)); + // the 10009 write reads the newest list first, everywhere (a first write) + let list = null; + for (let i = 0; i < 80 && !list; i++) { list = (await published()).filter((e) => e.kind === 10009).pop(); if (!list) await sleep(250); } + check('the group is added to the kind 10009 list', !!list && list.tags.some((x) => x[0] === 'group' && x[1] === GID && /pyramid\.fiatjaf\.com/.test(x[2])), JSON.stringify(list?.tags)); + + // say something (answered OK in place, never sent) + await page.click('#msg-draft'); + await page.type('#msg-draft', 'hello from the nip29 test'); + await page.keyboard.press('Enter'); + await sleep(1500); + const msg = (await published()).find((e) => e.kind === 9 && e.content === 'hello from the nip29 test'); + check('a message is a kind 9 with the group h tag', !!msg && msg.tags.some((x) => x[0] === 'h' && x[1] === GID), JSON.stringify(msg?.tags)); + check('...and previous tags from the timeline', !!msg && msg.tags.some((x) => x[0] === 'previous' && /^[0-9a-f]{8}$/.test(x[1]))); + check('it shows in the group', await waitText('hello from the nip29 test', 3000)); + + // members: the relay's list + await page.evaluate(() => document.querySelector('.chat-head .clickable')?.click()); await sleep(800); + check('the member panel says it is a relay group', await waitText('not encrypted', 3000)); + const n = await page.evaluate(() => document.querySelectorAll('.chat-members .chat-thread-row').length); + check('members come from the relay', n > 10, n + ' members'); + + // back home: a row of its own; reload keeps it + await page.evaluate(() => document.querySelector('.chat-head button')?.click()); await sleep(600); + check('the group is listed in Messages', await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => r.textContent.includes('pyramid runners')))); + await page.reload({ waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.click('[aria-label="Messages"]'); + let kept = false; + for (let i = 0; i < 40 && !kept; i++) { kept = await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => r.textContent.includes('pyramid runners'))); if (!kept) await sleep(250); } + check('...and still is after a reload', kept); + + // leave: two taps, a 9022 to the relay, and off the list + await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].find((r) => r.textContent.includes('pyramid runners'))?.click()); await sleep(800); + await page.click('[aria-label="Leave group"]'); await sleep(200); + await page.click('[aria-label="Leave group"]'); + let left = null, relist = null; + for (let i = 0; i < 80 && !(left && relist); i++) { + const evs = await published(); + left = evs.find((e) => e.kind === 9022); + relist = evs.filter((e) => e.kind === 10009).find((e) => !e.tags.some((x) => x[0] === 'group')); + if (!(left && relist)) await sleep(250); + } + check('leaving tells the relay (9022)', !!left && left.tags.some((x) => x[0] === 'h' && x[1] === GID)); + check('...and takes it off the 10009 list', !!relist); + check('...and out of Messages', !(await page.evaluate(() => [...document.querySelectorAll('.chat-thread-row')].some((r) => r.textContent.includes('pyramid runners'))))); + check('no page errors', !errs.length, errs.join(' | ')); +} catch (e) { console.error(e); ok = false; } finally { + await browser.close(); server.stop(); +} +console.log(ok ? '\nPASS' : '\nFAIL'); +process.exit(ok ? 0 : 1); |
