// 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);