1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
// 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 = `<!doctype html><meta charset="utf-8"><style>${await Bun.file('src/style.css').text()}</style><div id="rows" class="list"></div><script type="module">${await bundle.outputs[0].text()}</script>`;
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); }
|