Context
Part of the #18985 contributor door. Today three external contributors closed four good first issue tickets between them, which left two open across the whole organisation — so the constraint on that epic is now supply, and this is one of the slices its census identified.
It is deliberately sized as a first issue rather than a second one: the unit tier already has a toolbar/ directory with two specs in it, so there is no fixture app to build and no new directory to invent. Contrast #19010, whose author had to establish a component-tier fixture app first and lost two debugging rounds to a setupClass side-effect.
Sweeps, 2026-09-21T13:18Z. Live latest-20 open: no equivalent. Duplicate search on toolbar.Paging returns eight tickets, all CLOSED and all from the v4 era — #3019, #3023, #3025, #3029, #3030, #3033, #3034, #3035. They are the feature work that built this class; none is about covering it, and #3033 (updateNavigationButtons()) is the origin of the method this ticket's sharpest edge lives in. A2A in-flight: no claim on toolbar. Memory Core on the symptom: no prior decision (weak negative — the semantic index returned unrelated rows). Own open assignments: none on this surface.
The Problem
src/toolbar/Paging.mjs is 277 lines with five reactive configs, four click handlers and three update methods. Nothing covers it at either tier. git grep finds it constructed in exactly one place outside its own example: test/playwright/unit/data/StoreSortLoadContract.spec.mjs, incidentally, for a store contract rather than for the toolbar.
The class is a good first target because most of its behaviour is arithmetic plus enablement, both observable without rendering.
The Architectural Reality
src/toolbar/Paging.mjs:186 — getMaxPages() is Math.ceil(this.store.totalCount / this.pageSize). One line, pure, and the source of every edge below.
:247 — updateNavigationButtons() sets four disabled flags from currentPage === 1 and currentPage === maxPages, strict equality on both ends.
:193–:231 — the four handlers. onNextPageButtonClick and onPrevPageButtonClick guard their bounds; onFirstPageButtonClick and onLastPageButtonClick assign directly.
:234 — onStoreLoad() re-reads currentPage from the store and calls all three update methods.
test/playwright/unit/toolbar/ already holds ActionButton.spec.mjs and Actions.spec.mjs — the precedent for testing a toolbar at the unit tier with Neo.create.
examples/toolbar/paging/view/MainContainer.mjs is a runnable reference: npm run server-start and open it to see the real thing before writing anything.
The Fix
Add test/playwright/unit/toolbar/Paging.spec.mjs, following the setup idiom of its two siblings in that directory.
Read the values off the running engine rather than deriving them from the source. That is what made #19007 and #19010 hold up: where the behaviour looks unintended, assert it as-is and say so in the test body, so the disagreement is recorded rather than silently blessed or silently "fixed".
Four areas, and the arithmetic edges are the interesting ones. These are candidates to measure, not defects I am claiming — I read the source, I did not run it:
getMaxPages() boundaries. An exact multiple (totalCount: 60, pageSize: 30), a partial last page (61), a page size larger than the record count, and an empty store (totalCount: 0), where Math.ceil(0 / 30) is 0.
- Navigation enablement at the ends. On page 1, first/prev disabled. On the last page, next/last disabled. Then the empty-store case: with
maxPages at 0 and currentPage at 1, currentPage === maxPages is 1 === 0 — what do next and last do? Measure it. If they stay enabled, record that, and record what onLastPageButtonClick then assigns.
- Handler bounds.
onNextPageButtonClick refuses to pass getMaxPages(), onPrevPageButtonClick refuses to go below 1 — both should be asserted by trying to cross the boundary, not by calling them once in the middle.
pageSize changes while on a late page. Growing the page size shrinks maxPages. Does currentPage still sit inside the new range, and what do the buttons report if it does not? updateNavigationButtons compares with ===, so a currentPage above maxPages matches neither end.
Acceptance Criteria
Out of Scope
- Changing
src/toolbar/Paging.mjs. If the empty-store case or the === comparison turns out to be wrong, that is a finding for its own ticket, and this spec is what will pin whichever way it is decided.
toolbar/Base.mjs (769 lines), Breadcrumb.mjs and ActionButton.mjs. One class per PR; the rest of the directory stays open for the next person.
- The component tier. Everything above is reachable with
Neo.create at the unit tier, which is why this is a first issue.
Avoided Traps
- Asserting the value the code just wrote.
expect(toolbar.currentPage).toBe(3) after setting it to 3 pins nothing. The assertions worth having are the ones where an input (a store count, a page size) produces an output through arithmetic you did not restate in the test.
- Deriving expectations from the source. Read them off the running instance. A test written from the implementation agrees with the implementation by construction, including where it is wrong.
- Treating an odd result as a bug to fix here. Record it, name it in the PR, let a maintainer decide. #19007 did exactly that and the recorded case became #19027.
Related
- #18985 — the contributor door this supplies.
- #19007 / #19010 — the shape that worked: measured expectations, sabotage-tested, odd cases recorded rather than blessed.
- #3033 — the closed v4 ticket that added
updateNavigationButtons().
examples/toolbar/paging/ — the runnable reference.
unowned-rationale: parked for the community as a good first issue. One class, one spec file, an existing directory and a runnable example — completable on a clean clone with no Agent OS service.
Origin Session ID: 2c9d83d3-7879-46f6-b49d-590b631d4f55
Retrieval Hint: query_raw_memories("toolbar Paging getMaxPages navigation enablement empty store coverage").
Context
Part of the #18985 contributor door. Today three external contributors closed four
good first issuetickets between them, which left two open across the whole organisation — so the constraint on that epic is now supply, and this is one of the slices its census identified.It is deliberately sized as a first issue rather than a second one: the unit tier already has a
toolbar/directory with two specs in it, so there is no fixture app to build and no new directory to invent. Contrast #19010, whose author had to establish a component-tier fixture app first and lost two debugging rounds to asetupClassside-effect.Sweeps, 2026-09-21T13:18Z. Live latest-20 open: no equivalent. Duplicate search on
toolbar.Pagingreturns eight tickets, all CLOSED and all from the v4 era — #3019, #3023, #3025, #3029, #3030, #3033, #3034, #3035. They are the feature work that built this class; none is about covering it, and #3033 (updateNavigationButtons()) is the origin of the method this ticket's sharpest edge lives in. A2A in-flight: no claim ontoolbar. Memory Core on the symptom: no prior decision (weak negative — the semantic index returned unrelated rows). Own open assignments: none on this surface.The Problem
src/toolbar/Paging.mjsis 277 lines with five reactive configs, four click handlers and three update methods. Nothing covers it at either tier.git grepfinds it constructed in exactly one place outside its own example:test/playwright/unit/data/StoreSortLoadContract.spec.mjs, incidentally, for a store contract rather than for the toolbar.The class is a good first target because most of its behaviour is arithmetic plus enablement, both observable without rendering.
The Architectural Reality
src/toolbar/Paging.mjs:186—getMaxPages()isMath.ceil(this.store.totalCount / this.pageSize). One line, pure, and the source of every edge below.:247—updateNavigationButtons()sets fourdisabledflags fromcurrentPage === 1andcurrentPage === maxPages, strict equality on both ends.:193–:231— the four handlers.onNextPageButtonClickandonPrevPageButtonClickguard their bounds;onFirstPageButtonClickandonLastPageButtonClickassign directly.:234—onStoreLoad()re-readscurrentPagefrom the store and calls all three update methods.test/playwright/unit/toolbar/already holdsActionButton.spec.mjsandActions.spec.mjs— the precedent for testing a toolbar at the unit tier withNeo.create.examples/toolbar/paging/view/MainContainer.mjsis a runnable reference:npm run server-startand open it to see the real thing before writing anything.The Fix
Add
test/playwright/unit/toolbar/Paging.spec.mjs, following the setup idiom of its two siblings in that directory.Read the values off the running engine rather than deriving them from the source. That is what made #19007 and #19010 hold up: where the behaviour looks unintended, assert it as-is and say so in the test body, so the disagreement is recorded rather than silently blessed or silently "fixed".
Four areas, and the arithmetic edges are the interesting ones. These are candidates to measure, not defects I am claiming — I read the source, I did not run it:
getMaxPages()boundaries. An exact multiple (totalCount: 60,pageSize: 30), a partial last page (61), a page size larger than the record count, and an empty store (totalCount: 0), whereMath.ceil(0 / 30)is0.maxPagesat0andcurrentPageat1,currentPage === maxPagesis1 === 0— what do next and last do? Measure it. If they stay enabled, record that, and record whatonLastPageButtonClickthen assigns.onNextPageButtonClickrefuses to passgetMaxPages(),onPrevPageButtonClickrefuses to go below 1 — both should be asserted by trying to cross the boundary, not by calling them once in the middle.pageSizechanges while on a late page. Growing the page size shrinksmaxPages. DoescurrentPagestill sit inside the new range, and what do the buttons report if it does not?updateNavigationButtonscompares with===, so acurrentPageabovemaxPagesmatches neither end.Acceptance Criteria
test/playwright/unit/toolbar/Paging.spec.mjsexists and passes withnpm run test-unit -- test/playwright/unit/toolbar/.getMaxPages()is covered at an exact multiple, a partial last page, and an empty store.src/is untouched. This is a coverage ticket; a fix, if one is warranted, is a separate ticket that this spec will then pin.src/toolbar/Paging.mjs— for example inverting adisabledcomparison or changingMath.ceiltoMath.floor— restored afterwards. A test that cannot fail proves nothing.Out of Scope
src/toolbar/Paging.mjs. If the empty-store case or the===comparison turns out to be wrong, that is a finding for its own ticket, and this spec is what will pin whichever way it is decided.toolbar/Base.mjs(769 lines),Breadcrumb.mjsandActionButton.mjs. One class per PR; the rest of the directory stays open for the next person.Neo.createat the unit tier, which is why this is a first issue.Avoided Traps
expect(toolbar.currentPage).toBe(3)after setting it to 3 pins nothing. The assertions worth having are the ones where an input (a store count, a page size) produces an output through arithmetic you did not restate in the test.Related
updateNavigationButtons().examples/toolbar/paging/— the runnable reference.unowned-rationale: parked for the community as a
good first issue. One class, one spec file, an existing directory and a runnable example — completable on a clean clone with no Agent OS service.Origin Session ID: 2c9d83d3-7879-46f6-b49d-590b631d4f55
Retrieval Hint:
query_raw_memories("toolbar Paging getMaxPages navigation enablement empty store coverage").