aboutsummaryrefslogtreecommitdiffstats
path: root/tools/feed-window-coast-test.js
blob: 7c7cead29eb89cbb8b6265f7a3f7c0574c38eed3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// A continuous coast reuses the mounted overscan instead of rebuilding it
// one row at a time. Runs the production window/measurement/anchor functions.
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';
const src = await Bun.file('src/features/messages.js').text();
const start = src.indexOf('  const WINDOW_AHEAD =');
const end = src.indexOf('  // While the feed is', start);
assert(start > 0 && end > start);
const script = `
const h = (tag, attrs = {}, ...kids) => {
  const el = document.createElement(tag);
  for (const [k,v] of Object.entries(attrs)) el.setAttribute(k,v);
  for (const child of kids.flat()) el.append(child);
  return el;
};
const notes = Array.from({ length: 200 }, (_, i) => ({ id: String(i), height: 140 + (i % 4) * 60 }));
const feed = { heights: new Map(notes.map(e => [e.id, e.height])), winList: () => notes };
let renders = 0, reads = 0;
const rect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () { if (this.matches('.row')) reads++; return rect.call(this); };
${src.slice(start,end)}
function render() {
 renders++;
 document.querySelector('.notes-feed').replaceChildren(...windowedRows(feed, notes,
  e => h('div', { class: 'row', 'data-key': e.id, style: 'height:'+e.height+'px' }, 'Post '+e.id)));
}
window.addEventListener('scroll', () => windowScrolled(feed));
render();
window.runCoast = async () => {
 const frame = () => new Promise(requestAnimationFrame);
 await frame(); await frame(); renders = reads = 0;
 let gaps = 0;
 for (const direction of [1, -1]) {
  for (let i = 0; i < 180; i++) {
   window.scrollBy(0, direction * 50);
   await frame(); await frame();
   const rows = [...document.querySelectorAll('.row')];
   if (!rows.some(r => { const b = rect.call(r); return b.top <= 0 && b.bottom >= 0; })
     && !rows.some(r => { const b = rect.call(r); return b.top >= 0 && b.top <= 2; })) gaps++;
  }
 }
 const result = { renders, reads, gaps, y: scrollY, mounted: document.querySelectorAll('.row').length };
 // Large jumps must still replace the window immediately.
 scrollTo(0, 30000); await frame(); await frame(); await frame();
 const visible = [...document.querySelectorAll('.row')].some(r => { const b = rect.call(r); return b.bottom > 0 && b.top < innerHeight; });
 return { ...result, jumpCovered: visible };
};`;
const html = `<style>body{margin:0}.notes-feed{overflow-anchor:none}</style><div class="notes-feed"></div><script>${script}</script>`;
const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'Content-Type': 'text/html' } }) });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
 const page = await browser.newPage();
 await page.setViewport({ width: 800, height: 800 });
 await page.goto(server.url.href);
 const result = await page.evaluate(() => runCoast());
 console.log(result);
 assert.equal(result.gaps, 0, 'mounted posts always cover the viewport');
 assert.equal(result.y, 0, 'no accumulated anchor drift');
 assert(result.jumpCovered, 'a scrollbar jump is covered');
 assert(result.mounted < 35, 'DOM remains bounded');
 assert(result.renders <= 20, 'reuse the overscan during a coast');
 assert(result.reads < 2200, 'do not measure every mounted post on every scroll frame');
} finally { await browser.close(); server.stop(true); }