aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-07 08:34:13 -0700
committerAdam Soltys <[email protected]>2026-10-07 08:34:13 -0700
commit6d288aeb194d2765282cf9fd72bd1f5fdd747a71 (patch)
tree9ea340ae990af7e675d0871a80d4d3cdd9b0ac4a
parenta6bae186071e7e07136a37f63864d0bf578edd16 (diff)
parentd04f10db5e3c007db2cd8a5f9e12eff55bc1c653 (diff)
downloadcoinosv3-6d288aeb194d2765282cf9fd72bd1f5fdd747a71.tar.gz
coinosv3-6d288aeb194d2765282cf9fd72bd1f5fdd747a71.zip
Merge branch 'worktree-agent-ab051145d709af327'
-rw-r--r--src/features/messages.js31
-rw-r--r--tools/amber-login-test.js26
-rw-r--r--tools/avatar-profile-test.js1
-rw-r--r--tools/chat-layout-test.js5
-rw-r--r--tools/onboard-backup-test.js14
-rw-r--r--tools/padlock-flow-test.js51
-rw-r--r--tools/profile-draft-test.js2
-rw-r--r--tools/profile-feed-test.js30
-rw-r--r--tools/profile-open-test.js6
-rw-r--r--tools/profile-recovery-test.js4
-rw-r--r--tools/sw-reload-test.js4
-rw-r--r--tools/unread-dot-test.js14
12 files changed, 127 insertions, 61 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 899ee1d..bbec23e 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -645,7 +645,13 @@ export function messagesFeature(ctx) {
if (!wallet.mnemonic && !wallet.xprv && !wallet.xpub) return;
profilesWarmed = true;
const cached = profilesDisk();
- for (const [pk, p] of Object.entries(cached)) if (!profiles.has(pk)) profiles.set(pk, p);
+ // (a row asked for before the cache opened — still loading, or a miss
+ // while offline — gives way to the remembered face; a kind 0 that
+ // really named nothing keeps its say)
+ for (const [pk, p] of Object.entries(cached)) {
+ const cur = profiles.get(pk);
+ if (!cur || (!cur.name && !cur.picture && !cur.eventAt)) profiles.set(pk, p);
+ }
// someone known to have no face paints their punk from the first frame,
// instead of a blank circle that turns into one when the relays answer
for (const [pk, t] of Object.entries(facelessDisk())) {
@@ -1176,6 +1182,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(),
@@ -1251,6 +1261,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);
@@ -1260,6 +1271,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);
@@ -1286,6 +1298,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);
@@ -2140,7 +2153,11 @@ export function messagesFeature(ctx) {
// public data, and Back lands on the app's own front door.
if (urlFeed) {
ui.pubProf = true; ui.chatOpen = true; ui.msgView = 'feed';
+ // hold the frame until the URL's feed is chosen: painted a tick early,
+ // the feed view would open (and fetch) the default feed first
+ ui.pubFeedPending = true;
setTimeout(() => {
+ ui.pubFeedPending = null;
if (openFeedUrl(urlFeed.path, urlFeed.search)) return;
ui.pubProf = null; ui.chatOpen = false; ui.msgView = null; // not a feed we know: the front door
render();
@@ -2506,7 +2523,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;
}
@@ -4789,8 +4810,9 @@ export function messagesFeature(ctx) {
const curateFeed = (evs) => { const lang = getLang(); return evs.filter((e) => !isMachinePost(e.content) && inLanguage(e.content, lang)); };
async function popularPass(extra, merge, c, def) {
// Only the default public feed shares a snapshot. Custom relays and older
- // pages keep their own queries. A static/offline install falls back quickly.
- if (extra.until == null && !feedRelays(def)) {
+ // pages keep their own queries. A static/offline install falls back quickly;
+ // the single file opened from a disk has no server to ask at all.
+ if (extra.until == null && !feedRelays(def) && /^https?:$/.test(location.protocol)) {
try {
const response = await fetch('/api/feed', { signal: AbortSignal.timeout(1500) });
if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) throw new Error('No feed cache');
@@ -12816,6 +12838,7 @@ export function messagesFeature(ctx) {
ctx.brandHeader(false),
h('div', { class: 'card row gap6', style: 'justify-content:center;align-items:center;padding:16px' },
h('span', { class: 'spinner sm' }), h('span', { class: 'small faint' }, t('noteRefLoading'))));
+ if (ui.pubFeedPending) return h('div', { class: 'col', style: 'gap:16px' }, ctx.brandHeader(false));
if (ui.pubProfPending) return h('div', { class: 'col', style: 'gap:16px' },
ctx.brandHeader(false),
h('div', { class: 'card col', style: 'gap:12px' },
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/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/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');
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);
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..8ea7faa 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));
@@ -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(' | '));
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();
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-<token>.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`);