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
|
// A profile visited for the first time, from search: its posts used to land
// all at once after several seconds of blank (the slowest relay, then every
// post's media). Now the page shows post-shaped placeholders while it waits,
// the first posts paint as the quick relays answer, and the top search
// results are fetched before they're tapped. Reads real relays
// (relays: 'read'; nothing is published).
// Run: bun tools/profile-stream-test.js (Q="ben arc" to search someone else)
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 Q = process.env.Q || 'fiatjaf';
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
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(String(e.stack || e).slice(0, 600)));
try {
await page.setViewport({ width: 390, height: 844 });
if (process.env.DARK) await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]);
await page.goto(server.url.href + 'feed', { waitUntil: 'domcontentloaded' });
await sleep(1500);
await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.getAttribute('aria-label') === 'Search')?.click());
await page.waitForSelector('.user-search-main', { timeout: 10000 });
await page.type('.user-search-main', Q, { delay: 60 });
// the result rows: people (not the "posts that say this" or #topic rows)
const rows = () => page.evaluate(() => [...document.querySelectorAll('.list .item')].filter((r) => !/^[\u{1F50D}#]/u.test(r.textContent.trim())).map((r) => r.textContent.trim().slice(0, 40)));
let found = [];
for (let i = 0; i < 60 && !found.length; i++) { found = await rows(); if (!found.length) await sleep(250); }
if (!found.length) console.log(await page.evaluate(() => document.querySelector('.user-search-main')?.closest('.col')?.parentElement?.innerHTML.slice(0, 1500)));
check('search finds people', found.length > 0, found.slice(0, 2).join(' | '));
const wait = Number(process.env.WAIT || 0); // ms between results and the tap (prefetch's head start)
if (wait) await sleep(wait);
const t0 = Date.now();
await page.evaluate(() => [...document.querySelectorAll('.list .item')].filter((r) => !/^[\u{1F50D}#]/u.test(r.textContent.trim()))[0].click());
let ghostAt = 0, firstAt = 0, rowsAt3 = 0;
for (let i = 0; i < 200; i++) {
const s = await page.evaluate(() => ({
ghost: !!document.querySelector('.note-ghosts'),
rows: document.querySelectorAll('.notes-feed:not(.note-ghosts) > [data-key]').length,
}));
const t = Date.now() - t0;
if (s.ghost && !ghostAt) { ghostAt = t; if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT }); }
if (s.rows && !firstAt) firstAt = t;
if (t >= 3000 && !rowsAt3) rowsAt3 = s.rows || -1;
if (firstAt && t >= 3000) break;
await sleep(50);
}
console.log(` placeholder ${ghostAt ? ghostAt + ' ms' : 'not seen'}, first post ${firstAt || '—'} ms, rows at 3 s ${rowsAt3}`);
check('posts arrive', firstAt > 0);
check('...while waiting, the page says posts are loading', !!ghostAt || firstAt < 300, ghostAt ? 'placeholder shown' : 'posts were already in hand');
// a second search typed into the desktop header field while that profile
// is open: the results come forward ("the search box stops working",
// Adam 2026-11-14 — they updated a page hidden under the profile)
await page.setViewport({ width: 1300, height: 900 });
await sleep(500);
const field = await page.$('.header-search');
check('the desktop header has a search field', !!field);
if (field) {
await page.evaluate(() => { const f = document.querySelector('.header-search'); f.focus(); f.value = ''; });
await field.type('jack', { delay: 60 });
let again = [];
for (let i = 0; i < 80 && !(again.length && again[0] !== found[0]); i++) { again = await rows(); if (!(again.length && again[0] !== found[0])) await sleep(250); }
const onProfile = await page.evaluate(() => !!document.querySelector('.prof-tabs'));
check('typing a new search from a profile shows its results', again.length > 0 && !onProfile, again.slice(0, 2).join(' | '));
await page.goBack(); await sleep(800);
check('...and Back returns to the profile', await page.evaluate(() => !!document.querySelector('.prof-tabs')));
}
check('no page errors', !errs.length, errs.join(' | '));
} catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(); }
console.log(ok ? '\nPASS' : '\nFAIL');
process.exit(ok ? 0 : 1);
|