aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-07 12:02:51 -0700
committerAdam Soltys <[email protected]>2026-10-07 12:02:51 -0700
commit86d9818abfc160dac031bb292abe28d6f28fc624 (patch)
treea96983d60fb0f4dcb92a04c7aba50761da05cf9f /tools
parentb415b1fb31bd734619e1da3c88ca7e3822151c87 (diff)
downloadcoinosv3-86d9818abfc160dac031bb292abe28d6f28fc624.tar.gz
coinosv3-86d9818abfc160dac031bb292abe28d6f28fc624.zip
Browser tests: public relays answered in the page, never reached
A suite run let ~90 throwaway test wallets subscribe and publish on public relays from one home address, and nos.lol and relay.damus.io banned it. tools/puppeteer.js wraps puppeteer-core: every socket to a non-local host gets an in-page stand-in relay (EVENT -> OK, REQ -> EOSE). Tests that read real content launch with { relays: 'read' } (real reads, publishes answered in place); feed-live, chat-layout and cordn-ui launch with { relays: 'live' }. LIVE_RELAYS=read|1|host,... overrides per run. profile-key-qr: measure the QR again if a repaint swapped it out. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'tools')
-rw-r--r--tools/account-access-test.js2
-rw-r--r--tools/amber-login-test.js2
-rw-r--r--tools/ark/cooperative-exit-ui-test.js2
-rw-r--r--tools/ark/gift-shot.js2
-rw-r--r--tools/ark/offboard-minimum-test.js2
-rw-r--r--tools/ark/renew-confirm-ui-test.js2
-rw-r--r--tools/ark/ui-shots.js2
-rw-r--r--tools/ark/ui-test.js2
-rw-r--r--tools/avatar-profile-test.js2
-rw-r--r--tools/avatar-thumb-test.js2
-rw-r--r--tools/boot-balance-test.js2
-rw-r--r--tools/boot-signin-flash-test.js2
-rw-r--r--tools/chat-layout-test.js4
-rw-r--r--tools/chat-watchdog-test.js4
-rw-r--r--tools/comments-test.js4
-rw-r--r--tools/compose-preview-test.js2
-rw-r--r--tools/composer-media-test.js2
-rw-r--r--tools/cordn-ui-test.js4
-rw-r--r--tools/emoji-test.js4
-rw-r--r--tools/feed-bottom-nav-test.js2
-rw-r--r--tools/feed-choice-per-wallet-test.js2
-rw-r--r--tools/feed-custom-test.js4
-rw-r--r--tools/feed-editor-relays-test.js2
-rw-r--r--tools/feed-follow-test.js2
-rw-r--r--tools/feed-list-test.js2
-rw-r--r--tools/feed-live-test.js4
-rw-r--r--tools/feed-media-cache-test.js4
-rw-r--r--tools/feed-media-views-test.js2
-rw-r--r--tools/feed-new-posts-test.js2
-rw-r--r--tools/feed-stability-test.js2
-rw-r--r--tools/feed-streaming-test.js2
-rw-r--r--tools/feed-switch-seed-test.js2
-rw-r--r--tools/feed-video-aspect-test.js2
-rw-r--r--tools/feed-wallet-back-test.js2
-rw-r--r--tools/feed-window-dom-test.js4
-rw-r--r--tools/follows-overwrite-test.js2
-rw-r--r--tools/image-tap-test.js2
-rw-r--r--tools/image-zoom-test.js2
-rw-r--r--tools/link-preview-test.js4
-rw-r--r--tools/lock-wallet-test.js2
-rw-r--r--tools/locked-gift-test.js2
-rw-r--r--tools/markdown-note-test.js2
-rw-r--r--tools/marmot-chat-test.js2
-rw-r--r--tools/minimal-smoke.js2
-rw-r--r--tools/moderation-test.js4
-rw-r--r--tools/note-page-test.js2
-rw-r--r--tools/notif-categories-test.js2
-rw-r--r--tools/notif-faces-test.js2
-rw-r--r--tools/notif-note-cache-test.js2
-rw-r--r--tools/offline-cache-test.js4
-rw-r--r--tools/onboard-backup-test.js2
-rw-r--r--tools/padlock-ask-test.js2
-rw-r--r--tools/padlock-flow-test.js2
-rw-r--r--tools/poll-test.js4
-rw-r--r--tools/pos-test.js2
-rw-r--r--tools/post-actions-test.js2
-rw-r--r--tools/post-emoji-test.js2
-rw-r--r--tools/profile-actions-test.js4
-rw-r--r--tools/profile-batch-test.js4
-rw-r--r--tools/profile-draft-test.js4
-rw-r--r--tools/profile-feed-test.js4
-rw-r--r--tools/profile-key-qr-test.js6
-rw-r--r--tools/profile-open-test.js4
-rw-r--r--tools/profile-recovery-test.js2
-rw-r--r--tools/public-feed-browser-test.js2
-rw-r--r--tools/puppeteer.js126
-rw-r--r--tools/push-unavailable-test.js2
-rw-r--r--tools/quote-card-test.js4
-rw-r--r--tools/relays-settings-test.js2
-rw-r--r--tools/reply-count-test.js2
-rw-r--r--tools/room-faces-test.js4
-rw-r--r--tools/send-avatar-loading-test.js2
-rw-r--r--tools/send-dest-test.js2
-rw-r--r--tools/send-lnaddr-advance-test.js2
-rw-r--r--tools/standalone-file-test.js2
-rw-r--r--tools/sw-reload-test.js2
-rw-r--r--tools/sync-browser-test.js2
-rw-r--r--tools/thread-back-scroll-test.js2
-rw-r--r--tools/thread-reply-test.js2
-rw-r--r--tools/topic-depth-test.js4
-rw-r--r--tools/ui-morph-test.js2
-rw-r--r--tools/unifiedpush-test.js2
-rw-r--r--tools/unread-dot-test.js2
-rw-r--r--tools/upload-progress-test.js2
-rw-r--r--tools/video-autoplay-test.js2
-rw-r--r--tools/video-compress-test.js2
-rw-r--r--tools/video-unmute-test.js2
-rw-r--r--tools/youtube-embed-test.js2
-rw-r--r--tools/zap-deep-profile-test.js4
-rw-r--r--tools/zap-feedback-test.js2
-rw-r--r--tools/zap-tally-cache-test.js2
91 files changed, 241 insertions, 113 deletions
diff --git a/tools/account-access-test.js b/tools/account-access-test.js
index 4a83fd0..c9af32e 100644
--- a/tools/account-access-test.js
+++ b/tools/account-access-test.js
@@ -2,7 +2,7 @@
// account switching and logout. Uses an unfunded, generated public key only.
// Run: bun tools/account-access-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { accountXpubFor, cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/amber-login-test.js b/tools/amber-login-test.js
index 0b4c3c9..9da49a8 100644
--- a/tools/amber-login-test.js
+++ b/tools/amber-login-test.js
@@ -3,7 +3,7 @@
// pasted); creating the wallet then makes an encrypt and a sign round trip.
// The intent launcher and the clipboard are stubbed; relays answer nothing.
// Run: bun tools/amber-login-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { npubEncode } from 'nostr-tools/nip19';
import { buildHtml } from '../build.js';
diff --git a/tools/ark/cooperative-exit-ui-test.js b/tools/ark/cooperative-exit-ui-test.js
index d8f990d..d8fa0d8 100644
--- a/tools/ark/cooperative-exit-ui-test.js
+++ b/tools/ark/cooperative-exit-ui-test.js
@@ -1,7 +1,7 @@
// Real app navigation/forms with a local Ark manager stub; never transfers funds.
// Run: bun tools/ark/cooperative-exit-ui-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
diff --git a/tools/ark/gift-shot.js b/tools/ark/gift-shot.js
index aac6809..f8c96a8 100644
--- a/tools/ark/gift-shot.js
+++ b/tools/ark/gift-shot.js
@@ -2,7 +2,7 @@
// wallet, open its link in the same browser (wallet 1 still in session),
// capture the "choose where to receive" screen.
import { spawn, execSync } from 'node:child_process';
-import puppeteer from 'puppeteer-core';
+import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
diff --git a/tools/ark/offboard-minimum-test.js b/tools/ark/offboard-minimum-test.js
index 9593089..94d166c 100644
--- a/tools/ark/offboard-minimum-test.js
+++ b/tools/ark/offboard-minimum-test.js
@@ -3,7 +3,7 @@
// note for when the ASP actually failed to co-operate. Local manager stub.
// Run: bun tools/ark/offboard-minimum-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
diff --git a/tools/ark/renew-confirm-ui-test.js b/tools/ark/renew-confirm-ui-test.js
index e5a68c8..698c424 100644
--- a/tools/ark/renew-confirm-ui-test.js
+++ b/tools/ark/renew-confirm-ui-test.js
@@ -5,7 +5,7 @@
// 2026-10-03 report; nothing is sent anywhere.
// Run: bun tools/ark/renew-confirm-ui-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
diff --git a/tools/ark/ui-shots.js b/tools/ark/ui-shots.js
index 5331ab8..cda336a 100644
--- a/tools/ark/ui-shots.js
+++ b/tools/ark/ui-shots.js
@@ -1,7 +1,7 @@
// Screenshot run: import a wallet, receive over Ark, show the balance card +
// Ark settings card. Writes PNGs to /tmp/ark-ui-*.png.
import { spawn, execSync } from 'node:child_process';
-import puppeteer from 'puppeteer-core';
+import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
diff --git a/tools/ark/ui-test.js b/tools/ark/ui-test.js
index e4e2e5a..75d3fc5 100644
--- a/tools/ark/ui-test.js
+++ b/tools/ark/ui-test.js
@@ -9,7 +9,7 @@
// Usage: bun tools/ark/ui-test.js (ark regtest stack up)
import { execSync } from 'node:child_process';
-import puppeteer from 'puppeteer-core';
+import puppeteer from '../puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { ALICE, aliceReady, aliceBalance } from './alice.js';
diff --git a/tools/avatar-profile-test.js b/tools/avatar-profile-test.js
index 4ed1b25..5c5293d 100644
--- a/tools/avatar-profile-test.js
+++ b/tools/avatar-profile-test.js
@@ -3,7 +3,7 @@
// wherever the reader was in the feed. Back returns to the same place.
//
// Run: bun tools/avatar-profile-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/avatar-thumb-test.js b/tools/avatar-thumb-test.js
index e7ff843..4a13f33 100644
--- a/tools/avatar-thumb-test.js
+++ b/tools/avatar-thumb-test.js
@@ -9,7 +9,7 @@
// and that a refresh of the profile doesn't throw the thumbnail away.
//
// Run: bun tools/avatar-thumb-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { seedPubkey } from '../src/nostr.js';
diff --git a/tools/boot-balance-test.js b/tools/boot-balance-test.js
index 482149b..96a60d2 100644
--- a/tools/boot-balance-test.js
+++ b/tools/boot-balance-test.js
@@ -10,7 +10,7 @@
// first appears, on a RELOAD (session restore — the case the user sees).
//
// Run: bun tools/boot-balance-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/boot-signin-flash-test.js b/tools/boot-signin-flash-test.js
index 75f5b50..c9c42de 100644
--- a/tools/boot-signin-flash-test.js
+++ b/tools/boot-signin-flash-test.js
@@ -9,7 +9,7 @@
// for the welcome screen.
//
// Run: bun tools/boot-signin-flash-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/chat-layout-test.js b/tools/chat-layout-test.js
index 875f75b..a6f4162 100644
--- a/tools/chat-layout-test.js
+++ b/tools/chat-layout-test.js
@@ -4,7 +4,7 @@
// composer below the fold.
//
// Run: bun tools/chat-layout-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
@@ -20,7 +20,7 @@ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? '
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5229, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'live', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
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 setInput = (sel, v) => page.evaluate((s, val) => { const e = document.querySelector(s); if (!e) return false; e.value = val; e.dispatchEvent(new Event('input', { bubbles: true })); return true; }, sel, v);
diff --git a/tools/chat-watchdog-test.js b/tools/chat-watchdog-test.js
index 16a9ae0..d3f17c8 100644
--- a/tools/chat-watchdog-test.js
+++ b/tools/chat-watchdog-test.js
@@ -10,7 +10,7 @@
// sockets are reached through a registry shimmed onto window.WebSocket.
//
// Run: bun tools/chat-watchdog-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
@@ -21,7 +21,7 @@ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? '
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5232, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
// every socket the page opens, so the test can kill them the way the OS does
await page.evaluateOnNewDocument(() => {
diff --git a/tools/comments-test.js b/tools/comments-test.js
index 6801f44..5a73e6f 100644
--- a/tools/comments-test.js
+++ b/tools/comments-test.js
@@ -5,7 +5,7 @@
// are caught at the socket; nothing is published.
//
// Run: bun tools/comments-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { SimplePool } from 'nostr-tools/pool';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -23,7 +23,7 @@ for (let i = 0; i < 3 && !(root && comments.length); i++) {
if (!root || !comments.length) { console.log(' ✗ the article or its comments are gone from the relays', !!root, comments.length); process.exit(1); }
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5283, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
// The article and its comments, fetched above, are also handed to the app
// on every relay socket that asks for them: the app reaching the same public
diff --git a/tools/compose-preview-test.js b/tools/compose-preview-test.js
index c943655..fdfb05b 100644
--- a/tools/compose-preview-test.js
+++ b/tools/compose-preview-test.js
@@ -8,7 +8,7 @@
// preview shows is what a real post shows, for the same text.
//
// Run: bun tools/compose-preview-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/composer-media-test.js b/tools/composer-media-test.js
index 6f30995..512b8bd 100644
--- a/tools/composer-media-test.js
+++ b/tools/composer-media-test.js
@@ -5,7 +5,7 @@
// paperclip, its URL landing in the draft.
//
// Run: bun tools/composer-media-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
diff --git a/tools/cordn-ui-test.js b/tools/cordn-ui-test.js
index 6948428..8fc707a 100644
--- a/tools/cordn-ui-test.js
+++ b/tools/cordn-ui-test.js
@@ -5,7 +5,7 @@
// cordn-interop-test.js).
//
// Run: bun tools/cordn-ui-test.js [screenshot.png]
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { npubEncode } from 'nostr-tools/nip19';
@@ -29,7 +29,7 @@ const aliceRef = /ref=([0-9a-f]{64})/.exec(alice('gen-kp kp' + Date.now().toStri
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5234, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'live', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
const errs = [];
page.on('pageerror', (e) => { errs.push(e.message); console.log(' [pageerror]', e.message); });
diff --git a/tools/emoji-test.js b/tools/emoji-test.js
index d661329..c91cec0 100644
--- a/tools/emoji-test.js
+++ b/tools/emoji-test.js
@@ -4,7 +4,7 @@
// installs the pack. Read-only: nothing is posted.
//
// Run: bun tools/emoji-test.js [screenshot.png]
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
@@ -16,7 +16,7 @@ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? '
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5231, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
page.on('pageerror', (e) => console.log(' [pageerror]', e.message));
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);
diff --git a/tools/feed-bottom-nav-test.js b/tools/feed-bottom-nav-test.js
index 4354e97..ea466fd 100644
--- a/tools/feed-bottom-nav-test.js
+++ b/tools/feed-bottom-nav-test.js
@@ -1,7 +1,7 @@
// Real feed/navigation views with local posts and no relay traffic.
// Run: bun tools/app-bottom-nav-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf("const root = document.getElementById('app');"));
const entry = `
diff --git a/tools/feed-choice-per-wallet-test.js b/tools/feed-choice-per-wallet-test.js
index 0642bde..322388e 100644
--- a/tools/feed-choice-per-wallet-test.js
+++ b/tools/feed-choice-per-wallet-test.js
@@ -1,7 +1,7 @@
// The feed you were on is remembered per wallet: a new account starts on
// Popular, not on whatever the last account on this device was reading.
// Run: bun tools/feed-choice-per-wallet-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/feed-custom-test.js b/tools/feed-custom-test.js
index c2c94de..ef595ff 100644
--- a/tools/feed-custom-test.js
+++ b/tools/feed-custom-test.js
@@ -4,7 +4,7 @@
// feed, and deleting a feed. No relays are contacted for real — the
// WebSocket shim answers the subscriptions.
// Run: bun tools/feed-custom-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -28,7 +28,7 @@ const PACK_NADDR = naddrEncode({ kind: 39089, pubkey: PACK.pubkey, identifier: '
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5297, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument((pack) => {
window.__reqs = [];
diff --git a/tools/feed-editor-relays-test.js b/tools/feed-editor-relays-test.js
index 5a808a7..c12a22f 100644
--- a/tools/feed-editor-relays-test.js
+++ b/tools/feed-editor-relays-test.js
@@ -2,7 +2,7 @@
// behind the feed and leave the form up), and it can make a firehose —
// every post on the relays you name — as a saved feed.
// Run: bun tools/feed-editor-relays-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/feed-follow-test.js b/tools/feed-follow-test.js
index bae06a9..f0f688c 100644
--- a/tools/feed-follow-test.js
+++ b/tools/feed-follow-test.js
@@ -6,7 +6,7 @@
// way it would arrive from one.
//
// Run: bun tools/feed-follow-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/feed-list-test.js b/tools/feed-list-test.js
index 641d43d..6d60240 100644
--- a/tools/feed-list-test.js
+++ b/tools/feed-list-test.js
@@ -5,7 +5,7 @@
// unticking "Keep as a nostr list" sends the NIP-09 deletion. No relay is
// written for real — the WebSocket shim swallows EVENT and answers OK.
// Run: bun tools/feed-list-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
diff --git a/tools/feed-live-test.js b/tools/feed-live-test.js
index 8f80d46..b9879d0 100644
--- a/tools/feed-live-test.js
+++ b/tools/feed-live-test.js
@@ -10,7 +10,7 @@
// notice takes you up to them when tapped.
//
// Run: bun tools/feed-live-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -41,7 +41,7 @@ const server = Bun.serve({ port: 5296, fetch: async (req) => {
}
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'live', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
window.__feedReqs = 0;
diff --git a/tools/feed-media-cache-test.js b/tools/feed-media-cache-test.js
index 46c485d..02850ef 100644
--- a/tools/feed-media-cache-test.js
+++ b/tools/feed-media-cache-test.js
@@ -3,7 +3,7 @@
// slow picture), a reload paints the grid from that cache at once and
// resumes the crawl where it stopped, and tile frames are kept as pictures.
// Run: bun tools/feed-media-cache-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -64,7 +64,7 @@ all.push(tagged(151, 'size hidden ' + base + '/h/h4.mp4', base + '/h/h4.mp4'));
all.push(tagged(152, 'no cors ' + base + '/n/n5.mp4', base + '/n/n5.mp4'));
all.push(tagged(153, 'seen in the feed first ' + base + '/r/r6.mp4', base + '/r/r6.mp4'));
-const browser = await puppeteer.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument(([evs, pk]) => {
window.__reqs = [];
diff --git a/tools/feed-media-views-test.js b/tools/feed-media-views-test.js
index e29d346..4890981 100644
--- a/tools/feed-media-views-test.js
+++ b/tools/feed-media-views-test.js
@@ -5,7 +5,7 @@
// until tapped: then the author, the words as a caption, likes and sats.
// Back and Escape close it.
// Run: bun tools/feed-media-views-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
diff --git a/tools/feed-new-posts-test.js b/tools/feed-new-posts-test.js
index 4f5b8fb..aa3e667 100644
--- a/tools/feed-new-posts-test.js
+++ b/tools/feed-new-posts-test.js
@@ -2,7 +2,7 @@
// each new post keeps a tint until it has been in view a couple of seconds.
// Run: bun tools/feed-new-posts-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf("const root = document.getElementById('app');"));
const entry = `
diff --git a/tools/feed-stability-test.js b/tools/feed-stability-test.js
index 1426bba..6befa1e 100644
--- a/tools/feed-stability-test.js
+++ b/tools/feed-stability-test.js
@@ -1,6 +1,6 @@
// Offline browser regression: cached rows must arrive whole and stay still.
// Run: bun tools/feed-stability-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/feed-streaming-test.js b/tools/feed-streaming-test.js
index 3070771..c69b1eb 100644
--- a/tools/feed-streaming-test.js
+++ b/tools/feed-streaming-test.js
@@ -1,7 +1,7 @@
// Signed relay events must surface before EOSE and independently of slow media.
// Run: bun tools/feed-streaming-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { buildVerifyWorker } from '../build.js';
const sk = generateSecretKey(), author = getPublicKey(sk), now = Math.floor(Date.now() / 1000);
diff --git a/tools/feed-switch-seed-test.js b/tools/feed-switch-seed-test.js
index 1bad161..bf2b830 100644
--- a/tools/feed-switch-seed-test.js
+++ b/tools/feed-switch-seed-test.js
@@ -3,7 +3,7 @@
// other open feeds already hold, and once the feed on screen settles the
// other feeds' first posts are fetched ahead into the disk cache.
// Run: bun tools/feed-switch-seed-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
diff --git a/tools/feed-video-aspect-test.js b/tools/feed-video-aspect-test.js
index 65cf9bb..d052b33 100644
--- a/tools/feed-video-aspect-test.js
+++ b/tools/feed-video-aspect-test.js
@@ -4,7 +4,7 @@
// shape still gets the 16:9 box.
// Photos too: a big one paints from a column-sized copy (see makeMediaThumb).
// Run: bun tools/feed-video-aspect-test.js (CHROME=<path> to pick a browser)
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
diff --git a/tools/feed-wallet-back-test.js b/tools/feed-wallet-back-test.js
index 1b0abe7..1b39728 100644
--- a/tools/feed-wallet-back-test.js
+++ b/tools/feed-wallet-back-test.js
@@ -1,7 +1,7 @@
// Exercise the real app history: a public feed must survive Wallet -> Back.
// Run: bun tools/feed-wallet-back-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { buildHtml } from '../build.js';
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: req => new URL(req.url).pathname === '/api/feed'
diff --git a/tools/feed-window-dom-test.js b/tools/feed-window-dom-test.js
index 72ca535..c86e98e 100644
--- a/tools/feed-window-dom-test.js
+++ b/tools/feed-window-dom-test.js
@@ -3,7 +3,7 @@
// page keeps its length, the row under your eyes never moves when the window
// shifts, and scrolling back up brings the top posts back.
// Run: bun tools/feed-window-dom-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
@@ -25,7 +25,7 @@ const server = Bun.serve({ port: 5305, fetch: (req) => {
if (path.startsWith('/punks')) return new Response(Bun.file('dist' + path));
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
// the stub relay holds every post: a kind-1 request is answered from it
// (honouring until/limit), everything else gets a bare EOSE
diff --git a/tools/follows-overwrite-test.js b/tools/follows-overwrite-test.js
index 8ed2a82..d0ea3a2 100644
--- a/tools/follows-overwrite-test.js
+++ b/tools/follows-overwrite-test.js
@@ -8,7 +8,7 @@
// real list, and record every event the wallet publishes.
// Run: bun tools/follows-overwrite-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { bytesToHex } from '@noble/hashes/utils';
diff --git a/tools/image-tap-test.js b/tools/image-tap-test.js
index 5ed182e..2bf840b 100644
--- a/tools/image-tap-test.js
+++ b/tools/image-tap-test.js
@@ -3,7 +3,7 @@
// the picture opens the picture.
//
// Run: bun tools/image-tap-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
diff --git a/tools/image-zoom-test.js b/tools/image-zoom-test.js
index f1a57c5..3ada708 100644
--- a/tools/image-zoom-test.js
+++ b/tools/image-zoom-test.js
@@ -3,7 +3,7 @@
// dismissal; the close button bypasses gesture suppression.
// Run: bun tools/image-zoom-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf("const root = document.getElementById('app');"));
const viewer = app.slice(app.indexOf('function imageViewer()'), app.indexOf("if (typeof window !== 'undefined') {\n window.addEventListener('keydown'"));
diff --git a/tools/link-preview-test.js b/tools/link-preview-test.js
index 0b9e863..52ddca1 100644
--- a/tools/link-preview-test.js
+++ b/tools/link-preview-test.js
@@ -4,7 +4,7 @@
// nothing to say stays a bare link; pictures and YouTube keep their own
// boxes.
// Run: bun tools/link-preview-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -37,7 +37,7 @@ const server = Bun.serve({ port: 5311, fetch: (req) => {
if (u.pathname.startsWith('/punks') || u.pathname === '/verify-worker.js') return new Response(Bun.file('dist' + u.pathname));
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument((posts, profile) => {
// the registrar's preview endpoint answers from this test's server
diff --git a/tools/lock-wallet-test.js b/tools/lock-wallet-test.js
index 107ccd0..b6e8a10 100644
--- a/tools/lock-wallet-test.js
+++ b/tools/lock-wallet-test.js
@@ -6,7 +6,7 @@
// The padlock is the counterpart with its own rules (see padlock-flow-test).
//
// Run: bun tools/lock-wallet-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/locked-gift-test.js b/tools/locked-gift-test.js
index 12c0dff..0522b47 100644
--- a/tools/locked-gift-test.js
+++ b/tools/locked-gift-test.js
@@ -3,7 +3,7 @@
// on the unlock screen, the right one walks into the ordinary claim flow.
//
// Run: bun tools/locked-gift-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { hex, base32nopad } from '@scure/base';
import { buildHtml } from '../build.js';
import { lockGift, previewLockedGift } from '../src/features/gifts-wallet.js';
diff --git a/tools/markdown-note-test.js b/tools/markdown-note-test.js
index 8e7cca1..d353b67 100644
--- a/tools/markdown-note-test.js
+++ b/tools/markdown-note-test.js
@@ -9,7 +9,7 @@
// mangle normal posts to pretty up the rare bridged one.
//
// Run: bun tools/markdown-note-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/marmot-chat-test.js b/tools/marmot-chat-test.js
index 7310fd0..f8bccca 100644
--- a/tools/marmot-chat-test.js
+++ b/tools/marmot-chat-test.js
@@ -14,7 +14,7 @@
// invitation → card → Join → messages both ways; "New message" to a White
// Noise user founding a direct group whose replies land in the DM thread;
// "New group" from the app.
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { spawnSync } from 'node:child_process';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
diff --git a/tools/minimal-smoke.js b/tools/minimal-smoke.js
index ebb57f5..76162d5 100644
--- a/tools/minimal-smoke.js
+++ b/tools/minimal-smoke.js
@@ -5,7 +5,7 @@
// Usage: bun tools/minimal-smoke.js
import { execSync } from 'node:child_process';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/moderation-test.js b/tools/moderation-test.js
index d6df879..426115c 100644
--- a/tools/moderation-test.js
+++ b/tools/moderation-test.js
@@ -5,7 +5,7 @@
// are hidden; Settings → Nostr lays the mute list out and edits it.
// No relay is written for real — the WebSocket shim swallows EVENT.
// Run: bun tools/moderation-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
@@ -39,7 +39,7 @@ const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5299,
fetch: (req, srv) => (new URL(req.url).pathname === '/relay' && srv.upgrade(req) ? undefined : new Response(html, { headers: { 'content-type': 'text/html' } })),
websocket: { message: (ws, data) => { try { const m = JSON.parse(data); if (m[0] === 'REQ') ws.send(JSON.stringify(['EOSE', m[1]])); } catch {} } } });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument((fx) => {
window.__reqs = [];
diff --git a/tools/note-page-test.js b/tools/note-page-test.js
index 915d892..96ced70 100644
--- a/tools/note-page-test.js
+++ b/tools/note-page-test.js
@@ -4,7 +4,7 @@
// no Back button inventing a parent the visitor never came from.
// Run: bun tools/note-page-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { neventEncode } from 'nostr-tools/nip19';
import { parseNoteRef, buildThread, createThreadCache, renderNotePage } from '../feed/note-page.js';
diff --git a/tools/notif-categories-test.js b/tools/notif-categories-test.js
index 92b0c71..f38dd01 100644
--- a/tools/notif-categories-test.js
+++ b/tools/notif-categories-test.js
@@ -1,7 +1,7 @@
// Settings → Notifications lists zaps and reactions as their own categories,
// each with its own switch. Run: bun tools/notif-categories-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const html = await buildHtml({ minify: true, pwa: false });
diff --git a/tools/notif-faces-test.js b/tools/notif-faces-test.js
index 3093f51..b43a41f 100644
--- a/tools/notif-faces-test.js
+++ b/tools/notif-faces-test.js
@@ -3,7 +3,7 @@
// opens with faces, not circles filling in. The stub relay answers the kind-0
// batch for the actors with pictures served by this test.
// Run: bun tools/notif-faces-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
diff --git a/tools/notif-note-cache-test.js b/tools/notif-note-cache-test.js
index c0ceeca..a3173fa 100644
--- a/tools/notif-note-cache-test.js
+++ b/tools/notif-note-cache-test.js
@@ -1,7 +1,7 @@
// Referenced notification notes are fetched before the page opens, persisted,
// and available on the first paint after a reload.
// Run: bun tools/notif-note-cache-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
diff --git a/tools/offline-cache-test.js b/tools/offline-cache-test.js
index c01dc1c..ffa42e4 100644
--- a/tools/offline-cache-test.js
+++ b/tools/offline-cache-test.js
@@ -4,7 +4,7 @@
// build from dist/ so the service worker serves the shell offline.
//
// Run: bun run build && bun tools/offline-cache-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { join } from 'node:path';
@@ -16,7 +16,7 @@ const server = Bun.serve({ port: 5234, async fetch(req) {
if (await f.exists()) return new Response(f, { headers: { 'cache-control': 'no-store' } });
return new Response(Bun.file(dist + '/index.html'), { headers: { 'cache-control': 'no-store' } });
} });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+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('pageerror', (e) => errs.push(e.message)); page.on('console', (m) => { if (m.type() === 'error') errs.push('console: ' + m.text().slice(0, 160)); });
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);
diff --git a/tools/onboard-backup-test.js b/tools/onboard-backup-test.js
index 9fe5a3e..ddef8d7 100644
--- a/tools/onboard-backup-test.js
+++ b/tools/onboard-backup-test.js
@@ -10,7 +10,7 @@
// setup, and if they put it off the wallet keeps one quiet line about it.
//
// Run: bun tools/onboard-backup-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { bech32 } from '@scure/base';
import { generateSecretKey } from 'nostr-tools/pure';
import { seedFromNostrKey } from '../src/nostr-login.js';
diff --git a/tools/padlock-ask-test.js b/tools/padlock-ask-test.js
index 5570b8f..763dcae 100644
--- a/tools/padlock-ask-test.js
+++ b/tools/padlock-ask-test.js
@@ -6,7 +6,7 @@
// leaves you exactly where you tapped.
//
// Run: bun tools/padlock-ask-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/padlock-flow-test.js b/tools/padlock-flow-test.js
index 59aee32..dcc0a85 100644
--- a/tools/padlock-flow-test.js
+++ b/tools/padlock-flow-test.js
@@ -4,7 +4,7 @@
// 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';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/poll-test.js b/tools/poll-test.js
index bc1715e..edcb061 100644
--- a/tools/poll-test.js
+++ b/tools/poll-test.js
@@ -4,7 +4,7 @@
// caught at the socket so the test never votes on someone's real poll.
//
// Run: bun tools/poll-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { SimplePool } from 'nostr-tools/pool';
import { cacheKeyFor } from '../src/wallet.js';
import { neventOf } from '../src/nostr.js';
@@ -21,7 +21,7 @@ const labels = poll.tags.filter((x) => x[0] === 'option').map((x) => x[2]);
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5273, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
window.__votes = [];
diff --git a/tools/pos-test.js b/tools/pos-test.js
index c7c7c29..55349cb 100644
--- a/tools/pos-test.js
+++ b/tools/pos-test.js
@@ -2,7 +2,7 @@
// the bill and the tip counted apart. The Ark side is faked: no relays, no
// server, no money. Run: bun tools/pos-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const domHelper = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
diff --git a/tools/post-actions-test.js b/tools/post-actions-test.js
index 4c5d92e..a33079e 100644
--- a/tools/post-actions-test.js
+++ b/tools/post-actions-test.js
@@ -7,7 +7,7 @@
// a relay to answer, so the test doesn't depend on one).
//
// Run: bun tools/post-actions-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/post-emoji-test.js b/tools/post-emoji-test.js
index 6c7a9aa..558dca4 100644
--- a/tools/post-emoji-test.js
+++ b/tools/post-emoji-test.js
@@ -1,7 +1,7 @@
// Exercise public reaction ingestion, rendering, signing and withdrawal.
// No requests or events leave this local fixture. Run: bun tools/post-emoji-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
const entry = `
diff --git a/tools/profile-actions-test.js b/tools/profile-actions-test.js
index e44c037..1886058 100644
--- a/tools/profile-actions-test.js
+++ b/tools/profile-actions-test.js
@@ -1,7 +1,7 @@
// Send clears on leaving; profile Pay starts fresh; messaging Back pops history.
// Run: bun tools/profile-actions-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
@@ -26,7 +26,7 @@ const server = Bun.serve({ port: 0, fetch(req) {
return new URL(req.url).pathname === '/' || new URL(req.url).pathname.startsWith('/npub1')
? new Response(html, { headers: { 'Content-Type': 'text/html' } }) : new Response(null, { status: 404 });
} });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
const page = await browser.newPage(), errors = [];
page.on('pageerror', (e) => errors.push(e.message));
diff --git a/tools/profile-batch-test.js b/tools/profile-batch-test.js
index 4a993a5..0b78f61 100644
--- a/tools/profile-batch-test.js
+++ b/tools/profile-batch-test.js
@@ -7,7 +7,7 @@
// relays and real pubkeys from that feed.
//
// Run: bun tools/profile-batch-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -31,7 +31,7 @@ if (notes.length < 10) { console.log(' ✗ relays too quiet to test with'); proc
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5273, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
page.on('console', (m) => { const t = m.text(); if (/profiles asked/.test(t)) console.log(' [page]', t); });
// count the kind-0 requests the app sends, and how many pubkeys each carries
diff --git a/tools/profile-draft-test.js b/tools/profile-draft-test.js
index 437dd61..9880d68 100644
--- a/tools/profile-draft-test.js
+++ b/tools/profile-draft-test.js
@@ -2,7 +2,7 @@
// without adding a browser-history entry for each keystroke.
// Run: bun tools/profile-draft-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { seedPubkey } from '../src/nostr.js';
@@ -11,7 +11,7 @@ import { buildHtml } from '../build.js';
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
const origin = `http://localhost:${server.port}`;
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
const page = await browser.newPage();
const errors = [];
page.on('pageerror', e => errors.push(e.message));
diff --git a/tools/profile-feed-test.js b/tools/profile-feed-test.js
index 8ea7faa..f2ca9e6 100644
--- a/tools/profile-feed-test.js
+++ b/tools/profile-feed-test.js
@@ -3,7 +3,7 @@
// live coinos community: open chat, tap an avatar, land on their profile.
//
// Run: bun tools/profile-feed-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
@@ -14,7 +14,7 @@ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? '
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5234, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
const pageErrors = [];
page.on('pageerror', (e) => pageErrors.push(String(e)));
diff --git a/tools/profile-key-qr-test.js b/tools/profile-key-qr-test.js
index fb3c846..f67fc3d 100644
--- a/tools/profile-key-qr-test.js
+++ b/tools/profile-key-qr-test.js
@@ -2,7 +2,7 @@
// really says the npub printed under it.
//
// Run: bun tools/profile-key-qr-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import jsQR from 'jsqr';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
@@ -43,7 +43,9 @@ try {
await page.waitForSelector('.npub-qr svg', { timeout: 5000 });
const npub = await page.evaluate(() => document.querySelector('.npub-box').textContent.trim());
check('the key is shown', /^npub1[a-z0-9]{58}$/.test(npub), npub);
- const box = await (await page.$('.npub-qr')).boundingBox();
+ // a repaint can swap the box out between finding and measuring it
+ let box = null;
+ for (let i = 0; i < 20 && !box; i++) { box = await (await page.$('.npub-qr'))?.boundingBox().catch(() => null); if (!box) await sleep(100); }
check('the QR fits a phone screen', box.width <= 420 && box.x >= 0, JSON.stringify(box));
// read it back the way a camera would: from pixels
const shot = join(tmpdir(), 'profile-key-qr.png');
diff --git a/tools/profile-open-test.js b/tools/profile-open-test.js
index aa0c943..c09a239 100644
--- a/tools/profile-open-test.js
+++ b/tools/profile-open-test.js
@@ -10,7 +10,7 @@
// Measured as the browser measures it — layout-shift entries after the tap.
//
// Run: bun tools/profile-open-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -45,7 +45,7 @@ console.log(` ${authors.length} people with a real bio, ${notes.length} of the
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5276, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
window.__shift = 0;
diff --git a/tools/profile-recovery-test.js b/tools/profile-recovery-test.js
index 2f6ae90..158a511 100644
--- a/tools/profile-recovery-test.js
+++ b/tools/profile-recovery-test.js
@@ -2,7 +2,7 @@
// and remain retryable. Tests real rendering with deterministic relay replies.
// Run: bun tools/profile-recovery-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
const entry = `
diff --git a/tools/public-feed-browser-test.js b/tools/public-feed-browser-test.js
index bfeb82b..59eeb3b 100644
--- a/tools/public-feed-browser-test.js
+++ b/tools/public-feed-browser-test.js
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { finalizeEvent, generateSecretKey } from 'nostr-tools/pure';
import { buildHtml, buildVerifyWorker } from '../build.js';
const sk = generateSecretKey(), now = Math.floor(Date.now() / 1000);
diff --git a/tools/puppeteer.js b/tools/puppeteer.js
new file mode 100644
index 0000000..4e0f121
--- /dev/null
+++ b/tools/puppeteer.js
@@ -0,0 +1,126 @@
+// puppeteer-core for the browser tests, with one difference: a page's
+// sockets to public relays never leave the machine. Throwaway test wallets
+// boot, subscribe and publish like real ones, and a suite run from one home
+// address gets that address banned (nos.lol, relay.damus.io, 2026-10-07).
+// So every socket to a host that isn't local is answered in the page by a
+// stand-in relay that has nothing stored: EVENT gets OK, REQ gets EOSE,
+// COUNT gets 0. Local relays (ws://127.0.0.1:…, a test's own) are untouched,
+// and a test's own WebSocket stubs still see every send, as before.
+//
+// A test that reads real content off the public relays launches with
+// { relays: 'read' }: its sockets are real, but nothing it publishes leaves
+// the page (each EVENT is answered OK in place). LIVE_RELAYS=read does the
+// same for any test. { relays: 'live' } (a test of a public service, like
+// the Cordn coordinator) or LIVE_RELAYS=1 turns the guard off, and
+// LIVE_RELAYS=relay.example.com,other.example lets just those hosts through.
+// puppeteer.connect (driving a real Chrome) is never guarded.
+//
+// Use: import puppeteer from './puppeteer.js'; (in place of 'puppeteer-core')
+import real from 'puppeteer-core';
+
+const live = (process.env.LIVE_RELAYS || '').trim();
+const allow = live && live !== '1' && live !== 'read' ? live.split(',').map((h) => h.trim()).filter(Boolean) : [];
+
+// Runs in the page before anything else.
+function relayGuard(allow, readOnly) {
+ if (window.__relayGuard) return;
+ window.__relayGuard = { blocked: [], sent: 0, events: [] }; // events: what didn't go out
+ const Real = window.WebSocket;
+ const nativeSend = Real.prototype.send;
+ const nativeClose = Real.prototype.close;
+ const fakes = new WeakSet();
+ const reading = new WeakSet(); // real public sockets in read mode
+ const local = (h) => h === 'localhost' || h === '[::1]' || h === '0.0.0.0' || /^127\./.test(h) || h.endsWith('.localhost');
+ const answer = (ws, msg) => setTimeout(() => {
+ if (ws.readyState !== 1) return;
+ ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(msg) }));
+ }, 5);
+ const relay = (ws, d) => {
+ let m;
+ try { m = JSON.parse(d); } catch { return; }
+ if (!Array.isArray(m)) return;
+ window.__relayGuard.sent++;
+ if (m[0] === 'EVENT' && m[1]?.id) { window.__relayGuard.events.push(m[1]); answer(ws, ['OK', m[1].id, true, '']); }
+ else if (m[0] === 'REQ') answer(ws, ['EOSE', m[1]]);
+ else if (m[0] === 'COUNT') answer(ws, ['COUNT', m[1], { count: 0 }]);
+ else if (m[0] === 'AUTH' && m[1]?.id) answer(ws, ['OK', m[1].id, true, '']);
+ };
+ // Every send goes through here, so a test that wraps
+ // WebSocket.prototype.send (before or after) still sees a stand-in's.
+ Real.prototype.send = function (d) {
+ if (fakes.has(this)) { if (this.readyState !== 1) throw new DOMException('not open', 'InvalidStateError'); return relay(this, d); }
+ if (reading.has(this) && typeof d === 'string' && /^\s*\[\s*"EVENT"/.test(d)) return relay(this, d);
+ return nativeSend.call(this, d);
+ };
+ Real.prototype.close = function (code, reason) {
+ if (!fakes.has(this)) return nativeClose.call(this, code, reason);
+ if (this.readyState >= 2) return;
+ this.readyState = 2;
+ setTimeout(() => { this.readyState = 3; this.dispatchEvent(new CloseEvent('close', { code: code || 1000, reason: reason || '', wasClean: true })); }, 0);
+ };
+ const handlerProp = (ws, type) => {
+ let fn = null;
+ const call = (e) => fn && fn.call(ws, e);
+ ws.addEventListener(type, call);
+ Object.defineProperty(ws, 'on' + type, { get: () => fn, set: (v) => { fn = typeof v === 'function' ? v : null; }, configurable: true });
+ };
+ function WebSocket(url, protocols) {
+ if (!new.target) throw new TypeError("Failed to construct 'WebSocket': Please use the 'new' operator");
+ let u;
+ try { u = new URL(url, location.href); } catch { return new Real(url, protocols); }
+ if (u.protocol !== 'wss:' && u.protocol !== 'ws:') return new Real(url, protocols);
+ if (local(u.hostname) || allow.includes(u.hostname)) return new Real(url, protocols);
+ if (readOnly) { const ws = new Real(url, protocols); reading.add(ws); return ws; }
+ window.__relayGuard.blocked.push(u.href);
+ // a genuine EventTarget (so dispatchEvent works) that passes instanceof
+ const ws = Object.setPrototypeOf(new EventTarget(), Real.prototype);
+ fakes.add(ws);
+ Object.defineProperties(ws, {
+ url: { value: u.href }, protocol: { value: '' }, extensions: { value: '' },
+ bufferedAmount: { value: 0 }, binaryType: { value: 'blob', writable: true },
+ readyState: { value: 0, writable: true },
+ });
+ for (const t of ['open', 'message', 'close', 'error']) handlerProp(ws, t);
+ setTimeout(() => { if (ws.readyState !== 0) return; ws.readyState = 1; ws.dispatchEvent(new Event('open')); }, 0);
+ return ws;
+ }
+ WebSocket.prototype = Real.prototype;
+ for (const k of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) WebSocket[k] = Real[k];
+ window.WebSocket = WebSocket;
+}
+
+const guarded = new WeakSet();
+const guardPage = async (page, ro) => {
+ if (!page || guarded.has(page)) return;
+ guarded.add(page);
+ await page.evaluateOnNewDocument(relayGuard, allow, ro).catch(() => {});
+};
+const guardContext = (ctx, ro) => {
+ const newPage = ctx.newPage.bind(ctx);
+ ctx.newPage = async (...a) => { const p = await newPage(...a); await guardPage(p, ro); return p; };
+};
+const guardBrowser = async (browser, ro) => {
+ guardContext(browser, ro);
+ for (const name of ['createBrowserContext', 'createIncognitoBrowserContext']) {
+ if (typeof browser[name] !== 'function') continue;
+ const make = browser[name].bind(browser);
+ browser[name] = async (...a) => { const c = await make(...a); guardContext(c, ro); return c; };
+ }
+ // pages it opened itself (popups, the first tab)
+ browser.on('targetcreated', async (t) => { if (t.type() === 'page') await guardPage(await t.page().catch(() => null), ro); });
+ for (const p of await browser.pages()) await guardPage(p, ro);
+};
+
+const launch = async ({ relays, ...opts } = {}) => {
+ const browser = await real.launch(opts);
+ if (live !== '1' && relays !== 'live') await guardBrowser(browser, live === 'read' || relays === 'read');
+ return browser;
+};
+
+export default new Proxy(real, {
+ get(t, k) {
+ if (k === 'launch') return launch;
+ const v = t[k];
+ return typeof v === 'function' ? v.bind(t) : v;
+ },
+});
diff --git a/tools/push-unavailable-test.js b/tools/push-unavailable-test.js
index 32fddff..6b754ea 100644
--- a/tools/push-unavailable-test.js
+++ b/tools/push-unavailable-test.js
@@ -5,7 +5,7 @@
// actually wrong and what works instead.
//
// Run: bun tools/push-unavailable-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/quote-card-test.js b/tools/quote-card-test.js
index 2cb9d41..3544c31 100644
--- a/tools/quote-card-test.js
+++ b/tools/quote-card-test.js
@@ -6,7 +6,7 @@
// fiatjaf's, chosen because it will still be there tomorrow.
//
// Run: bun tools/quote-card-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
@@ -29,7 +29,7 @@ const FAR = 'nevent1qvzqqqqqqypzqntcggz30qhq60ltqdx32zku9d46unhrkjtcv7fml7jx3dh4
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5269, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
// --slow: the relays miss the quoted note the first time it is asked for
// (ours was down for an evening once), so the row freezes with the fallback
diff --git a/tools/relays-settings-test.js b/tools/relays-settings-test.js
index df89388..9e18f59 100644
--- a/tools/relays-settings-test.js
+++ b/tools/relays-settings-test.js
@@ -3,7 +3,7 @@
// both only after a fresh lookup, newer than the copies it replaces.
// No relay is written for real — the WebSocket shim swallows EVENT.
// Run: bun tools/relays-settings-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { privateKeyFromSeedWords } from 'nostr-tools/nip06';
import { buildHtml } from '../build.js';
diff --git a/tools/reply-count-test.js b/tools/reply-count-test.js
index 659d629..10f03f6 100644
--- a/tools/reply-count-test.js
+++ b/tools/reply-count-test.js
@@ -1,7 +1,7 @@
// Each post's reply button carries the number of replies the relays know.
// Run: bun tools/reply-count-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateSecretKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
diff --git a/tools/room-faces-test.js b/tools/room-faces-test.js
index ab673ff..cc4c6fe 100644
--- a/tools/room-faces-test.js
+++ b/tools/room-faces-test.js
@@ -4,14 +4,14 @@
// profiles cached — the worst case — against the real coinos community.
//
// Run: bun tools/room-faces-test.js [--no-hover]
-import puppeteer from 'puppeteer-core';
+import puppeteer 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 html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5277, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t);
const waitText = async (x, ms = 25000) => { 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; };
diff --git a/tools/send-avatar-loading-test.js b/tools/send-avatar-loading-test.js
index daedb7c..b1022bf 100644
--- a/tools/send-avatar-loading-test.js
+++ b/tools/send-avatar-loading-test.js
@@ -1,7 +1,7 @@
// Real recipient portraits and profile fetches with controlled relay answers.
// Run: bun tools/send-avatar-loading-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const domHelper = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
diff --git a/tools/send-dest-test.js b/tools/send-dest-test.js
index 6dc331c..8403fc9 100644
--- a/tools/send-dest-test.js
+++ b/tools/send-dest-test.js
@@ -2,7 +2,7 @@
// (it used to be repeated in small type underneath), and the history list
// steps aside once you're on to the amount.
// Run: bun tools/send-dest-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { p2tr } from '@scure/btc-signer';
diff --git a/tools/send-lnaddr-advance-test.js b/tools/send-lnaddr-advance-test.js
index 5b79a47..fd6c1a1 100644
--- a/tools/send-lnaddr-advance-test.js
+++ b/tools/send-lnaddr-advance-test.js
@@ -3,7 +3,7 @@
// alone a moment — but only once it's real: a half-typed domain just waits,
// with no error. Uses the live registrar and DNS ([email protected]).
// Run: bun tools/send-lnaddr-advance-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/standalone-file-test.js b/tools/standalone-file-test.js
index 9254481..743bc7d 100644
--- a/tools/standalone-file-test.js
+++ b/tools/standalone-file-test.js
@@ -3,7 +3,7 @@
// Run: bun run build && bun tools/standalone-file-test.js
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
diff --git a/tools/sw-reload-test.js b/tools/sw-reload-test.js
index 7a41c7f..6ad4fed 100644
--- a/tools/sw-reload-test.js
+++ b/tools/sw-reload-test.js
@@ -8,7 +8,7 @@
// case where a reload is genuinely owed.
//
// Run: bun tools/sw-reload-test.js (needs a built dist/)
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
diff --git a/tools/sync-browser-test.js b/tools/sync-browser-test.js
index 05f8316..3de405f 100644
--- a/tools/sync-browser-test.js
+++ b/tools/sync-browser-test.js
@@ -2,7 +2,7 @@
// retry, and an already-connected second device adopting the missing change.
// Run: bun tools/sync-browser-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { verifyEvent } from 'nostr-tools/pure';
const build = await Bun.build({ entrypoints: ['./tools/sync-browser-harness.js'], target: 'browser', minify: true });
diff --git a/tools/thread-back-scroll-test.js b/tools/thread-back-scroll-test.js
index b1f63b3..55c8817 100644
--- a/tools/thread-back-scroll-test.js
+++ b/tools/thread-back-scroll-test.js
@@ -1,7 +1,7 @@
// The thread page's on-screen Back returns to the feed at the row the reader
// left, not the top. Run: bun tools/thread-back-scroll-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateSecretKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
diff --git a/tools/thread-reply-test.js b/tools/thread-reply-test.js
index 16b6f50..738a31c 100644
--- a/tools/thread-reply-test.js
+++ b/tools/thread-reply-test.js
@@ -1,7 +1,7 @@
// Real reply buttons/composer with controlled relay replies and no publishing.
// Run: bun tools/thread-reply-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
const entry = `
diff --git a/tools/topic-depth-test.js b/tools/topic-depth-test.js
index 9876224..d027e60 100644
--- a/tools/topic-depth-test.js
+++ b/tools/topic-depth-test.js
@@ -4,14 +4,14 @@
// Before: ~5 posts, then "You're all caught up".
//
// Run: bun tools/topic-depth-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer 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 html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5285, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
try {
await page.setViewport({ width: 390, height: 844 });
diff --git a/tools/ui-morph-test.js b/tools/ui-morph-test.js
index 343ccd2..ad685bc 100644
--- a/tools/ui-morph-test.js
+++ b/tools/ui-morph-test.js
@@ -3,7 +3,7 @@
// across background renders — the property the morph exists to provide.
// Usage: bun build.js && bun tools/ui-morph-test.js
import { spawn } from 'node:child_process';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const PORT = 5230;
const server = spawn('bun', ['--bun', 'x', 'serve', '-p', String(PORT), 'dist'], { stdio: 'ignore' });
diff --git a/tools/unifiedpush-test.js b/tools/unifiedpush-test.js
index 9bdb4cb..b5bcb6b 100644
--- a/tools/unifiedpush-test.js
+++ b/tools/unifiedpush-test.js
@@ -10,7 +10,7 @@
// have worked on such a phone.
//
// Run: bun tools/unifiedpush-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/unread-dot-test.js b/tools/unread-dot-test.js
index b98388a..3b43b7d 100644
--- a/tools/unread-dot-test.js
+++ b/tools/unread-dot-test.js
@@ -4,7 +4,7 @@
// seven times in the first seconds after a refresh, which reads as flashing.
//
// Run: bun tools/unread-dot-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
diff --git a/tools/upload-progress-test.js b/tools/upload-progress-test.js
index 25bf91c..8d4b24b 100644
--- a/tools/upload-progress-test.js
+++ b/tools/upload-progress-test.js
@@ -2,7 +2,7 @@
// upload events — fetch() can't), from 0 to 1, on a slow uplink.
//
// Run: bun tools/upload-progress-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
let ok = true;
const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
const js = await Bun.build({ entrypoints: ['src/media-upload.js'], target: 'browser' }).then((b) => b.outputs[0].text());
diff --git a/tools/video-autoplay-test.js b/tools/video-autoplay-test.js
index 93453ee..6b943f2 100644
--- a/tools/video-autoplay-test.js
+++ b/tools/video-autoplay-test.js
@@ -4,7 +4,7 @@
// loaded for a box that never comes into view. Set CLIP to a webm to serve;
// the default is made with ffmpeg on the fly.
// Run: bun tools/video-autoplay-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
diff --git a/tools/video-compress-test.js b/tools/video-compress-test.js
index c1175f8..19735c8 100644
--- a/tools/video-compress-test.js
+++ b/tools/video-compress-test.js
@@ -4,7 +4,7 @@
// upload is caught and its bytes checked with ffprobe.
//
// Run: bun tools/video-compress-test.js
-import puppeteer from 'puppeteer-core';
+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; };
diff --git a/tools/video-unmute-test.js b/tools/video-unmute-test.js
index fec3955..ac7cfc4 100644
--- a/tools/video-unmute-test.js
+++ b/tools/video-unmute-test.js
@@ -2,7 +2,7 @@
// Run: bun tools/video-unmute-test.js
// Phone: adb reverse tcp:5302 tcp:5302; BROWSER_URL=http://localhost:9223 bun tools/video-unmute-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const messages = await Bun.file('src/features/messages.js').text();
const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf("const root = document.getElementById('app');"));
diff --git a/tools/youtube-embed-test.js b/tools/youtube-embed-test.js
index 5d50b35..5133d1e 100644
--- a/tools/youtube-embed-test.js
+++ b/tools/youtube-embed-test.js
@@ -5,7 +5,7 @@
// decided to watch anything.
//
// Run: bun tools/youtube-embed-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { cacheKeyFor } from '../src/wallet.js';
diff --git a/tools/zap-deep-profile-test.js b/tools/zap-deep-profile-test.js
index 55c7b5d..3f0e72d 100644
--- a/tools/zap-deep-profile-test.js
+++ b/tools/zap-deep-profile-test.js
@@ -6,7 +6,7 @@
// at "not enough in Spending" — past the address lookup, which is the point).
//
// Run: bun tools/zap-deep-profile-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';
@@ -19,7 +19,7 @@ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? '
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5253, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
-const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const browser = await puppeteer.launch({ relays: 'read', executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.setViewport({ width: 420, height: 900 });
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);
diff --git a/tools/zap-feedback-test.js b/tools/zap-feedback-test.js
index 838aef4..3fdd3bc 100644
--- a/tools/zap-feedback-test.js
+++ b/tools/zap-feedback-test.js
@@ -2,7 +2,7 @@
// settlement. No wallet funds, relays, or payment services are contacted.
// Run: bun tools/zap-feedback-test.js
import assert from 'node:assert/strict';
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const domHelper = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
diff --git a/tools/zap-tally-cache-test.js b/tools/zap-tally-cache-test.js
index b1e37b6..e5eb8dd 100644
--- a/tools/zap-tally-cache-test.js
+++ b/tools/zap-tally-cache-test.js
@@ -4,7 +4,7 @@
// tally that changed (or a receipt that was deleted) still settles correctly.
//
// Run: bun tools/zap-tally-cache-test.js
-import puppeteer from 'puppeteer-core';
+import puppeteer from './puppeteer.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { buildHtml } from '../build.js';