aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-14 22:03:23 -0700
committerAdam Soltys <[email protected]>2026-11-14 22:03:23 -0700
commit933e85020c2cc56b511ff47a73943c92ea1d044a (patch)
treee12f9ffd835cd9c1fc9bffa81b76f910e194be80 /tools
parent065d1b96afa32781f1a0805e0b4a02d32bf883ee (diff)
downloadcoinosv3-933e85020c2cc56b511ff47a73943c92ea1d044a.tar.gz
coinosv3-933e85020c2cc56b511ff47a73943c92ea1d044a.zip
Search inside someone's posts: their profile puts them in the search field, and "franca wildwood" is understood. "i found him by searching franca but then i was wanting some way to search his feed for something … i was wanting to search 'franca wildwood' but knew it wouldn't work … we couldn't just have a clever way of guessing what the user is trying to do?" (Adam, 2026-11-14). No second field: on a profile the header field (and the phone's search page opened from a profile) carries the person as a chip, "Search their posts, or anyone…"; Enter searches only their posts, people still show below for picking someone else, and × or Backspace on the empty field searches everywhere again. Words that begin or end with someone's name (first/last one or two words, matched at the start of a word of their name or address) are offered as "“wildwood” in Franca Yibisi's posts" at the top of the results; when that's someone you follow or whose profile you opened this session, it's the Enter choice (↵), otherwise only a row ("bitcoin wallet" is not Bitcoin Magazine's posts), and never when the whole query is someone's name. The search is a feed like any text search, /feed?q=wildwood&p=npub…: the NIP-50 relays asked with the author, plus the author's own recent posts (home, archive and their own relays, up to four pages of 400 until ten match) checked here word by word, since few people are in a search index; what their profile already loaded shows at once. tools/search-scope-test.js (real relays): chip on fiatjaf's profile, "relay" → only his posts, every one saying it; "fiatjaf relay" guessed and taken by Enter; × and Backspace; phone width. profile-stream, post-search, search-avatar-loading and features-smoke pass
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMan6UKGJyuGyZFxddYRxS
Diffstat (limited to 'tools')
-rw-r--r--tools/profile-stream-test.js8
-rw-r--r--tools/search-scope-test.js96
2 files changed, 100 insertions, 4 deletions
diff --git a/tools/profile-stream-test.js b/tools/profile-stream-test.js
index b7dfb9b..2b426bb 100644
--- a/tools/profile-stream-test.js
+++ b/tools/profile-stream-test.js
@@ -27,7 +27,7 @@ try {
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)));
+ const rows = () => page.evaluate(() => [...document.querySelectorAll('.list .item:not(.search-scoped-row)')].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)));
@@ -35,7 +35,7 @@ try {
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());
+ await page.evaluate(() => [...document.querySelectorAll('.list .item:not(.search-scoped-row)')].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(() => ({
@@ -57,10 +57,10 @@ try {
// 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');
+ const field = await page.$('input.header-search, .header-search-field');
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 page.evaluate(() => { const f = document.querySelector('input.header-search, .header-search-field'); 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); }
diff --git a/tools/search-scope-test.js b/tools/search-scope-test.js
new file mode 100644
index 0000000..122fcd0
--- /dev/null
+++ b/tools/search-scope-test.js
@@ -0,0 +1,96 @@
+// Searching inside one person's posts (Adam, 2026-11-14: "i found him by
+// searching franca but then i was wanting some way to search his feed for
+// something … i was wanting to search 'franca wildwood'"). On a profile the
+// header field carries the person as a chip and Enter searches their posts;
+// words that begin with the name of someone whose profile you opened
+// ("fiatjaf relay") are offered, and Enter takes them there. Reads real
+// relays (relays: 'read'; nothing is published).
+// Run: bun tools/search-scope-test.js (WHO=npub… NAME=fiatjaf WORD=relay)
+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 WHO = process.env.WHO || 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6'; // fiatjaf
+const NAME = process.env.NAME || 'fiatjaf';
+const WORD = process.env.WORD || 'relay';
+
+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)));
+const until = async (fn, ms = 15000, arg) => { for (let i = 0; i < ms / 200; i++) { const v = await page.evaluate(fn, arg); if (v) return v; await sleep(200); } return null; };
+const feedState = () => page.evaluate((word) => {
+ const posts = [...document.querySelectorAll('.notes-feed:not(.note-ghosts) > [data-key]')].filter((p) => !/^(hr:|win-)/.test(p.getAttribute('data-key')));
+ return { path: location.pathname + location.search, title: document.querySelector('.feed-page h2, .feed-title, .feed-name')?.textContent || '',
+ n: posts.length, without: posts.filter((p) => !p.textContent.toLowerCase().includes(word)).length };
+}, WORD);
+try {
+ await page.setViewport({ width: 1300, height: 900 });
+ await page.goto(server.url.href + WHO, { waitUntil: 'domcontentloaded' });
+ check('the profile opens', !!(await until(() => document.querySelector('.prof-tabs'))));
+ const chip = await until((n) => { const c = document.querySelector('.header-search-scoped .search-scope-chip')?.textContent || ''; return c.toLowerCase().includes(n) && c; }, 15000, NAME.toLowerCase());
+ check('the header field carries the person as a chip', !!chip && chip.toLowerCase().includes(NAME.toLowerCase()), chip || '');
+ if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT.replace(/(\.png)?$/, '-chip.png'), clip: { x: 0, y: 0, width: 1300, height: 120 } });
+
+ await page.click('.header-search-field');
+ await page.type('.header-search-field', WORD, { delay: 50 });
+ const row = await until(() => document.querySelector('.search-scoped-row')?.textContent);
+ check('the results offer the words in their posts, first', !!row && row.includes(WORD) && /posts/.test(row), row || '');
+ const people = await until(() => document.querySelectorAll('.list .item:not(.search-scoped-row)').length > 2);
+ check('…and still find people', !!people);
+ await page.keyboard.press('Enter');
+ const f1 = await until(async () => document.querySelectorAll('.notes-feed:not(.note-ghosts) > [data-key]').length >= 1 && location.search.includes('p=npub'), 25000);
+ const s1 = await feedState();
+ check('Enter searches only their posts (URL carries the person)', !!f1 && s1.path.includes('q=' + WORD) && s1.path.includes('p=npub'), JSON.stringify(s1));
+ check('…every post says the word', s1.n > 0 && s1.without === 0, `${s1.n} posts, ${s1.without} without "${WORD}"`);
+ const authors = await page.evaluate(() => [...new Set([...document.querySelectorAll('.notes-feed:not(.note-ghosts) > [data-key] .note-author, .notes-feed:not(.note-ghosts) > [data-key] .chat-name')].map((n) => n.textContent.trim()))].slice(0, 5));
+ console.log(' authors seen:', authors.join(', '));
+ if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT.replace(/(\.png)?$/, '-feed.png') });
+
+ // the guess: "<name> <word>" from scratch, the profile having been opened
+ await page.goto(server.url.href + 'feed', { waitUntil: 'domcontentloaded' });
+ await sleep(1500);
+ await page.evaluate((pk) => { /* nothing: a reload forgets the session's profiles */ }, WHO);
+ check('no chip away from a profile', !(await page.$('.search-scope-chip')));
+ // open the profile in this session, then go back and type the combined query
+ await page.goto(server.url.href + WHO, { waitUntil: 'domcontentloaded' });
+ await until(() => document.querySelector('.prof-tabs'));
+ await page.click('.search-scope-x');
+ check('× takes the chip out', !!(await until(() => !document.querySelector('.search-scope-chip'))));
+ await page.click('input.header-search');
+ await page.type('input.header-search', `${NAME} ${WORD}`, { delay: 50 });
+ const guess = await until(() => document.querySelector('.search-scoped-row')?.textContent, 15000);
+ check('"name word" is offered as their posts', !!guess && guess.includes(`“${WORD}”`) && guess.toLowerCase().includes(NAME.toLowerCase()), guess || '');
+ check('…as the strong guess (Enter hint)', !!(await page.$('.search-scoped-row .search-enter-hint')));
+ await page.keyboard.press('Enter');
+ const f2 = await until(() => location.search.includes('p=npub') && location.search.includes('q=' + encodeURIComponent('relay').slice(0, 3)), 15000);
+ check('Enter takes the guess', !!f2, (await feedState()).path);
+
+ // a query that is someone's whole name is not split
+ await page.click('input.header-search, .header-search-field');
+ await page.evaluate(() => { const f = document.querySelector('input.header-search, .header-search-field'); f.value = ''; });
+ await page.type('input.header-search, .header-search-field', 'nobody wordthatmatchesnoone', { delay: 30 });
+ await sleep(3000);
+ check('no guess when no name starts the words', !(await page.$('.search-scoped-row')));
+ // a phone: the magnifier on a profile opens search with the chip in it
+ await page.setViewport({ width: 390, height: 844 });
+ await page.goto(server.url.href + WHO, { waitUntil: 'domcontentloaded' });
+ await until(() => document.querySelector('.prof-tabs'));
+ await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => b.getAttribute('aria-label') === 'Search' && b.offsetParent)?.click());
+ const mchip = await until(() => document.querySelector('.user-search-main .search-scope-chip')?.textContent);
+ check('phone: search from a profile carries the chip', !!mchip, mchip || '');
+ await page.type('.user-search-input', WORD, { delay: 40 });
+ check('phone: …and offers their posts', !!(await until(() => document.querySelector('.search-scoped-row'))));
+ await page.click('.user-search-input');
+ await page.keyboard.press('End');
+ for (let i = 0; i < WORD.length; i++) await page.keyboard.press('Backspace');
+ await page.keyboard.press('Backspace');
+ check('phone: Backspace on the empty field takes the chip out', !!(await until(() => !document.querySelector('.search-scope-chip'))));
+ if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT.replace(/(\.png)?$/, '-phone.png') });
+ 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);