LearnNewsExamplesServices
Frontmatter
id19038
titleNeo.toolbar.Paging has no coverage, and its page maths has edges
stateClosed
labels
enhancementgood first issueaitesting
assignees[]
createdAtSep 21, 2026, 3:18 PM
updatedAtSep 23, 2026, 10:54 AM
githubUrlhttps://github.com/neomjs/neo/issues/19038
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 23, 2026, 10:54 AM

Neo.toolbar.Paging has no coverage, and its page maths has edges

Closed v13.2.0/archive-v13-2-0-chunk-22 enhancementgood first issueaitesting
neo-opus-ada
neo-opus-ada commented on Sep 21, 2026, 3:18 PM

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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

  • test/playwright/unit/toolbar/Paging.spec.mjs exists and passes with npm run test-unit -- test/playwright/unit/toolbar/.
  • getMaxPages() is covered at an exact multiple, a partial last page, and an empty store.
  • Navigation-button enablement is asserted at both ends and in the empty-store case, with the measured result recorded whatever it turns out to be.
  • Each handler bound is proved by attempting to cross it, not by a call in the middle of the range.
  • Any behaviour that looks unintended is asserted as-is with a comment saying why it is recorded rather than changed — and named in the PR body so a maintainer can decide whether it wants its own ticket.
  • src/ is untouched. This is a coverage ticket; a fix, if one is warranted, is a separate ticket that this spec will then pin.
  • The PR shows the spec failing against at least one deliberate mutation of src/toolbar/Paging.mjs — for example inverting a disabled comparison or changing Math.ceil to Math.floor — restored afterwards. A test that cannot fail proves nothing.

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").

sloemo01
sloemo01 Sep 22, 2026, 12:18 PM

Taking this one.

Both premises check out against dev, and I read all four of the areas you flagged as candidates rather than defects before writing anything.

My reading, to be confirmed by measurement rather than asserted:

  • getMaxPages() at :186 is the Math.ceil you describe, so the empty store gives 0 and an exact multiple is exact.
  • updateNavigationButtons() at :247 compares currentPage === maxPages on the last-page side, and the empty store is where those two disagree: maxPages is 0 while currentPage is 1.
  • The two guarded handlers refuse at their bounds; the other two assign directly, which is where the === result matters most.

Plan: the spec follows test/playwright/unit/toolbar/'s existing Neo.create idiom, every expected value read off the running instance, and all four areas covered with the odd cases recorded as-is and named in the PR body.

On the sabotage requirement: I will run the Math.ceil -> Math.floor mutation and the inverted disabled comparison, show both failing, and restore. I will paste the failure output unfiltered rather than a summary line, since a run I truncate is a run I cannot quote.

If the empty-store case turns out to be the === reading it appears to be from the source, I will assert what actually happens and leave the decision to you as the ticket asks.

sloemo01 cross-referenced by PR #19049 on Sep 22, 2026, 12:55 PM
sloemo01 referenced in commit 23f766a - "test(toolbar): clarify handler bounds are unreachable from the UI (#19038) on Sep 22, 2026, 6:34 PM
sloemo01 referenced in commit db2d568 - "test(toolbar): drop .serial from the Paging spec (#19038) on Sep 23, 2026, 5:19 AM
tobiu referenced in commit bac9e80 - "test(toolbar): cover Paging page maths and navigation enablement (#19038) (#19049) on Sep 23, 2026, 10:54 AM
tobiu closed this issue on Sep 23, 2026, 10:54 AM