Defer and coalesce global catalogue refreshes while SessionDetail is active, carry session metadata through incremental patches, and gate in-flight catalogue commits by route. Harden the virtual timeline cold-open and user-scroll lifecycle, and add Electron frame, continuity, metadata, and overlap regressions.
131 lines
3.9 KiB
JavaScript
131 lines
3.9 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { createSessionUserScroll } from '../app/src/renderer/src/session-user-scroll.mjs';
|
|
|
|
function createScheduler() {
|
|
let now = 0;
|
|
let nextId = 1;
|
|
const tasks = new Map();
|
|
return {
|
|
setTimeout(callback, delay) {
|
|
const id = nextId++;
|
|
tasks.set(id, { callback, due: now + delay });
|
|
return id;
|
|
},
|
|
clearTimeout(id) {
|
|
tasks.delete(id);
|
|
},
|
|
advance(milliseconds) {
|
|
now += milliseconds;
|
|
while (true) {
|
|
const ready = [...tasks.entries()]
|
|
.filter(([, task]) => task.due <= now)
|
|
.sort((left, right) => left[1].due - right[1].due)[0];
|
|
if (!ready) return;
|
|
tasks.delete(ready[0]);
|
|
ready[1].callback();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function dispatch(target, type, properties = {}) {
|
|
const event = new Event(type);
|
|
for (const [key, value] of Object.entries(properties)) {
|
|
Object.defineProperty(event, key, { value });
|
|
}
|
|
target.dispatchEvent(event);
|
|
}
|
|
|
|
test('native scrollend ends a user scroll after a short wheel-burst grace period', () => {
|
|
const scheduler = createScheduler();
|
|
const target = new EventTarget();
|
|
target.scrollTop = 100;
|
|
target.onscrollend = null;
|
|
let ended = 0;
|
|
const userScroll = createSessionUserScroll({
|
|
quietMs: 450,
|
|
scrollEndGraceMs: 100,
|
|
setTimeout: scheduler.setTimeout,
|
|
clearTimeout: scheduler.clearTimeout,
|
|
onEnd: () => { ended++; },
|
|
});
|
|
|
|
userScroll.attach(target);
|
|
dispatch(target, 'wheel', { deltaY: -24 });
|
|
assert.equal(userScroll.isActive(), true);
|
|
assert.equal(userScroll.hasUpwardIntent(), true);
|
|
|
|
scheduler.advance(150);
|
|
assert.equal(userScroll.isActive(), true, '150ms silence must not end native momentum');
|
|
assert.equal(ended, 0);
|
|
|
|
dispatch(target, 'scrollend');
|
|
scheduler.advance(99);
|
|
assert.equal(userScroll.isActive(), true, 'a following wheel packet can retain scroll ownership');
|
|
scheduler.advance(1);
|
|
assert.equal(userScroll.isActive(), false);
|
|
assert.equal(ended, 1);
|
|
|
|
userScroll.detach();
|
|
dispatch(target, 'wheel', { deltaY: 12 });
|
|
assert.equal(userScroll.isActive(), false, 'detached controllers ignore later DOM events');
|
|
});
|
|
|
|
test('unsupported scrollend falls back to a 450ms quiet window', () => {
|
|
const scheduler = createScheduler();
|
|
const target = new EventTarget();
|
|
target.scrollTop = 0;
|
|
let ended = 0;
|
|
const userScroll = createSessionUserScroll({
|
|
quietMs: 450,
|
|
setTimeout: scheduler.setTimeout,
|
|
clearTimeout: scheduler.clearTimeout,
|
|
onEnd: () => { ended++; },
|
|
});
|
|
|
|
userScroll.attach(target);
|
|
dispatch(target, 'wheel', { deltaY: 12 });
|
|
scheduler.advance(449);
|
|
assert.equal(userScroll.isActive(), true);
|
|
scheduler.advance(1);
|
|
assert.equal(userScroll.isActive(), false);
|
|
assert.equal(ended, 1);
|
|
});
|
|
|
|
test('a missed native scrollend still settles through the quiet watchdog', () => {
|
|
const scheduler = createScheduler();
|
|
const target = new EventTarget();
|
|
target.scrollTop = 0;
|
|
target.onscrollend = null;
|
|
let ended = 0;
|
|
const userScroll = createSessionUserScroll({
|
|
quietMs: 450,
|
|
setTimeout: scheduler.setTimeout,
|
|
clearTimeout: scheduler.clearTimeout,
|
|
onEnd: () => { ended++; },
|
|
});
|
|
|
|
userScroll.attach(target);
|
|
dispatch(target, 'wheel', { deltaY: -12 });
|
|
scheduler.advance(450);
|
|
|
|
assert.equal(userScroll.isActive(), false);
|
|
assert.equal(ended, 1, 'a boundary wheel cannot freeze live commits forever');
|
|
});
|
|
|
|
test('downward wheel intent re-enables tail following after an upward escape', () => {
|
|
const target = new EventTarget();
|
|
target.scrollTop = 100;
|
|
target.onscrollend = null;
|
|
const userScroll = createSessionUserScroll();
|
|
userScroll.attach(target);
|
|
|
|
dispatch(target, 'wheel', { deltaY: -1 });
|
|
assert.equal(userScroll.hasUpwardIntent(), true);
|
|
dispatch(target, 'wheel', { deltaY: 1 });
|
|
assert.equal(userScroll.hasUpwardIntent(), false);
|
|
userScroll.detach();
|
|
});
|