From 9a49107afe8cf7104ea7da1638e680518fa55bb8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:10:17 -0700 Subject: Profile tests: reach the feed and chats through the bottom nav, start on Following, follow today's onboarding - avatar-profile, profile-open: a new wallet opens the feed on Popular, so seed this wallet's last feed as Following; profile-open opens Feed from the nav - profile-feed: a mainnet import offers a username first (Not now), Messages is a bottom-nav button - profile-draft, profile-recovery: the import button reads 'Have an existing seed or entropy to import?'; profile batches stream through queryStreamOn, so the recovery test answers that too Co-Authored-By: Claude Opus 5.5 --- tools/avatar-profile-test.js | 1 + tools/profile-draft-test.js | 2 +- tools/profile-feed-test.js | 8 ++++---- tools/profile-open-test.js | 6 +++--- tools/profile-recovery-test.js | 4 +++- 5 files changed, 12 insertions(+), 9 deletions(-) (limited to 'tools') diff --git a/tools/avatar-profile-test.js b/tools/avatar-profile-test.js index 293d7c1..4ed1b25 100644 --- a/tools/avatar-profile-test.js +++ b/tools/avatar-profile-test.js @@ -39,6 +39,7 @@ try { })); localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', a], ['p', b]], c: '', at: now })); localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)); + localStorage.setItem('btc-wallet-feed:' + k, 'following'); // this wallet's last feed (new ones open on Popular) localStorage.setItem(k + ':profiles', JSON.stringify({ [a]: { name: 'Alice Poster', t: Date.now() }, [b]: { name: 'Bob Poster', about: 'bob bio here', t: Date.now() } })); }, [base, A, B]); await page.reload({ waitUntil: 'domcontentloaded' }); diff --git a/tools/profile-draft-test.js b/tools/profile-draft-test.js index 9062726..437dd61 100644 --- a/tools/profile-draft-test.js +++ b/tools/profile-draft-test.js @@ -42,7 +42,7 @@ try { await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); await page.reload({ waitUntil: 'domcontentloaded' }); await click('Create a new wallet'); - await click('Have an existing seed'); + await click('Have an existing seed or entropy to import?'); await page.waitForSelector('textarea'); const mnemonic = generateMnemonic(wordlist); await page.type('textarea', mnemonic); diff --git a/tools/profile-feed-test.js b/tools/profile-feed-test.js index 9c743ae..266e3f8 100644 --- a/tools/profile-feed-test.js +++ b/tools/profile-feed-test.js @@ -32,12 +32,12 @@ try { await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('button', 'Open wallet'); + // a mainnet wallet offers a username first (the spending-account step) + if (await waitText('claim a username', 8000)) await click('button', 'Not now'); check('wallet opens', await waitText('receive', 20000)); - // .header-msgs is the shared header-button class — settings, lock and - // search wear it too, and the first one is no longer this. Ask for the - // messages button by name. - await page.evaluate(() => [...document.querySelectorAll('button')] + // Messages is a button in the bottom nav; ask for it by name. + await page.evaluate(() => [...document.querySelectorAll('.app-nav-button')] .find((b) => /message/i.test(b.getAttribute('aria-label') || ''))?.click()); await sleep(800); check('chat home opens', await waitText('coinos', 10000)); diff --git a/tools/profile-open-test.js b/tools/profile-open-test.js index e2fcb3a..aa0c943 100644 --- a/tools/profile-open-test.js +++ b/tools/profile-open-test.js @@ -73,12 +73,12 @@ try { await page.evaluate(([k, pks, ns]) => { localStorage.setItem(k + ':follows', JSON.stringify({ tags: pks.map((p) => ['p', p]), c: '', at: Math.floor(Date.now() / 1000) })); localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)); + localStorage.setItem('btc-wallet-feed:' + k, 'following'); // this wallet's last feed (new ones open on Popular) }, [base, authors, notes.map((e) => ({ id: e.id, pubkey: e.pubkey, kind: 1, created_at: e.created_at, content: e.content.slice(0, 200), tags: [] }))]); await page.reload({ waitUntil: 'domcontentloaded' }); await waitText('receive', 20000); - await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((e) => /message/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); - await sleep(1000); - await page.evaluate(() => { const e = [...document.querySelectorAll('.item')].find((n) => /feed/i.test(n.textContent)); if (e) e.click(); }); + // the Feed is a button in the bottom nav + await page.evaluate(() => { const b = [...document.querySelectorAll('.app-nav-button')].find((e) => /^feed$/i.test(e.getAttribute('aria-label') || '')); if (b) b.click(); }); // let the feed settle completely — this test is about what happens AFTER await sleep(12000); diff --git a/tools/profile-recovery-test.js b/tools/profile-recovery-test.js index b2c871a..2f6ae90 100644 --- a/tools/profile-recovery-test.js +++ b/tools/profile-recovery-test.js @@ -46,7 +46,9 @@ const bundle = await Bun.build({ entrypoints: ['profile-test-entry'], target: 'b build.onLoad({ filter: /src\/features\/messages\.js$/ }, async ({ path }) => ({ contents: (await Bun.file(path).text()) .replace("id: 'messages',", "id: 'messages', testFetch: fetchFullProfile, testView: profileScreen, testApply: applyProfile, testLight: (pk) => profiles.get(pk), testFull: (pk) => fullProfiles.get(pk),"), loader: 'js' })); build.onLoad({ filter: /src\/nostr\.js$/ }, async ({ path }) => ({ contents: (await Bun.file(path).text()) - .replace('export async function queryOn(relays, filter, maxWait = 1500) {', 'export async function queryOn(relays, filter, maxWait = 1500) { if (globalThis.__profileQuery) return globalThis.__profileQuery(relays, filter);'), loader: 'js' })); + .replace('export async function queryOn(relays, filter, maxWait = 1500) {', 'export async function queryOn(relays, filter, maxWait = 1500) { if (globalThis.__profileQuery) return globalThis.__profileQuery(relays, filter);') + // profile batches stream each answer as it lands (our relays and the indexes at once) + .replace('export async function queryStreamOn(relays, filter, onEvent, maxWait = 1500) {', 'export async function queryStreamOn(relays, filter, onEvent, maxWait = 1500) { if (globalThis.__profileQuery) { const evs = await globalThis.__profileQuery(relays, filter); for (const ev of evs || []) onEvent(ev); return; }'), loader: 'js' })); } }] }); assert(bundle.success, bundle.logs.join('\n')); const css = await Bun.file('src/style.css').text(); -- cgit v1.2.3 From fdb044b44019d45bf8f849a4cf64a56efbc54ada Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:22:58 -0700 Subject: padlock-flow, onboard-backup, amber-login tests follow today's UI - padlock-flow: the padlock lives behind the balance card's gear (wallet settings), declining lands back there; another wallet is added from the profile's 'Sign into another account' (import tab) and counted in the identity switcher, since the Accounts list is no longer reached from the wallet - onboard-backup: the welcome card lost its blurb (42c8d4e); the way in now says what the wallet is by putting the twelve words on screen at once - amber-login: the starter follows wait on the deep contact-list lookup (follows-overwrite fix), so their signature can come after the wallet opens; keep answering sign requests until things go quiet Co-Authored-By: Claude Opus 5.5 --- tools/amber-login-test.js | 26 +++++++++++++++------- tools/onboard-backup-test.js | 14 +++++++++--- tools/padlock-flow-test.js | 51 +++++++++++++++++++++++++------------------- 3 files changed, 58 insertions(+), 33 deletions(-) (limited to 'tools') diff --git a/tools/amber-login-test.js b/tools/amber-login-test.js index 7439f0d..0b4c3c9 100644 --- a/tools/amber-login-test.js +++ b/tools/amber-login-test.js @@ -73,15 +73,25 @@ try { // walked through const signedKinds = []; let signedUrls = 1; + // answer the next sign_event intent, if one is waiting + const signNext = async () => { + const pend = (await urls()).filter((u) => /type=sign_event/.test(u)); + if (pend.length <= signedUrls) return false; + const u = pend[signedUrls]; signedUrls++; + const ev = JSON.parse(decodeURIComponent(u.slice('nostrsigner:'.length).split('?')[0])); + signedKinds.push(ev.kind); + await deliver(JSON.stringify(finalizeEvent({ kind: ev.kind, created_at: ev.created_at, tags: ev.tags, content: ev.content }, SK))); + return true; + }; for (let i = 0; i < 80 && !(await page.evaluate(() => document.body.innerText.toLowerCase().includes('receive'))); i++) { - const us = await urls(); - const pend = us.filter((u) => /type=sign_event/.test(u)); - if (pend.length > signedUrls) { - const u = pend[signedUrls]; signedUrls++; - const ev = JSON.parse(decodeURIComponent(u.slice('nostrsigner:'.length).split('?')[0])); - signedKinds.push(ev.kind); - await deliver(JSON.stringify(finalizeEvent({ kind: ev.kind, created_at: ev.created_at, tags: ev.tags, content: ev.content }, SK))); - } else { await click('later') || await click('skip verification') || await click("i've written it down") || await click('continue'); } + if (!(await signNext())) { await click('later') || await click('skip verification') || await click("i've written it down") || await click('continue'); } + await sleep(500); + } + // The starter follows wait on a deep look for a list this key may already + // have (2026-10-02 follows-overwrite fix), so their signature can arrive + // after the wallet is open: keep answering until things go quiet. + for (let i = 0, idle = 0; i < 60 && idle < 8; i++) { + if (await signNext()) idle = 0; else if (signedKinds.length) idle++; await sleep(500); } if (!(await page.evaluate(() => document.body.innerText.toLowerCase().includes('receive')))) console.log(' DEBUG loop end:', JSON.stringify(await page.evaluate(() => ({ btns: [...document.querySelectorAll('button')].map((b) => b.textContent.trim()).filter(Boolean).slice(0, 12), text: document.body.innerText.slice(0, 160).replace(/\n+/g, ' | ') })))); diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js index 3b242dd..9fe5a3e 100644 --- a/tools/onboard-backup-test.js +++ b/tools/onboard-backup-test.js @@ -73,15 +73,23 @@ try { primaryTop: primary ? primary.getBoundingClientRect().top : -1, signinTop: signins.length ? signins[0].getBoundingClientRect().top : -1, divider: !!document.querySelector('.onb-or'), - body: (document.querySelector('.onb p') || {}).textContent || '', }; }); check('the way in says what it makes', /wallet/i.test(front.label || ''), front.label || 'missing'); check('...and comes before the sign-in buttons', front.first && front.primaryTop < front.signinTop, `create at ${Math.round(front.primaryTop)}, first sign-in at ${Math.round(front.signinTop)}`); check('...with a line between them, so they read as the alternative', front.divider); - check('the page says what the wallet actually is', /twelve words|seed|phrase/i.test(front.body), - front.body.slice(0, 60) + '…'); + // The welcome card is just the title and the buttons now (42c8d4e): the + // way in itself says what the wallet is — tapping it puts the twelve + // words on screen at once, with what they are. + await page.evaluate(() => document.getElementById('onb-create')?.click()); + await sleep(600); + const made = await page.evaluate(() => ({ + words: document.querySelectorAll('.words .w .t').length, + says: /12 words|twelve words/i.test(document.body.innerText), + })); + check('the way in shows what the wallet actually is: its twelve words', made.words === 12 && made.says, + made.words + ' words' + (made.says ? ', explained' : ', unexplained')); await page.close(); } diff --git a/tools/padlock-flow-test.js b/tools/padlock-flow-test.js index e67711f..59aee32 100644 --- a/tools/padlock-flow-test.js +++ b/tools/padlock-flow-test.js @@ -1,7 +1,7 @@ // The padlock on a fresh (passwordless) wallet must never cost you your // session: the set-a-password ask renders over the open wallet, declining it // lands you exactly where you were, the logo stays a way home, and the -// Wallets list shows only the on-chain wallet until Spending is set up. +// wallet settings speak only of the on-chain wallet until Spending is set up. // Setting a password locks in place: watch-only home, closed padlock. // Run: bun tools/padlock-flow-test.js import puppeteer from 'puppeteer-core'; @@ -20,9 +20,8 @@ const page = await browser.newPage(); await page.setViewport({ width: 420, height: 880 }); 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 clickTitle = (t) => page.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.title === x); if (b) { b.click(); return true; } return false; }, t); -// the padlock lives on the Accounts page: open it from the balance card first -const padlock = async (pg, title) => { await pg.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(500); return pg.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.title === x); if (b) { b.click(); return true; } return false; }, title); }; +// the padlock lives on the wallet settings: open them from the balance card's gear first +const padlock = async (pg, title) => { await pg.evaluate(() => document.querySelector('.balance-gear')?.click()); await sleep(500); return pg.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.title === x); if (b) { b.click(); return true; } return false; }, title); }; const body = () => page.evaluate(() => document.body.innerText); 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; }; @@ -41,8 +40,8 @@ try { check('wallet open', await waitText('receive')); await sleep(2000); - console.log('\n[the Wallets list before opting into Spending]'); - await page.evaluate(() => document.querySelector('.balance-switch')?.click()); + console.log('\n[the wallet settings before opting into Spending]'); + await page.evaluate(() => document.querySelector('.balance-gear')?.click()); await sleep(600); let txt = await body(); check('shows the on-chain wallet', /savings/i.test(txt), txt.slice(0, 140).replace(/\n+/g, ' | ')); @@ -60,7 +59,7 @@ try { await click('button', 'Not now'); await sleep(600); txt = await body(); - check('declining lands back where the padlock was tapped (the Accounts page)', /add account/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); + check('declining lands back where the padlock was tapped (the wallet settings)', /security/i.test(txt) && !(await page.$('input[type=password]')), txt.slice(0, 120).replace(/\n+/g, ' | ')); console.log('\n[the logo still means home]'); await page.evaluate(() => document.querySelector('.brand')?.click()); @@ -79,7 +78,7 @@ try { await sleep(1200); txt = await body(); check('still home (watch-only), not signed out', /receive/i.test(txt) && /balance/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | ')); - await page.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(500); + await page.evaluate(() => document.querySelector('.balance-gear')?.click()); await sleep(500); const closed = await page.evaluate(() => [...document.querySelectorAll('button')].some((b) => b.title && /unlock/i.test(b.title))); check('padlock is closed', closed); await page.evaluate(() => document.querySelector('.brand')?.click()); await sleep(400); @@ -95,7 +94,6 @@ try { const p2 = await ctx2.newPage(); await p2.setViewport({ width: 420, height: 880 }); const click2 = (t) => p2.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x.toLowerCase())); if (e) { e.click(); return true; } return false; }, t); - const clickTitle2 = (t) => p2.evaluate((x) => { const b = [...document.querySelectorAll('button')].find((n) => n.title === x); if (b) { b.click(); return true; } return false; }, t); const body2 = () => p2.evaluate(() => document.body.innerText); const waitText2 = async (x, ms = 20000) => { for (let i = 0; i < ms / 250; i++) { if ((await body2()).toLowerCase().includes(x.toLowerCase())) return true; await sleep(250); } return false; }; const importWallet = async () => { @@ -106,15 +104,25 @@ try { await click2('Open wallet'); return waitText2('receive'); }; + // another wallet comes in the way the app offers it now: your profile → + // Sign into another account (→ a new one, once others exist) → import const addWallet = async () => { - await p2.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); - await click2('Add wallet'); await sleep(400); - await click2('Savings'); await sleep(200); - await click2('Continue'); await sleep(600); + await p2.evaluate(() => document.querySelector('.header-avatar')?.click()); await sleep(800); + await click2('Sign into another account'); await sleep(500); + await click2('Sign into a new account'); await sleep(500); + await click2('Import existing'); await sleep(300); // the sign-in screen's import tab return importWallet(); }; - const savingsRows = () => p2.evaluate(() => [...document.querySelectorAll('button')].map((e) => e.textContent.trim()).filter((x) => /^[●○] Savings/.test(x)).length); - + // the wallets on this device, as the identity switcher lists them (the + // one open plus the others) + const walletCount = async () => { + await p2.evaluate(() => document.querySelector('.header-avatar')?.click()); await sleep(800); + await click2('Sign into another account'); await sleep(500); + const n = await p2.evaluate(() => document.querySelectorAll('.confirm-pop .clickable').length + 1); + await p2.evaluate(() => document.querySelector('.confirm-pop-backdrop')?.click()); await sleep(300); + await p2.evaluate(() => document.querySelector('.brand')?.click()); await sleep(500); + return n; + }; console.log('\n[a wallet created after a reload still auto-saves]'); await p2.goto('http://localhost:5236/', { waitUntil: 'domcontentloaded' }); await p2.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); @@ -144,16 +152,15 @@ try { console.log('\n[setting the password kept every saved wallet]'); await p2.reload({ waitUntil: 'domcontentloaded' }); check('reload keeps the locked view on screen', await waitText2('receive')); - await clickTitle2('Unlock'); + await padlock(p2, 'Unlock'); await sleep(600); check('re-entry asks for the password', /unlock saved wallets|enter your password/i.test(await body2())); await (await p2.$('input[type=password]')).type('hunter22'); await click2('Unlock'); check('password opens it', await waitText2('receive')); await sleep(1000); - await p2.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); - check('both wallets survived the re-encrypt', (await savingsRows()) === 2, `${await savingsRows()} row(s)`); - await click2('Back'); await sleep(500); + let n = await walletCount(); + check('both wallets survived the re-encrypt', n === 2, `${n} wallet(s)`); console.log('\n[a new wallet against a passworded vault: the ask explains itself]'); await p2.reload({ waitUntil: 'domcontentloaded' }); // session restore again: vault password not in memory @@ -176,14 +183,14 @@ try { check('the right one saves C and locks in place', await waitText2('receive')); await p2.reload({ waitUntil: 'domcontentloaded' }); check('locked view survives the reload', await waitText2('receive')); - await clickTitle2('Unlock'); + await padlock(p2, 'Unlock'); await sleep(600); await (await p2.$('input[type=password]')).type('hunter22'); await click2('Unlock'); check('unlocked once more', await waitText2('receive')); await sleep(1000); - await p2.evaluate(() => document.querySelector('.balance-switch')?.click()); await sleep(600); - check('all three wallets are in the vault', (await savingsRows()) === 3, `${await savingsRows()} row(s)`); + n = await walletCount(); + check('all three wallets are in the vault', n === 3, `${n} wallet(s)`); await ctx2.close(); } catch (e) { check('run completed', false, e.message); -- cgit v1.2.3 From f5450cd46cf752a843205ff51e6d12cf0da4dac8 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:23:42 -0700 Subject: Communities: a room rebuilt with fresher keys folds its channels again When our own community list came back with a channel key the local copy lacked (every community we create: its list entry carries the keys), the room was torn down to be rebuilt with them, but the wraps it had already opened stayed in seenWraps. The rebuilt room's subscription then skipped the relay replay, so its Control fold held only editions newer than the rebuild: a second channel showed up as the only one (no picker; general gone from the fold), and the guestbook lost its joins. Each room now remembers the wrap ids it opened and hands them back when it is rebuilt. chat-layout-test reaches Messages through the bottom nav. Co-Authored-By: Claude Opus 5.5 --- src/features/messages.js | 13 ++++++++++++- tools/chat-layout-test.js | 5 ++--- 2 files changed, 14 insertions(+), 4 deletions(-) (limited to 'tools') diff --git a/src/features/messages.js b/src/features/messages.js index 9745717..02a19b4 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -1178,6 +1178,10 @@ export function messagesFeature(ctx) { folded: null, controlEntries: [], guestEntries: [], + // the wraps this room opened: a room rebuilt with fresher keys hands + // them back to seenWraps, or its new subscription would skip the + // replay and fold without them + wrapIds: new Set(), members: new Map(), byChannel: new Map(), edits: new Map(), @@ -1253,6 +1257,7 @@ export function messagesFeature(ctx) { subscribeOn(room.relays, { kinds: [1059], authors: [room.control.pk], limit: 500 }, (wrap) => { if (seenWraps.has(wrap.id)) return; seenWraps.add(wrap.id); + room.wrapIds.add(wrap.id); openWrapBg(wrap, room.control, (opened) => { if (!opened || opened.rumor.kind !== 3308) return; room.controlEntries.push(opened); @@ -1262,6 +1267,7 @@ export function messagesFeature(ctx) { subscribeOn(room.relays, { kinds: [1059], authors: [room.guestbook.pk], limit: 500 }, (wrap) => { if (seenWraps.has(wrap.id)) return; seenWraps.add(wrap.id); + room.wrapIds.add(wrap.id); openWrapBg(wrap, room.guestbook, (opened) => { if (!opened) return; room.guestEntries.push(opened); @@ -1288,6 +1294,7 @@ export function messagesFeature(ctx) { const u = subscribeOn(room.relays, { kinds: [1059, 21059], authors: [stream.pk], limit: 200 }, (wrap) => { if (seenWraps.has(wrap.id)) return; seenWraps.add(wrap.id); + room.wrapIds.add(wrap.id); openWrapBg(wrap, stream, (opened) => { if (!opened) return; onChat(room, id, opened); @@ -2508,7 +2515,11 @@ export function messagesFeature(ctx) { if (!newer && !fill) continue; Object.assign(have, jmSubset(jm), { name: have.name || jm.name, relays: have.relays && have.relays.length ? have.relays : jm.relays }); const stale = rooms.get(have.community_id); // rebuilt with the new keys on next open - if (stale) { for (const u of stale.unsubs || []) { try { u(); } catch {} } rooms.delete(have.community_id); } + if (stale) { + for (const u of stale.unsubs || []) { try { u(); } catch {} } + for (const id of stale.wrapIds || []) seenWraps.delete(id); + rooms.delete(have.community_id); + } changed = true; continue; } diff --git a/tools/chat-layout-test.js b/tools/chat-layout-test.js index 34c8dc0..875f75b 100644 --- a/tools/chat-layout-test.js +++ b/tools/chat-layout-test.js @@ -51,9 +51,8 @@ try { await page.type('textarea', generateMnemonic(wordlist)); await click('button', 'Open wallet'); await waitText('receive', 15000); - // .header-msgs is the shared header-button class — settings, lock and - // search wear it too, and the first one is no longer this one - await page.evaluate(() => [...document.querySelectorAll('button')] + // Messages is a button in the bottom nav + await page.evaluate(() => [...document.querySelectorAll('.app-nav-button')] .find((b) => /message/i.test(b.getAttribute('aria-label') || ''))?.click()); await sleep(800); await click('button', 'Create'); -- cgit v1.2.3 From 190d27995e751ad3f64d84832ea3358765bc5c63 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:26:57 -0700 Subject: unread-dot, sw-reload tests follow the bottom nav and the split build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - unread-dot: the front door's way to import is Create a new wallet → Have an existing seed; the dot is the Messages bottom-nav button's - sw-reload: the split build loads app-.js, so a restamped build asks for app-deadbeef….js; serve the real entry for it Co-Authored-By: Claude Opus 5.5 --- tools/sw-reload-test.js | 4 +++- tools/unread-dot-test.js | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) (limited to 'tools') diff --git a/tools/sw-reload-test.js b/tools/sw-reload-test.js index a869d9d..7a41c7f 100644 --- a/tools/sw-reload-test.js +++ b/tools/sw-reload-test.js @@ -32,7 +32,9 @@ const server = Bun.serve({ if (p === '/' || p === '/index.html') return new Response(current.html, { headers: { 'content-type': 'text/html', 'cache-control': 'no-store' } }); // The worker precaches the shell during install and addAll rejects on any // miss — a 404 here means it never activates and the test proves nothing. - const f = Bun.file('dist' + p); + // The split build names its entry after the token (app-.js), so + // a restamped build asks for app-deadbeef….js: the same file. + const f = Bun.file('dist' + p.replace(/deadbeef0000\d/, token)); return new Response(f, { headers: { 'cache-control': 'no-store' } }); }, }); diff --git a/tools/unread-dot-test.js b/tools/unread-dot-test.js index 839105a..b98388a 100644 --- a/tools/unread-dot-test.js +++ b/tools/unread-dot-test.js @@ -1,4 +1,4 @@ -// The unread dot either says someone is waiting or it doesn't — it must not +// The unread dot (on Messages in the bottom nav) either says someone is waiting or it doesn't — it must not // flash on the way in. render() rebuilds the whole DOM, so any entrance // animation on the dot replays on every repaint; an earlier build popped it // seven times in the first seconds after a refresh, which reads as flashing. @@ -23,7 +23,7 @@ try { 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 click('button', 'Create a new wallet'); await sleep(300); await click('button', 'Have an existing seed'); await sleep(300); @@ -36,11 +36,11 @@ try { // Now the refresh the report is about. await page.evaluate(() => { window.__dot = { anims: 0, appear: 0, disappear: 0, renders: 0 }; - addEventListener('animationstart', (e) => { if (e.target && e.target.classList && e.target.classList.contains('header-msgs')) window.__dot.anims++; }, true); + addEventListener('animationstart', (e) => { if (e.target && e.target.classList && e.target.closest && e.target.closest('.app-nav-button')) window.__dot.anims++; }, true); let had = null; new MutationObserver(() => { window.__dot.renders++; - const now = !!document.querySelector('.header-msgs.unread'); + const now = [...document.querySelectorAll('.app-nav-button.unread')].some((b) => /messages/i.test(b.getAttribute('aria-label') || '')); if (had !== null && now !== had) { if (now) window.__dot.appear++; else window.__dot.disappear++; } had = now; }).observe(document.getElementById('app'), { childList: true, subtree: true }); @@ -49,17 +49,17 @@ try { // re-arm after the reload wipes it await page.evaluate(() => { window.__dot = { anims: 0, appear: 0, disappear: 0, renders: 0 }; - addEventListener('animationstart', (e) => { if (e.target && e.target.classList && e.target.classList.contains('header-msgs')) window.__dot.anims++; }, true); + addEventListener('animationstart', (e) => { if (e.target && e.target.classList && e.target.closest && e.target.closest('.app-nav-button')) window.__dot.anims++; }, true); let had = null; new MutationObserver(() => { window.__dot.renders++; - const now = !!document.querySelector('.header-msgs.unread'); + const now = [...document.querySelectorAll('.app-nav-button.unread')].some((b) => /messages/i.test(b.getAttribute('aria-label') || '')); if (had !== null && now !== had) { if (now) window.__dot.appear++; else window.__dot.disappear++; } had = now; }).observe(document.body, { childList: true, subtree: true }); }); await sleep(12000); - const r = await page.evaluate(() => ({ ...window.__dot, dotNow: !!document.querySelector('.header-msgs.unread') })); + const r = await page.evaluate(() => ({ ...window.__dot, dotNow: [...document.querySelectorAll('.app-nav-button.unread')].some((b) => /messages/i.test(b.getAttribute('aria-label') || '')) })); console.log('over 12s after a refresh:'); const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; check('the dot never animates', r.anims === 0, `${r.anims} animation(s) started`); -- cgit v1.2.3 From d04f10db5e3c007db2cd8a5f9e12eff55bc1c653 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:32:04 -0700 Subject: profile-feed-test: the ⚡ lives in each note's action bar; holding it sets the amount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tap pays the default amount at once (21 sats unless set), so the test holds the button to open the amount screen instead of tapping an empty wallet into a failed payment. Co-Authored-By: Claude Opus 5.5 --- tools/profile-feed-test.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) (limited to 'tools') diff --git a/tools/profile-feed-test.js b/tools/profile-feed-test.js index 266e3f8..8ea7faa 100644 --- a/tools/profile-feed-test.js +++ b/tools/profile-feed-test.js @@ -53,13 +53,27 @@ try { const empty = !fed && (await bodyText()).includes('No recent posts'); check('note feed resolves', fed || empty, fed ? 'notes shown' : 'no posts for this pk'); if (fed) { - const zaps = await page.evaluate(() => [...document.querySelectorAll('button')].filter((b) => b.textContent.trim() === '⚡').length); + // the ⚡ in every note's action bar, live when the wallet can pay + const zaps = await page.evaluate(() => [...document.querySelectorAll('.notes-feed button.note-zap')].filter((b) => b.getAttribute('aria-disabled') !== 'true').length); check('zap buttons on notes', zaps > 0, `${zaps} note(s) zappable`); const imgsBroken = await page.evaluate(() => [...document.querySelectorAll('.note-img')].some((i) => i.style.display !== 'none' && i.complete && i.naturalWidth === 0)); check('no broken inline images', !imgsBroken); - // tap a zap: should land on the send tab's zap card (resolution ladder) - await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '⚡')?.click()); - check('zap flow opens', await waitText('Send a zap', 10000)); + // A tap pays the default amount at once (this wallet is empty, so it + // would only fail); holding ⚡ opens the screen that sets the amount. + const box = await page.evaluate(() => { + const b = [...document.querySelectorAll('.notes-feed button.note-zap')].find((x) => x.getAttribute('aria-disabled') !== 'true'); + if (!b) return null; + b.scrollIntoView({ block: 'center' }); + const r = b.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; + }); + if (box) { + await page.mouse.move(box.x, box.y); + await page.mouse.down(); + await sleep(800); + await page.mouse.up(); + } + check('zap flow opens', await waitText('Set your zap amount', 10000)); } } check('no page errors', pageErrors.length === 0, pageErrors.slice(0, 2).join(' | ')); -- cgit v1.2.3