// Recipient-search suggestions: a registrar hit lands before its profile, and // its avatar must stay a quiet circle until the profile lookup settles — no // pubkey punk flashing in front of the picture the account actually chose. // Run: bun tools/search-avatar-loading-test.js import assert from 'node:assert/strict'; 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')); const entry = ` import { searchRecipients, resultRows } from './src/recipient-search.js'; ${domHelper} const lookups = new Map(), frames = []; // kind-0 fetches answer when the test says so; full-text search finds nothing window.searchLookup = (filter) => { if (filter.search || !filter.authors) return Promise.resolve([]); return Promise.all(filter.authors.map((pk) => new Promise((resolve) => lookups.set(pk, resolve)))).then((a) => a.flat()); }; window.fetch = async (url) => { const q = new URL(url).searchParams.get('q'); return { json: async () => ({ results: [{ pubkey: window.registrar[q], name: q, address: q + '@coinos.io' }] }) }; }; const paint = (rows) => { const el = document.querySelector('#rows'); el.replaceChildren(...resultRows(h, rows, () => {})); const a = el.querySelector('.chat-avatar'); frames.push({ punk: !!a.querySelector('.punk'), loading: a.classList.contains('loading'), picture: a.style.backgroundImage }); }; window.test = { search(q, pk) { window.registrar = { [q]: pk }; frames.length = 0; return searchRecipients(q, paint).then(paint); }, answer(pk, profile) { lookups.get(pk)(profile ? [{ pubkey: pk, created_at: 1, kind: 0, content: JSON.stringify(profile) }] : []); }, pending: (pk) => lookups.has(pk), get frames() { return frames; }, }; `; const bundle = await Bun.build({ entrypoints: ['search-test-entry'], target: 'browser', plugins: [{ name: 'search-test', setup(build) { build.onResolve({ filter: /^search-test-entry$/ }, () => ({ path: 'entry', namespace: 'search-test' })); build.onLoad({ filter: /.*/, namespace: 'search-test' }, () => ({ contents: entry, loader: 'js', resolveDir: process.cwd() })); build.onLoad({ filter: /src\/recipient-search\.js$/ }, async ({ path }) => ({ loader: 'js', // Primal's socket stays out of it: the registrar and kind-0 fetch are the race contents: (await Bun.file(path).text()).replace('const primalSearch = (q', 'const primalSearch = () => Promise.resolve([]);\nconst _primalSearch = (q'), })); build.onLoad({ filter: /src\/nostr\.js$/ }, async ({ path }) => ({ loader: 'js', contents: (await Bun.file(path).text()).replace('export async function queryOn(relays, filter, maxWait = 1500) {', 'export async function queryOn(relays, filter, maxWait = 1500) { return window.searchLookup(filter);'), })); } }] }); assert(bundle.success, bundle.logs.join('\n')); const html = `
`; const server = Bun.serve({ port: 0, fetch(req) { const path = new URL(req.url).pathname; if (/^\/punks(-sm)?\/\d+\.webp$/.test(path)) return new Response(Bun.file('static' + path)); return path === '/' ? 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'] }); try { const page = await browser.newPage(), errors = []; page.on('pageerror', (e) => errors.push(e.message)); await page.setRequestInterception(true); page.on('request', (r) => r.url().startsWith(server.url.origin) ? r.continue() : r.abort()); await page.goto(server.url.href); await page.waitForFunction(() => window.test); // a new account that picked a punk other than its pubkey's default const pk = 'a'.repeat(64); const done = page.evaluate((pk) => test.search('newbie', pk), pk); await page.waitForFunction((pk) => test.pending(pk) && test.frames.length, {}, pk); assert.deepEqual(await page.evaluate(() => test.frames.at(-1)), { punk: false, loading: true, picture: '' }, 'registrar hit paints a quiet circle while its profile loads'); await page.evaluate((pk) => test.answer(pk, { name: 'newbie', picture: '/punks/18.webp' }), pk); await done; const frames = await page.evaluate(() => test.frames); assert(frames.at(-1).picture.includes('/punks/18.webp'), 'the chosen picture lands'); assert(frames.every((f) => !f.punk), 'no default punk before the real picture'); console.log('✓ A new account\'s suggestion shows no default punk before its real picture'); // no profile at all: once the lookup settles, the default punk is the right answer const bare = 'b'.repeat(64); const done2 = page.evaluate((pk) => test.search('bare', pk), bare); await page.waitForFunction((pk) => test.pending(pk), {}, bare); await page.evaluate((pk) => test.answer(pk, null), bare); await done2; assert.equal((await page.evaluate(() => test.frames.at(-1))).punk, true, 'settled lookup without a picture shows the punk'); console.log('✓ Accounts without a picture settle into their default punk'); assert.deepEqual(errors, []); } finally { await browser.close(); server.stop(true); }