diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 01:06:37 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 01:06:37 -0700 |
| commit | 817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766 (patch) | |
| tree | dcee40a6ed25e6b7e8a1f0dcfa180aca9db05f97 /tools | |
| parent | 537b116400089ce15a1bdbeb0873cb88dfa35fe3 (diff) | |
| download | coinosv3-817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766.tar.gz coinosv3-817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766.zip | |
Restart wheel coast timing and respond immediately on reversal
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/feed-momentum-test.js | 14 | ||||
| -rw-r--r-- | tools/feed-wheel-reverse-test.js | 50 |
2 files changed, 64 insertions, 0 deletions
diff --git a/tools/feed-momentum-test.js b/tools/feed-momentum-test.js index 9f61667..29e4934 100644 --- a/tools/feed-momentum-test.js +++ b/tools/feed-momentum-test.js @@ -57,6 +57,20 @@ try { 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))); diff --git a/tools/feed-wheel-reverse-test.js b/tools/feed-wheel-reverse-test.js new file mode 100644 index 0000000..d8cc44a --- /dev/null +++ b/tools/feed-wheel-reverse-test.js @@ -0,0 +1,50 @@ +// Exercise the real input/coast handlers with controlled frame timing. +import assert from 'node:assert/strict'; +import { runInNewContext } from 'node:vm'; +const source = await Bun.file('src/app.js').text(); +const start = source.indexOf('(() => {', source.indexOf('// The feed scrolls like a phone')); +const end = source.indexOf("\nwindow.addEventListener('load'", start); +assert(start > 0 && end > start); +function fixture() { + let now = 100, id = 0; + const handlers = new Map(), frames = new Map(), steps = []; + const window = { scrollY: 5000, + addEventListener: (name, fn) => handlers.set(name, fn), + scrollBy: ({ top }) => { steps.push(top); window.scrollY += top; }, + }; + const document = { body: {}, addEventListener() {}, removeEventListener() {} }; + const target = { closest: (selector) => selector.includes('.feed-page') ? target : null, + scrollHeight: 100, clientHeight: 100, parentElement: document.body }; + runInNewContext(source.slice(start, end), { window, document, + matchMedia: () => ({ matches: false }), performance: { now: () => now }, + requestAnimationFrame: (fn) => { frames.set(++id, fn); return id; }, + cancelAnimationFrame: (id) => frames.delete(id), setTimeout, clearTimeout, + }); + return { + get y() { return window.scrollY; }, steps, + wheel(dir) { handlers.get('wheel')({ deltaY: dir * 100, wheelDeltaY: -dir * 120, + deltaMode: 0, deltaX: 0, target, timeStamp: now, preventDefault() {} }); }, + advance(ms) { now += ms; }, + frame(stamp = now) { const callbacks = [...frames.values()]; frames.clear(); for (const fn of callbacks) fn(stamp); }, + }; +} +for (const dir of [1, -1]) { + const f = fixture(); + f.wheel(dir); f.advance(17); f.frame(); + f.wheel(dir); f.advance(17); f.frame(); + // A busy main thread has missed several frames when the reverse arrives. + f.advance(120); + const before = f.y; + f.wheel(-dir); + assert((f.y - before) * dir < 0, 'first reverse notch responds immediately'); + assert(Math.abs(f.y - before) < 20, 'reverse starts with a small bounded step'); + const reversed = f.y; + f.frame(0); // a queued animation frame can carry an older timestamp + assert.equal(f.y, reversed, 'old frame time cannot revive the previous direction'); + f.advance(17); f.frame(); + assert((f.y - reversed) * dir < 0, 'next frame continues in the new direction'); + assert(Math.abs(f.y - reversed) < 20, 'new velocity is not applied to the old frame delay'); + for (let i = 0; i < 100; i++) { f.advance(17); f.frame(); } + assert(Math.abs(f.y - before) > 280 && Math.abs(f.y - before) < 330, 'one notch retains its coast distance'); +} +console.log('✓ immediate reversal both ways, stale frame timestamps, busy frame recovery, and coast distance'); |
