aboutsummaryrefslogtreecommitdiffstats
path: root/tools/feed-momentum-test.js
blob: 3debd8a90412d0603d17f477542119fb1c72c580 (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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// Desktop feed scrolling feels like a phone: a mouse-wheel notch coasts
// farther than the browser's own step, quick notches stack, a reverse turn
// brakes; a trackpad's smooth deltas are left to the browser; a mouse drag
// selects text, as on any page.
// Uses a tall stand-in feed so no relays are needed.
// Run: bun tools/feed-momentum-test.js
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 html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: (req) => {
  const p = new URL(req.url).pathname;
  if (p.startsWith('/punks') || p === '/verify-worker.js' || p.startsWith('/locales') || /\.js$/.test(p)) { const f = Bun.file('dist' + p); return f.size ? new Response(f) : new Response('', { status: 404 }); }
  return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
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)));
const y = () => page.evaluate(() => window.scrollY);
const settle = async () => { let a = -1, b = await y(); while (a !== b) { await sleep(150); a = b; b = await y(); } return b; };
const top = async () => { await page.evaluate(() => window.scrollTo(0, 0)); await sleep(100); };
// a real wheel notch, as Chrome makes one (wheelDeltaY ±120)
const notch = (dir = 1, smooth = false) => page.evaluate((d, s) => {
  const t = document.querySelector('#stand-in p:nth-child(3)');
  const ev = new WheelEvent('wheel', { deltaY: s ? d * 7.5 : d * 100, deltaMode: 0, bubbles: true, cancelable: true, clientX: 200, clientY: 300 });
  Object.defineProperty(ev, 'wheelDeltaY', { value: s ? -d * 9 : -d * 120 });
  t.dispatchEvent(ev);
  return ev.defaultPrevented;
}, dir, smooth);
try {
  await page.setViewport({ width: 1280, height: 900 });
  await page.goto(server.url.origin, { waitUntil: 'domcontentloaded' });
  await sleep(800);
  await page.evaluate(() => {
    const f = document.createElement('div'); f.id = 'stand-in'; f.className = 'feed-page';
    for (let i = 0; i < 400; i++) { const p = document.createElement('p'); p.style.cssText = 'height:120px;margin:0'; p.textContent = 'post ' + i; f.append(p); }
    const b = document.createElement('button'); b.id = 'tgt'; b.textContent = 'a button in a post'; b.style.cssText = 'position:relative;height:200px;width:600px';
    b.onclick = () => { window.__clicks = (window.__clicks || 0) + 1; };
    f.prepend(b);
    document.body.prepend(f);
  });
  await sleep(1500); await top(); // the app's own first paints settle before the wheel

  console.log('[wheel]');
  check('a notch is taken over', await notch(1));
  const one = await settle();
  check('one notch coasts well past a plain 100px step', one > 220 && one < 480, one + 'px');
  await top();
  for (let i = 0; i < 5; i++) { await notch(1); await sleep(40); }
  const five = await settle();
  check('five quick notches travel much farther', five > one * 2.5, five + 'px');
  await top();
  for (let i = 0; i < 3; i++) { await notch(1); await sleep(40); }
  await sleep(60); await notch(-1);
  const brake = await settle();
  check('a reverse turn brakes the coast', brake < 3 * one, brake + 'px');
  for (const dir of [1, -1]) {
    await page.evaluate(() => window.scrollTo(0, 2000));
    await notch(dir); await sleep(60);
    const first = await page.evaluate((dir) => {
      const ev = new WheelEvent('wheel', { deltaY: -dir * 100, deltaMode: 0, bubbles: true, cancelable: true });
      Object.defineProperty(ev, 'wheelDeltaY', { value: dir * 120 });
      const before = scrollY;
      document.querySelector('#stand-in p').dispatchEvent(ev);
      return scrollY - before;
    }, dir);
    check('reversal responds immediately with a small step (' + dir + ')', first * dir < 0 && Math.abs(first) <= 16, first + 'px');
    await settle();
  }

  await top();
  check('a trackpad\'s smooth deltas are the browser\'s', !(await notch(1, true)));
  check('...and a notch right after them too (still a trackpad)', !(await notch(1)));
  await sleep(500);

  console.log('[a mouse drag selects text]');
  await top();
  const b = await (await page.$('#tgt')).boundingBox();
  await page.evaluate(() => { window.__clicks = 0; getSelection().removeAllRanges(); });
  await page.mouse.move(300, 700); await page.mouse.down();
  for (let i = 1; i <= 10; i++) { await page.mouse.move(300, 700 - i * 40); await sleep(8); }
  await page.mouse.up();
  check('dragging up doesn\'t scroll the feed', (await settle()) === 0, await y() + 'px');
  check('...it selects text', await page.evaluate(() => getSelection().toString().length > 0));
  await page.click('#tgt'); await sleep(50);
  check('a plain click still clicks', await page.evaluate(() => window.__clicks) === 1);
  check('no page errors', errs.length === 0, errs.join('; ').slice(0, 300));
} catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(); }
console.log(ok ? 'PASS' : 'FAIL');
process.exit(ok ? 0 : 1);