aboutsummaryrefslogtreecommitdiffstats
path: root/tools/feed-live-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-23 22:51:58 -0700
committerAdam Soltys <[email protected]>2026-09-23 22:51:58 -0700
commit75679fdd5d24d36e3c54316e88ccefcf954afc82 (patch)
treec86a369455da3c2e9532d51d2e72898afda8fccc /tools/feed-live-test.js
parentfb170b4783c9b49c73e926c08aba9bc4d1bb418e (diff)
downloadcoinosv3-75679fdd5d24d36e3c54316e88ccefcf954afc82.tar.gz
coinosv3-75679fdd5d24d36e3c54316e88ccefcf954afc82.zip
Feed: threads cached locally, posts never inserted under the reader, rows keep their first look.
Work by Codex, wrapped up and verified here. Threads: a viewed conversation is persisted (thread-cache.js, up to 12 threads / 160 events), so a refresh or a cached note link shows the root and ancestors at once instead of the selected reply and a spinner; relays are re-asked in the background and an empty answer keeps the cache. Feed window (feed-window.js): arrivals are merged without inserting between visible rows — a gap fill under the reader's eyes waits until that part of the feed is offscreen, prepends anchor the reading position, the notice counts what went in above, and only its tap goes to the top. Older pages are prepared a few screens ahead; a page's pictures and faces are decoded before it is shown. Rows keep the presentation they were admitted with (feedPaint): background profile and quote refreshes no longer rewrite text or swap faces mid-read; media dimensions are known before paint. Thumbnails queue instead of being dropped past the first three. Tests: thread-cache, feed-window, feed-stability; feed-live updated for the new arrival rules (the notice counts up while you read). Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools/feed-live-test.js')
-rw-r--r--tools/feed-live-test.js57
1 files changed, 38 insertions, 19 deletions
diff --git a/tools/feed-live-test.js b/tools/feed-live-test.js
index c4807f1..7732045 100644
--- a/tools/feed-live-test.js
+++ b/tools/feed-live-test.js
@@ -6,8 +6,8 @@
// catches up by itself now.
//
// And a post that lands while you are reading halfway down must not shove the
-// page under your thumb. It waits behind a pill that says how many, and the
-// tap that shows them takes you up to them.
+// page under your thumb. Prepared posts arrive above the viewport, and the
+// notice takes you up to them when tapped.
//
// Run: bun tools/feed-live-test.js
import puppeteer from 'puppeteer-core';
@@ -33,7 +33,9 @@ const html = await buildHtml({ minify: true, pwa: false });
// a picture that takes its time, so a post showing it must have waited
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64');
const server = Bun.serve({ port: 5296, fetch: async (req) => {
- if (new URL(req.url).pathname === '/slow.png') {
+ const path = new URL(req.url).pathname;
+ if (path.startsWith('/punks')) return new Response(Bun.file('dist' + path));
+ if (path === '/slow.png') {
await sleep(700);
return new Response(PNG, { headers: { 'content-type': 'image/png', 'cache-control': 'no-store' } });
}
@@ -102,6 +104,7 @@ try {
await page.evaluate(([k, pk, ns]) => {
localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) }));
localStorage.setItem(k + ':feedNotes', JSON.stringify(ns));
+ localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Test Author', t: Date.now() } }));
}, [base, AUTHOR, seeded]);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitText('receive', 20000);
@@ -126,8 +129,7 @@ try {
const delivered = await page.evaluate((ev) => window.__inject(ev), fresh);
check('...a post really arrives on a live subscription', delivered > 0, delivered + ' socket(s)');
// A post waits at the door for its author's face and its pictures (or the
- // two-second deadline, for a face nobody has) before it goes in — so give
- // it that long, and no longer.
+ // loading deadline) before it goes in. This author is cached locally.
await sleep(2600);
const after = await page.evaluate((text) => {
const r = [...document.querySelectorAll('.notes-feed > .row')].find((n) => n.innerText.includes(text));
@@ -157,10 +159,13 @@ try {
await sleep(900);
check('...that reaching the top clears by itself', !(await page.$('.feed-new-pill')));
- console.log('\n[a post arrives whole, at the top]');
- // At the top of the feed a live post opens itself — no pill. It waits at
- // the door for its picture first, and the first time the row exists its
- // image is already decoded. A row used to paint first and fill in after.
+ console.log('\n[a post arrives whole, above the viewport]');
+ // Move the first post to the viewport edge so a prepend is offscreen.
+ await page.evaluate(() => {
+ const first = document.querySelector('.notes-feed > .row');
+ scrollBy(0, Math.ceil(first.getBoundingClientRect().top));
+ });
+ // The image is decoded before the row is admitted, with no height animation.
await page.evaluate(() => {
window.__firstSight = null;
const feed = document.querySelector('.notes-feed');
@@ -180,11 +185,11 @@ try {
});
const t0 = await page.evaluate(() => performance.now());
await page.evaluate((ev) => window.__inject(ev), signed('PICTURE POST http://localhost:5296/slow.png?' + Date.now(), 0));
- check('...sitting at the top', (await page.evaluate(() => window.scrollY)) < 120);
+ const heldPictureAnchor = await page.evaluate(() => { const r = document.querySelector('.notes-feed > .row'); return { id: r.dataset.key, top: r.getBoundingClientRect().top }; });
await page.waitForFunction(() => !!window.__firstSight, { timeout: 4000 }).catch(() => {});
- await sleep(120); // a hair in: the animation has been started and not finished
+ await sleep(120);
const sight = await page.evaluate(() => window.__firstSight);
- check('the post let itself in — no pill', !!sight && sight.img && !(await page.$('.feed-new-pill')), JSON.stringify(sight));
+ check('the prepared post arrived above the reader', !!sight && sight.img && !!(await page.$('.feed-new-pill')), JSON.stringify(sight));
check('...having waited for the slow host', !!sight && sight.at - t0 >= 700, sight ? Math.round(sight.at - t0) + 'ms' : '');
const mid = await page.evaluate(() => {
const row = [...document.querySelectorAll('.notes-feed > .row')].find((n) => /PICTURE POST/.test(n.innerText || ''));
@@ -195,8 +200,12 @@ try {
const row = [...document.querySelectorAll('.notes-feed > .row')].find((n) => /PICTURE POST/.test(n.innerText || ''));
return row ? { anims: row.getAnimations().length, h: row.getBoundingClientRect().height, overflow: row.style.overflow } : null;
});
- check('...and it opened rather than appeared', !!mid && !!settled && mid.anims > 0 && mid.h < settled.h
+ check('...without animating its height', !!mid && !!settled && mid.anims === 0 && mid.h === settled.h
&& settled.anims === 0 && settled.h > 0 && !settled.overflow, JSON.stringify({ mid, settled }));
+ check('...and the reading position is preserved', await page.evaluate(({ id, top }) => {
+ const row = document.querySelector('.notes-feed > [data-key="' + id + '"]');
+ return row && Math.abs(row.getBoundingClientRect().top - top) <= 1;
+ }, heldPictureAnchor));
// decoded before the row is inserted; the fresh <img> node reports it a
// task later, so the row's first frames are what is checked here
check('...and its picture was already decoded when the row appeared', !!sight && (sight.ready || sight.readyMs <= 60), JSON.stringify(sight));
@@ -235,13 +244,16 @@ try {
await page.evaluate(() => window.scrollTo(0, 900));
await sleep(500);
const held = await anchor();
+ // the notice keeps counting while the reader stays put: what matters is that it grew by three
+ const pillN = () => page.evaluate(() => parseInt((document.querySelector('.feed-new-pill')?.textContent || '0').replace(/\D/g, ''), 10) || 0);
+ const pillBefore = await pillN();
const r1 = await away(() => [signed('CATCH-UP 1', 0), signed('CATCH-UP 2', 1), signed('CATCH-UP 3', 2),
signed('an old post that was not in the cache', 5000), signed('another old one', 5100)]);
check('coming back re-asks the relays without being told', r1.reqsAfter > r1.reqsBefore, `${r1.reqsBefore} → ${r1.reqsAfter} kind-1 requests`);
const a1 = await rowAt(held.text);
const feedText = () => page.evaluate(() => document.querySelector('.notes-feed')?.innerText || '');
check('a few new posts go straight in', /CATCH-UP 1/.test(await feedText()));
- check('...and the pill says how many are above you', /3 new posts/i.test(await page.evaluate(() => document.querySelector('.feed-new-pill')?.textContent || '')));
+ check('...and the pill says how many more are above you', (await pillN()) === pillBefore + 3, pillBefore + ' → ' + (await pillN()));
check('...and the post you were reading has not moved', !!held && !!a1 && a1.text === held.text && Math.abs(a1.top - held.top) <= 2,
JSON.stringify({ held, now: a1 }));
// they sort below everything cached: page down to them
@@ -256,19 +268,26 @@ try {
const wasTop = await anchor();
await away(() => [signed('AT-THE-TOP A', 0), signed('AT-THE-TOP B', 1)]);
const a2 = await rowAt(wasTop.text);
- check('the post you had at the top stays under your eyes', !!wasTop && !!a2 && a2.text === wasTop.text && Math.abs(a2.top - wasTop.top) <= 2 && a2.y > 0,
+ check('the header and first post stay in place', !!wasTop && !!a2 && a2.text === wasTop.text && Math.abs(a2.top - wasTop.top) <= 2 && a2.y === wasTop.y,
JSON.stringify({ was: wasTop, now: a2 }));
- check('...with the new ones above it, newest first, and a pill saying so', /AT-THE-TOP A/.test(await topRow()) && /2 new posts/i.test(await page.evaluate(() => document.querySelector('.feed-new-pill')?.textContent || '')));
+ check('...new posts wait while the header is visible', !/AT-THE-TOP A/.test(await topRow()));
+ await page.evaluate(() => {
+ const first = document.querySelector('.notes-feed > .row');
+ scrollBy(0, Math.ceil(first.getBoundingClientRect().top));
+ });
+ await sleep(600);
+ check('...then arrive offscreen above it, with a notice', /AT-THE-TOP A/.test(await topRow()) && /2 new posts/i.test(await page.evaluate(() => document.querySelector('.feed-new-pill')?.textContent || '')));
console.log('\n[coming back after a long time away]');
await page.evaluate(() => window.scrollTo(0, 900));
await sleep(400);
+ const heldLong = await anchor();
// all in the same second — a post per second back would reach past the
// top of the page after the thirteen seconds the two waits above took
await away(() => Array.from({ length: 25 }, (_, i) => signed('LONG AWAY ' + (i + 1), 0)));
- const a3 = await anchor();
- check('many new posts: the feed goes to the top', !!a3 && a3.y < 120, JSON.stringify(a3));
- check('...at the newest post, with no pill', /LONG AWAY 1\b/.test(await topRow()) && !(await page.$('.feed-new-pill')), (await topRow()).slice(0, 40));
+ const a3 = await rowAt(heldLong.text);
+ check('many new posts still preserve the reading position', !!a3 && Math.abs(a3.top - heldLong.top) <= 2, JSON.stringify({ heldLong, a3 }));
+ check('...with the newest posts above and a notice', /LONG AWAY 1\b/.test(await topRow()) && !!(await page.$('.feed-new-pill')), (await topRow()).slice(0, 40));
check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} finally { await browser.close(); server.stop(true); }