diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 00:30:22 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 00:30:22 -0700 |
| commit | 62edcc721bf762cddad73ade54d8b580913051c7 (patch) | |
| tree | bda08449dfc488724b6736b49c85180c0835ce16 /tools | |
| parent | b6819d91422adc10ea71ea5174c8e3e2badd1340 (diff) | |
| download | coinosv3-62edcc721bf762cddad73ade54d8b580913051c7.tar.gz coinosv3-62edcc721bf762cddad73ade54d8b580913051c7.zip | |
Desktop feed scrolls like a phone: a mouse-wheel notch coasts and slows (quick turns stack, a reverse turn brakes, trackpads left alone), and a mouse can grab the feed and flick it (a sideways drag still selects text, a drag doesn't click what it started on)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/feed-momentum-test.js | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/tools/feed-momentum-test.js b/tools/feed-momentum-test.js new file mode 100644 index 0000000..5b218d3 --- /dev/null +++ b/tools/feed-momentum-test.js @@ -0,0 +1,97 @@ +// 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 +// scrolls and a release while moving throws it; a drag that moved doesn't +// click; a sideways drag is left alone (text selection). +// 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); + }); + + 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 > 180 && one < 400, 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 * 4, 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'); + 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('[grab and flick]'); + await top(); + 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); } + const during = await y(); + await page.mouse.up(); + check('dragging up scrolls with the mouse', during >= 380 && during <= 420, during + 'px'); + const flung = await settle(); + check('letting go while moving throws it', flung > during + 400, flung + 'px'); + await top(); + await page.mouse.move(300, 700); await page.mouse.down(); + for (let i = 1; i <= 10; i++) { await page.mouse.move(300, 700 - i * 30); await sleep(8); } + await sleep(250); // hold still + const held = await y(); + await page.mouse.up(); + check('held still before letting go: it stays put', (await settle()) - held < 5, held + ' → ' + await y()); + await top(); + const b = await (await page.$('#tgt')).boundingBox(); + await page.evaluate(() => { window.__clicks = 0; }); + await page.mouse.move(b.x + 50, b.y + 150); await page.mouse.down(); + for (let i = 1; i <= 6; i++) await page.mouse.move(b.x + 50, b.y + 150 - i * 15); + await page.mouse.up(); await sleep(50); + check('a drag that started on a button doesn\'t click it', await page.evaluate(() => window.__clicks) === 0); + await page.click('#tgt'); await sleep(50); + check('...but a plain click still does', await page.evaluate(() => window.__clicks) === 1); + await top(); + await page.mouse.move(100, 500); await page.mouse.down(); + for (let i = 1; i <= 10; i++) await page.mouse.move(100 + i * 30, 505); + await page.mouse.up(); + check('a sideways drag doesn\'t scroll (it selects text)', (await settle()) === 0); + 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); |
