aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 01:06:37 -0700
committerAdam Soltys <[email protected]>2026-10-09 01:06:37 -0700
commit817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766 (patch)
treedcee40a6ed25e6b7e8a1f0dcfa180aca9db05f97 /tools
parent537b116400089ce15a1bdbeb0873cb88dfa35fe3 (diff)
downloadcoinosv3-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.js14
-rw-r--r--tools/feed-wheel-reverse-test.js50
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');