Carousel Tiles: Modulus Pagination and a Jest Mock That Shared State
TL;DR
- Built the random tile fetching on top of the optional-field schema changes from last week.
- Skip/limit pagination broke at collection boundaries;
cohort age % document countreplaced it cleanly. findOnewith a stable sort beat offset arithmetic for fetching a single tile.- A Jest test failed because the mock returned a reference to shared data, not a fresh copy.
- Spreading the mock into a new array per test isolated them and fixed it.
Continuing the carousel work. Before the feature work I spent time mapping the existing entities and how they relate, which surfaced some redundant and unclear relationships worth consolidating — worth doing first, since the changes below touch several of them.
Wiring up random tiles
The backend endpoint for random tiles is in, with cohort IDs and date fields optional as designed. Running the tests and type checker confirmed the optional types didn’t break existing behaviour, which was the main risk of that change.
The weekly content logic needed rework. A flag on the cohort schema decides whether random tiles get prefetched, and when set, two generic tiles appear first in the carousel. If no prescribed content exists — or the cohort has finished — the script inserts generic content instead of leaving the carousel empty.
Pagination, and why modulus won
Fetching the right tile for a given day turned into the interesting problem.
I started with MongoDB’s skip and limit. skip maps directly to an index — skip zero to start at the first document — so aligning page numbers with it is straightforward until you reach the end of the collection. Then the offset runs past the last document and you get index out-of-bounds, or gaps where a day has no tile.
Wrapping around is the correct behaviour: when the cohort outlives the tile pool, start again from the beginning. I wrote explicit logic to handle the boundary, and it was fiddly enough that I went looking for something simpler.
Modulus is the answer. Calculating the index as cohort age modulo document count wraps automatically. No boundary case, no special handling, no gaps — the arithmetic can’t produce an out-of-range value. Considerably more maintainable than the offset logic it replaced.
From there I switched to fetching a single item with findOne rather than paginating at all. Once the index is a simple computation, there’s no reason to request a page and take one element from it.
Stable sort order turned out to be essential. Without a deterministic sort, the same index returns different documents on different queries, so I sort on createdAt and ID to keep results consistent regardless of when documents were created.
The test that lied
A test checking that random content gets added when “always show random tiles” is true kept failing on its third assertion. The first two passed — a quote tile and a learn tile matched — and then the content was wrong.
I assumed the logic was modifying data it shouldn’t. After adding logging, the real cause was subtler: the mock returned a reference to the mock object rather than a new array. Each test mutated the shared data, so by the third assertion it was operating on something two earlier tests had already changed.
The fix is to spread the mocked data into a new array on each call, so every test gets its own copy.
That’s a JavaScript reference-versus-value problem wearing test-framework clothing, and it’s worth internalising: a mock that returns an object hands out a pointer to the same object every time unless you explicitly copy it. The tests weren’t independent and nothing said so.
Testing plan for the display bug
Separately I put together a manual testing plan, aimed at the bug report about how tiles appear rather than at randomness itself.
The structure: define a cohort with no carousel content, use a CSV template covering days one through seven so any test day has visible content, then walk three scenarios — no content, one piece, two or more. Checking randomness means observing consecutive weeks with nothing prescribed, adjusting dates to move through them.
Logging each scenario as I go, so anything that breaks can be reproduced exactly rather than described vaguely.
ryer.io