From faad243486666eb4cbefcd7ff813329e707106dd Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Oct 2026 08:12:38 -0700 Subject: Feed tests: catch up with Articles, Popular as the default feed, timed feed defs and the tint that fades on sight - feed-media-views: the mode switch has an Articles button now - feed-media-cache: the live watch's since-only subscription isn't part of the crawl - feed-switch-seed: seeded feeds carry their edit time (the feeds sync merge drops one without) - feed-editor-relays: nos.lol is a local stand-in relay, so the test doesn't depend on reaching it - feed-streaming: the stub wallet opens on Following (no remembered feed = Popular); the held picture answers repeat fetches once released; the new-post tint is recorded as it paints (it fades on sight) Co-Authored-By: Claude Opus 5.5 --- tools/feed-editor-relays-test.js | 13 +++++++++---- tools/feed-media-cache-test.js | 5 +++-- tools/feed-media-views-test.js | 4 ++-- tools/feed-streaming-test.js | 24 +++++++++++++++++++----- tools/feed-switch-seed-test.js | 5 +++-- 5 files changed, 36 insertions(+), 15 deletions(-) (limited to 'tools') diff --git a/tools/feed-editor-relays-test.js b/tools/feed-editor-relays-test.js index 7e3edb5..5a808a7 100644 --- a/tools/feed-editor-relays-test.js +++ b/tools/feed-editor-relays-test.js @@ -10,16 +10,21 @@ 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 html = await buildHtml({ minify: true, pwa: false }); -const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +// nos.lol itself is a stand-in served here (an empty relay: EOSE to every +// REQ), so the test doesn't hang on whether the real one answers today +const server = Bun.serve({ port: 0, + fetch: (req, srv) => (new URL(req.url).pathname === '/relay' && srv.upgrade(req) ? undefined : new Response(html, { headers: { 'content-type': 'text/html' } })), + websocket: { message: (ws, data) => { try { const m = JSON.parse(data); if (m[0] === 'REQ') ws.send(JSON.stringify(['EOSE', m[1]])); } catch {} } } }); +const RELAY = 'ws://localhost:' + server.port + '/relay'; const browser = await puppeteer.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); const page = await browser.newPage(); const errs = []; page.on('pageerror', (e) => errs.push(String(e))); // record what the page asks which relay, without letting it publish anything -await page.evaluateOnNewDocument(() => { +await page.evaluateOnNewDocument((stand) => { window.__reqs = []; const Real = window.WebSocket; window.WebSocket = function (url, ...a) { - const ws = new Real(url, ...a); + const ws = new Real(/^wss:\/\/nos\.lol\/?$/.test(String(url)) ? stand : url, ...a); const send = ws.send.bind(ws); ws.send = (data) => { try { const m = JSON.parse(data); if (m[0] === 'REQ') window.__reqs.push({ url: String(url), filters: m.slice(2) }); if (m[0] === 'EVENT') return; } catch {} @@ -28,7 +33,7 @@ await page.evaluateOnNewDocument(() => { return ws; }; window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real); -}); +}, RELAY); const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); const text = () => page.evaluate(() => document.body.innerText); const waitFor = async (fn, ms = 15000) => { for (let i = 0; i < ms / 200; i++) { if (await page.evaluate(fn)) return true; await sleep(200); } return false; }; diff --git a/tools/feed-media-cache-test.js b/tools/feed-media-cache-test.js index fa90a15..46c485d 100644 --- a/tools/feed-media-cache-test.js +++ b/tools/feed-media-cache-test.js @@ -79,7 +79,7 @@ await page.evaluateOnNewDocument(([evs, pk]) => { const f = m[2] || {}; let out = []; if ((f.kinds || []).includes(1) && (f.authors || []).includes(pk) && !f['#e'] && !f.ids) { - window.__reqs.push({ until: f.until, limit: f.limit }); + window.__reqs.push({ until: f.until, limit: f.limit, since: f.since }); out = evs.filter((e) => !f.until || e.created_at <= f.until).slice(0, f.limit || 30); } setTimeout(() => { @@ -142,7 +142,8 @@ try { check('all six clips are found, the deepest 150 posts down', await waitFor(() => document.querySelectorAll('.media-grid .media-tile.video').length === 6, 15000), String(await tiles())); const took = Date.now() - t0; check('...without waiting on the slow picture along the way', took < 8000, took + ' ms'); - const reqs = await page.evaluate(() => window.__reqs); + // (the feed's live watch for new posts — a since-only subscription — isn't the crawl) + const reqs = await page.evaluate(() => window.__reqs.filter((r) => !(r.since && !r.until && !r.limit))); const steps = [...new Set(reqs.map((r) => r.until))]; // each step asks every relay check('...asking a hundred posts at a time, each step older than the last', reqs.length >= 1 && reqs.every((r) => r.limit === 100 && r.until) && steps.every((u, i) => !i || u < steps[i - 1]), JSON.stringify(steps)); diff --git a/tools/feed-media-views-test.js b/tools/feed-media-views-test.js index ccfd636..e29d346 100644 --- a/tools/feed-media-views-test.js +++ b/tools/feed-media-views-test.js @@ -126,8 +126,8 @@ try { await page.waitForSelector('.notes-feed:not([data-booting])', { timeout: 20000 }); console.log('\n[the switch]'); - check('All / Images / Videos sit under the chips, All on', await page.evaluate(() => - [...document.querySelectorAll('.media-mode')].map((b) => b.textContent + (b.classList.contains('on') ? '*' : '')).join(',') === 'All*,Images,Videos')); + check('All / Images / Videos / Articles sit under the chips, All on', await page.evaluate(() => + [...document.querySelectorAll('.media-mode')].map((b) => b.textContent + (b.classList.contains('on') ? '*' : '')).join(',') === 'All*,Images,Videos,Articles')); console.log('\n[Images: a grid]'); await mode('images'); diff --git a/tools/feed-streaming-test.js b/tools/feed-streaming-test.js index e662b42..3070771 100644 --- a/tools/feed-streaming-test.js +++ b/tools/feed-streaming-test.js @@ -51,6 +51,7 @@ const server = Bun.serve({ port: 0, fetch(req) { if (path.startsWith('/punks')) return new Response(png, { headers: { 'Content-Type': 'image/png' } }); return new Response(html, { headers: { 'Content-Type': 'text/html' } }); } }); +const heldPng = { status: 200, contentType: 'image/png', body: png }; const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] }); const errors = []; try { @@ -61,10 +62,23 @@ try { await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); await page.setRequestInterception(true); page.on('request', (r) => { - if (r.url() === 'https://media.example/held.png') { page.heldImage = r; return; } + if (r.url() === 'https://media.example/held.png') { + // held until the test lets it go; once it has, asking again (the + // picture is read more than once on its way in) is answered at once + if (page.released) r.respond(heldPng); else page.heldImage = r; + return; + } if (r.url().startsWith(server.url.origin) || r.url().startsWith('data:')) r.continue(); else r.abort(); }); await page.evaluateOnNewDocument(() => { + // the stub wallet's feed is Following (a wallet with none remembered opens on Popular) + try { localStorage.setItem('btc-wallet-feed:', 'following'); } catch {} + // rows that went on screen wearing the new-post tint (it fades the + // moment a row is seen, so the class alone can't be read afterwards) + window.freshSeen = new Set(); + new MutationObserver((ms) => { for (const m of ms) for (const n of m.type === 'attributes' ? [m.target] : m.addedNodes) + if (n.classList?.contains('note-fresh')) window.freshSeen.add(n.getAttribute('data-key')); }) + .observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); const sockets = []; window.feedClosed = 0; class FakeSocket extends EventTarget { @@ -125,7 +139,7 @@ try { assert.equal(await count(page), 2, 'out-of-order posts count and duplicates do not'); assert(Math.abs(await page.$eval(row(reading), (r) => r.getBoundingClientRect().top) - held) <= 1, 'streaming preserves reading position'); assert(page.heldImage, 'slow media is being prepared'); - await page.heldImage.respond({ status: 200, contentType: 'image/png', body: png }); + page.released = true; await page.heldImage.respond(heldPng); await page.waitForSelector(row(slow.id), { timeout: 2000 }); assert.equal(await count(page), 3); assert(await page.$eval(row(slow.id), (r) => { const img = r.querySelector('.note-img'); return img?.complete && img.naturalWidth > 0; }), 'photo is decoded when admitted'); @@ -173,9 +187,9 @@ try { assert(await page.evaluate(() => !window.bootDone), 'new-post notice does not wait for cached media'); await page.click('.feed-new-pill'); await page.waitForSelector(row(fast.id)); - assert(await page.$eval(row(fast.id), (r) => r.classList.contains('note-fresh')), 'prepared new rows keep their stable presentation during cache warm-up'); + assert(await page.evaluate((id) => window.freshSeen.has(id), fast.id), 'prepared new rows keep their stable presentation during cache warm-up'); assert(page.heldImage); - await page.heldImage.respond({ status: 200, contentType: 'image/png', body: png }); + page.released = true; await page.heldImage.respond(heldPng); await page.waitForFunction(() => window.bootDone); await page.evaluate(() => window.endQueries()); await page.waitForFunction(() => window.refreshDone); @@ -192,7 +206,7 @@ try { await page.click('.feed-new-pill'); await page.waitForSelector(row(fast.id)); assert(page.heldImage); - await page.heldImage.respond({ status: 200, contentType: 'image/png', body: png }); + page.released = true; await page.heldImage.respond(heldPng); await page.waitForFunction(() => window.batchDone); await page.evaluate(() => window.endQueries()); await page.waitForFunction(() => window.refreshDone); diff --git a/tools/feed-switch-seed-test.js b/tools/feed-switch-seed-test.js index e747744..1bad161 100644 --- a/tools/feed-switch-seed-test.js +++ b/tools/feed-switch-seed-test.js @@ -81,8 +81,9 @@ try { localStorage.setItem(k + ':feedNotes', JSON.stringify(ns)); localStorage.setItem(k + ':profiles', JSON.stringify({ [a]: { name: 'Alice', t }, [b]: { name: 'Bob', t } })); const s = JSON.parse(localStorage.getItem(k + ':messages') || '{}'); - s.feeds = [{ id: 'bob', name: 'Bobfeed', follows: false, authors: [b], topics: [], packs: [] }, - { id: 'btc', name: 'Btcfeed', follows: false, authors: [], topics: ['bitcoin'], packs: [] }]; + // (a feed carries its edit time, as the editor writes it: the feeds sync merge drops one without) + s.feeds = [{ id: 'bob', name: 'Bobfeed', follows: false, authors: [b], topics: [], packs: [], at: t }, + { id: 'btc', name: 'Btcfeed', follows: false, authors: [], topics: ['bitcoin'], packs: [], at: t }]; localStorage.setItem(k + ':messages', JSON.stringify(s)); localStorage.setItem('btc-wallet-feed:' + k, 'following'); // this wallet's last feed }, [base, PK_A, PK_B, alice]); -- cgit v1.2.3