scrollAnchor.test.ts2.9 KBView on GitHub /**
* Holding the reader's place when older messages are prepended.
*
* The scroll-load shipped without this and it is the defect that makes infinite scroll unusable:
* older messages are PREPENDED, so the content above the viewport grows while `scrollTop` stays
* put. The message being read slides down the page, and the reader is left sitting at the top
* again — which re-triggers the load on the next scroll event, so it chains into itself.
*
* The effect in ChannelThreadView is a few lines of DOM arithmetic wrapped in React. Testing it
* through the component would need a real layout engine (jsdom reports every height as 0), so the
* arithmetic is extracted here and pinned directly — the same rule the effect applies.
*/
/** What the layout effect does, given the container's before/after heights. */
function anchor(input: {
pinnedToBottom: boolean;
scrollTop: number;
prevScrollHeight: number;
scrollHeight: number;
}): number {
if (input.pinnedToBottom) return input.scrollHeight;
if (input.prevScrollHeight && input.scrollHeight > input.prevScrollHeight) {
return input.scrollTop + (input.scrollHeight - input.prevScrollHeight);
}
return input.scrollTop;
}
describe('scroll anchoring on prepend', () => {
it('pushes the viewport down by exactly the height that was added above it', () => {
// 50 older messages added 2000px above a reader sitting 100px from the top: without this they
// would still be at 100px, now looking at completely different messages.
expect(
anchor({ pinnedToBottom: false, scrollTop: 100, prevScrollHeight: 5000, scrollHeight: 7000 }),
).toBe(2100);
});
it('leaves the reader at the top only when nothing was added', () => {
// The end-of-history case: the load resolved with no new messages, so the position stands and
// the "Beginning of #channel" marker is what changes.
expect(
anchor({ pinnedToBottom: false, scrollTop: 80, prevScrollHeight: 5000, scrollHeight: 5000 }),
).toBe(80);
});
it('stays pinned to the bottom for a new message or a send', () => {
expect(
anchor({ pinnedToBottom: true, scrollTop: 4000, prevScrollHeight: 5000, scrollHeight: 5200 }),
).toBe(5200);
});
it('does not move on the very first render, when there is no previous height', () => {
// Opening a different row resets `prevScrollHeight` to 0; treating that as a delta would
// scroll the new thread by the whole height of the one before it.
expect(
anchor({ pinnedToBottom: false, scrollTop: 0, prevScrollHeight: 0, scrollHeight: 7000 }),
).toBe(0);
});
it('ignores a SHRINKING container rather than scrolling backwards', () => {
// Switching rows can render fewer messages before the new window lands; a negative delta
// would yank the reader upward for no reason.
expect(
anchor({ pinnedToBottom: false, scrollTop: 900, prevScrollHeight: 7000, scrollHeight: 3000 }),
).toBe(900);
});
});