aboutsummaryrefslogtreecommitdiffstats
path: root/tools/feed-wheel-reverse-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/feed-wheel-reverse-test.js')
-rw-r--r--tools/feed-wheel-reverse-test.js87
1 files changed, 57 insertions, 30 deletions
diff --git a/tools/feed-wheel-reverse-test.js b/tools/feed-wheel-reverse-test.js
index 8a00b11..80e2fde 100644
--- a/tools/feed-wheel-reverse-test.js
+++ b/tools/feed-wheel-reverse-test.js
@@ -5,15 +5,18 @@ 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() {
+function fixture(inside = '', { feedOnPage = true } = {}) {
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,
+ // the page shows a feed (or, for a thread case, a thread) unless told otherwise
+ const document = { body: {}, addEventListener() {}, removeEventListener() {},
+ querySelector: (selector) => (feedOnPage && selector.split(/,\s*/).some((p) => p === (inside === '.thread-page' ? '.thread-page' : '.feed-page')) ? {} : null) };
+ // the target sits inside the given element (over the posts unless it names something else)
+ const target = { closest: (selector) => selector.split(/,\s*/).some((p) => p === inside) ? target : null,
scrollHeight: 100, clientHeight: 100, parentElement: document.body };
runInNewContext(source.slice(start, end), { window, document,
matchMedia: () => ({ matches: false }), performance: { now: () => now },
@@ -33,37 +36,61 @@ 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.
+ // A busy main thread has missed several frames when the turn back arrives.
f.advance(120);
const before = f.y;
+ // [flying: the first turn back is a brake]
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;
+ assert.equal(f.y, before, 'the brake notch moves nothing by itself');
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');
+ assert.equal(f.y, before, 'old frame time moves nothing');
+ settle(f, 30);
+ const over = (f.y - before) * dir;
+ assert(over >= 0 && over < 120, 'the brake eases the flight out within a short way, never the other way: ' + over);
+ // [the next notch back: the gentle start]
+ const stopped = f.y;
+ f.wheel(-dir);
+ assert((stopped - f.y) * dir > 0 && (stopped - f.y) * dir < 20, 'the gentle start responds at once with a small step');
settle(f);
- // The turn back is a hunt: a quarter of a notch's travel, not a flight the other way.
- const back = (before - f.y) * dir;
- assert(back > 65 && back < 90, 'a reverse notch against a coast is a short step: ' + back);
- // Each notch on grows the step back towards a full one; the sixth is a plain notch again.
- const steps = [];
- for (let i = 0; i < 5; i++) { f.advance(200); const y = f.y; f.wheel(-dir); settle(f); steps.push((y - f.y) * dir); }
- assert(steps.every((s, i) => i === 0 || s > steps[i - 1] * 1.08), 'hunting steps grow notch by notch: ' + steps.map(Math.round));
- assert(steps[0] > 95 && steps[0] < 125 && steps[3] > 240 && steps[3] < 285 && steps[4] > 280 && steps[4] < 330, 'from about a third of a notch to a full one: ' + steps.map(Math.round));
- { f.advance(200); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 280 && s < 330, 'a few flicks on, the wheel flies again: ' + s); }
- // A turn back while hunting starts the small steps over.
- f.wheel(-dir); f.advance(17); f.frame(); f.advance(17); f.frame();
- { const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 65 && s < 90, 'a reversal while hunting is a short step again: ' + s); }
- { f.advance(300); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 95 && s < 120, 'and the next grows from there: ' + s); }
- // A pause ends the hunt: the next notch is the plain push from rest.
- { f.advance(2200); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 280 && s < 330, 'after a pause a notch is a full push: ' + s); }
- // A turn back against a coast that only just died hunts too.
- { f.wheel(dir); settle(f); f.advance(200); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 65 && s < 90, 'reversing right after the coast dies is a short step: ' + s); }
- // ...but a notch from rest the other way, well after, is a full one.
+ const back = (stopped - f.y) * dir;
+ assert(back > 130 && back < 170, 'the notch after the brake creeps back half a notch: ' + back);
+ // [then the wind-up]
+ { const y = f.y; f.wheel(-dir); f.advance(60); f.frame(); f.wheel(-dir); settle(f);
+ const on = (y - f.y) * dir; assert(on > 600, 'the notches after wind up: ' + on); }
+ // The gentle start waits a while for its notch: a second and a half after the brake, still gentle.
+ { f.advance(2500); f.wheel(dir); f.advance(17); f.frame(); f.wheel(dir); f.advance(120); f.frame();
+ f.wheel(-dir); settle(f, 30); f.advance(1500); const y = f.y; f.wheel(-dir); settle(f);
+ const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'a notch back a while after the brake is still the gentle start: ' + s); }
+ // ...but long after, the brake forgotten, a notch back is a plain push from rest.
+ { f.advance(2500); f.wheel(dir); f.advance(17); f.frame(); f.wheel(dir); f.advance(120); f.frame();
+ f.wheel(-dir); settle(f, 30); f.advance(3000); const y = f.y; f.wheel(-dir); settle(f);
+ const s = (y - f.y) * dir; assert(s > 280 && s < 330, 'long after the brake, a notch back is a full push: ' + s); }
+ // A stroll (one notch) turned back has nothing worth braking: the gentle start at once.
+ { f.advance(2500); f.wheel(dir); f.advance(100); f.frame(); const y = f.y; f.wheel(-dir);
+ assert((y - f.y) * dir > 0 && (y - f.y) * dir < 20, 'against a stroll the turn back starts at once, with a small step');
+ settle(f); const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'against a stroll the turn back is the gentle start: ' + s); }
+ // From rest a notch either way is a full push.
{ f.advance(2500); const y = f.y; f.wheel(dir); settle(f); const s = (f.y - y) * dir; assert(s > 280 && s < 330, 'from rest a notch either way is a full push: ' + s); }
+ // A turn back against a coast that only just died is the gentle start too.
+ { f.wheel(dir); settle(f, 40); f.advance(100); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 130 && s < 170, 'reversing right after the coast dies is the gentle start: ' + s); }
+ // Well after the coast died it is a plain notch from rest, whichever way.
+ { f.advance(2500); const y = f.y; f.wheel(-dir); settle(f); const s = (y - f.y) * dir; assert(s > 280 && s < 330, 'a notch the other way from rest is a full push: ' + s); }
+}
+// A notch over a video (or an audio player) in the feed coasts like any
+// other — the wheel does nothing to a player itself — and so does one on a
+// thread page, and one with the pointer anywhere else on the page while a
+// feed is up (the rail, the wallet column, the header).
+for (const sel of ['video', 'audio', '.thread-page', '.wallet-column', '']) {
+ const f = fixture(sel);
+ const y = f.y; f.wheel(1); settle(f);
+ assert(f.y - y > 280 && f.y - y < 330, 'a notch over ' + sel + ' coasts a full push: ' + (f.y - y));
+}
+// ...while a notch in a text box, or in the media pager, is left alone — and
+// so is every notch on a page with no feed on it (the wallet).
+for (const sel of ['textarea', '.media-pager']) {
+ const f = fixture(sel);
+ const y = f.y; f.wheel(1); settle(f);
+ assert.equal(f.y, y, 'a notch in ' + sel + ' is not ours');
}
-console.log('✓ immediate reversal both ways, stale frame timestamps, busy frame recovery, precision hunt after a reversal (short steps that grow, restart on another turn back, end on a pause)');
+{ const f = fixture('', { feedOnPage: false }); const y = f.y; f.wheel(1); settle(f); assert.equal(f.y, y, 'no feed on the page: the wheel is the browser\'s'); }
+console.log('✓ a turn back against a flight brakes, the next notch is the gentle start, then the wind-up; against a stroll or a dead coast the gentle start at once; stale frames; videos and threads coast');