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/feed-wheel-reverse-test.js | |
| parent | 537b116400089ce15a1bdbeb0873cb88dfa35fe3 (diff) | |
| download | coinosv3-817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766.tar.gz coinosv3-817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766.zip | |
Restart wheel coast timing and respond immediately on reversal
Diffstat (limited to 'tools/feed-wheel-reverse-test.js')
| -rw-r--r-- | tools/feed-wheel-reverse-test.js | 50 |
1 files changed, 50 insertions, 0 deletions
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'); |
