diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/profile-stream-test.js | 8 | ||||
| -rw-r--r-- | tools/search-scope-test.js | 96 |
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); |
