Frontmatter
| id | 19006 |
| title | The calendar month view has no component-tier witness |
| state | Closed |
| labels | enhancementgood first issueaitesting |
| assignees | [] |
| createdAt | Sep 19, 2026, 11:01 PM |
| updatedAt | Sep 24, 2026, 7:01 PM |
| githubUrl | https://github.com/neomjs/neo/issues/19006 |
| author | neo-opus-ada |
| commentsCount | 3 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |
| blockedBy | [] |
| blocking | [] |
| closedAt | Sep 24, 2026, 7:01 PM |
The calendar month view has no component-tier witness

Taking this one, the sibling of #19010 which is now up as #19024.
I have the component-test harness working from that PR, so the fixture and spec structure are known: component/apps/calendar-month/ with the three files, driven by URL and asserted with page.locator and page.evaluate.
Reading src/calendar/ and examples/calendar/basic/ before writing anything. Two of the five suggested assertions look most likely to find something real, and I will report which I chose and why:
- The runtime
weekStartDaychange, since a grid that re-renders its header but leaves the old first column is exactly the kind of thing that passes a config-level test. - The Sunday-start wrap, where the offset goes
0 -> 6. That is arithmetic I can derive by hand and write out literally, which is the point.
On the row count: I will assert what getWeeksOfMonth does and match the source rather than an idealised calendar, as the ticket says. If anything goes red against src/, I will stop and report it as a finding with the test as evidence rather than fixing the calendar here.

The component-tier witness found something on its first run, so per this ticket's own instruction I stopped and am reporting it instead of writing a green suite.
1. The month grid renders nothing on mount
An empty month renders zero rows. The header is there (Feb 2027, day names from the configured weekStartDay), and the scroll container is empty:
weekCount: 0 dayCount: 0 headerCount: 0
Mounting Neo.calendar.view.month.Component with both stores present but empty, the grid never builds. The chain in src/calendar/view/month/Component.mjs:
construct(:181) setsneedsEventUpdate = trueonly whencalendarStore.getCount() > 0 && eventStore.getCount() > 0. With empty stores it staysfalse.createContent()(:434) has a!me.mountedguard; while unmounted it only setsneedsEventUpdate = trueand builds nothing.afterSetMounted(:320) callscreateContent()only ifneedsEventUpdateis already true.
So an empty month never satisfies the guard that would let it draw, and nothing later revisits it. A calendar with no events yet is the normal first-run state, which is why I read this as a defect rather than a deliberate lazy path.
Proof that only that guard is at fault. A runtime weekStartDay change goes through afterSetWeekStartDay (:401), which calls createContent(true) unconditionally, and the grid appears immediately, complete and correct:
AFTER weekStartDay change: weekCount 18, dayCount 126
dayNames: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
Same component, same empty stores. The only difference is that the second path does not ask needsEventUpdate first.
2. Two of the ticket's premises do not match the source
Not defects, but they change what this spec can assert, so they are worth settling before someone writes it.
The month view does not use getWeeksOfMonth, and its row count is not 5 or 6. The helper's only caller under src/ is YearComponent.mjs:447. createContent builds 18 weeks (:450) to make the scrolling month continuous, so the real invariant is 18 weeks and 126 day cells, measured above. An assertion of "5 rows for a 5-row month" would have failed on correct code.
There is no neighbouring-month marker to assert on. createWeek (:509) gives a day cell cls: ['neo-day'] plus neo-weekend for Saturday/Sunday. A leading or trailing cell carries no class, flag, or property distinguishing it from a day of the current month; the ymd is in the element id. Asserting "leading cells are marked" would have meant inventing the marker, which is presumably why the ticket says to read the component rather than assume a name.
What I have
The fixture is built and mounts the view without the surrounding app shell: test/playwright/component/apps/calendar-month/ (three files) with a fixed currentDate of February 2027 and weekStartDay: 1, and nothing reads the wall clock. It is enough to reproduce both points above.
I have not opened a PR, because a spec in this state would either assert the empty grid as correct or duplicate work once #1 is fixed. Say which you want and I will do it:
- the spec written against the 18-week invariant and the day-name header, with the empty-store case recorded as a known red, or
- an issue for #1 on its own, leaving this ticket's spec until the mount path is settled.
I am happy to take #1 as a separate PR if you would rather have the fix than the report.
- 2026-09-21T12:51:31Z @neo-opus-ada cross-referenced by #18985
- 2026-09-23T15:03:02Z @neo-opus-ada cross-referenced by #19149
@neo-opus-ada - 2026-09-23T15:03:11Z
@sloemo01 thank you, and I'm sorry you waited three days for an answer. Your reading was right, and the ticket was wrong.
The defect is real, and narrower than it first looks. I measured the month view with real stores (unit environment, dev at 64ff609):
| calendars | events | grid after mount |
|---|---|---|
| 0 | 0 | empty |
| 1 | 0, store loaded | 18 weeks |
| 1 | store never loaded | empty |
| 1 | 1 | 18 weeks |
An empty event store is fine once it has loaded. Its load reaches onEventStoreLoad, and that marks the grid for building. The grid stays empty when there are no calendars, or while a store has not loaded. Every path that builds it first asks for a store count, and a count cannot tell "loaded and empty" from "not loaded yet". The check comes from #2460, which fixed a race with lazily loaded views. A month grid should show its days without any data, so this is its own issue: #19149, reserved for you because you found it. Please take the fix if you want it. Your fixture is its regression test.
Your premise corrections hold, and there is a third. The month view builds 18 weeks and never calls getWeeksOfMonth. Its day cells carry no neighbouring-month marker. And "the first cell of the grid" sits six weeks before the week that holds the 1st. I have rewritten the ticket's assertion list against the source, and measured every new line the same way.
Order: the fix first, then this spec on top of it. That way the spec asserts the no-calendar case green instead of recording a known red.
- 2026-09-24T10:55:51Z @neo-opus-grace cross-referenced by PR #19150
Context
src/calendar/is Neo's largest completely unwitnessed area. Measured 2026-09-19 atdev@3ab2e91ab5: 26 modules undersrc/calendar/are never imported by any file undertest/playwright/, and there is no calendar spec anywhere —test/playwright/unit/vdom/Calendar.spec.mjsis a VDom spec that happens to use calendar-shaped data, not coverage of the calendar.Meanwhile
Neo.util.Date— the arithmetic every calendar view computes its grid from — was pinned earlier today, by an outside contributor, in#19004. So the helpers underneath are now trustworthy and the views on top of them are still unobserved. This ticket closes the layer above.The Problem
src/calendar/view/month/Component.mjsrenders a scrolling window of 18 week rows around the current month. Where the month's first day lands, which row carries the month header, and which cells are weekend cells are all computed fromweekStartDayand the month's shape. Nothing checks any of it.A regression here does not throw. It renders a calendar that is quietly wrong by one day, which is the hardest class of bug to notice in review and the easiest to ship.
The interesting part — and the reason this is not a pure-unit ticket — is that the correct answers are not written anywhere. You derive them: a month starting on a Sunday with
weekStartDay: 1begins six cells in, becauseNeo.util.Date.getFirstDayOffsetwraps. Getting that wrong in the test is the trap, and it is why literal, hand-computed expectations matter more here than anywhere else.The Architectural Reality
examples/calendar/basic/is a complete, runnable application —MainContainer.mjs, a controller, and adata/folder. It is what you drive.src/calendar/view/month/Component.mjsis the view under test.src/calendar/view/MainContainer.mjsownsweekStartDayand the current date.src/util/Date.mjssupplies the arithmetic. The month view calls onlygetFirstDayOffset(createContent()), which#19004pinned, so you can trust it as an oracle for your expectations.getWeeksOfMonthbelongs to the year view.test/playwright/component/apps/<name>/—buffered-listis a readable small example of the shape. The runner isnpm run test-components.The Fix
test/playwright/component/apps/calendar-month/— the smallest app that mounts the month view with a fixed date and a controllableweekStartDay. Borrow fromexamples/calendar/basic/; do not depend on today's date anywhere.test/playwright/component/calendar/asserting what the month view renders.What to assert — pick what you can state confidently and say in the PR which you chose:
neo-weekrows of 7neo-daycells, 126 in all. February 2027 and August 2027 are both good cases. WithshowWeekends: falsethe weekend cells are removed, which leaves 90.0to6. That row starts on 2027-08-01 withweekStartDay: 0, and on 2027-07-26 withweekStartDay: 1. The day's date is in each cell's id (<id>__day__2027-08-01). Derive the dates by hand and write them out literally. Month and day names come fromIntl, so setlocale: 'en-US'in the fixture before asserting any name.neo-weekendfollows the day, not the column. In that row it is on the first and last cells withweekStartDay: 0, and on the last two withweekStartDay: 1.weekStartDayat runtime re-renders the rows and the day-name header. After a change from0to1, row 6 starts on 2027-07-26 and the day-name header starts with Monday.If an assertion goes red against
src/, stop and say so in the PR. Do not fix the calendar here; that finding becomes its own issue with your test as the evidence, which is a more valuable contribution than a green suite.How to run it
npm install npx playwright install chromium npm run test-components -- test/playwright/component/calendar/npm run test-componentsruns the whole component tier. Do not run a barenpx playwright test— this repository has several Playwright configurations and the default one is not the tier you want.To see what you are testing, run the example itself:
npm run build-themes -- -n -e dev -t all npm run server-start <h1 class="neo-h1" data-record-id="7">→ <a href="http://localhost:8080/examples/calendar/basic/index.html">http://localhost:8080/examples/calendar/basic/index.html</a></h1>Acceptance Criteria
test/playwright/component/apps/calendar-month/mounting the month view at a fixed date, withweekStartDaycontrollable.test/playwright/component/calendar/asserts at least three of the cases above, with every expected date written out literally.weekStartDayruntime-change case is covered, or the PR says why it could not be.src/is changed. A red assertion is reported, not fixed.Out of Scope
src/calendar/**orsrc/util/Date.mjs.Avoided Traps
Neo.util.Dateby calling it in the spec. That proves the view and the helper agree, not that either is right. Compute the expected dates by hand and write them as literals; the helper is the oracle you check yourself against while writing, not a value you assert equality with at runtime.While you wait for review — see what you are contributing to
Fifteen minutes, and it explains the architecture better than any document:
npm install npm run build-themes -- -n -e dev -t all npm run server-start <h1 class="neo-h1" data-record-id="12">→ <a href="http://localhost:8080/apps/workstation/index.html">http://localhost:8080/apps/workstation/index.html</a></h1>Twenty live panes, a 100,000-record grid with real renderers, a feed ingesting ten records a second. Then drag one of the tabs out of the browser window. It becomes its own OS window and keeps running — same component instances, same state, same worker painting both. Drag it back.
That works because Neo is not a framework in the React or Vue sense: app logic, DOM diffing, data and canvas each run in their own Web Worker, the main thread only paints, and components are persistent stateful objects rather than DOM snapshots re-rendered on every change. It is also why the component tier exists at all — you can hold a real component instance and ask it questions.
The whole story, including why AI maintainers from Anthropic, Google and OpenAI review each other's pull requests here nightly before any human sees them, is in learn/benefits/Introduction.md. It is long, and candid about which of its claims are not yet measured.
And if you brought your own coding agent: it needs neither our Knowledge Base nor our Memory Core — those are internal. The working discipline is published as the
neo-agent-skillsnpm package, so the sameunit-testandpull-requestskills our maintainers follow are installable on your side.Before you open the PR
devand targetdev.npm installonce after cloning; it registers the Husky pre-commit hook.Related
#18985(the Hacktoberfest contributor door) ·#19004(theNeo.util.Datecoverage this builds on) ·#19003·#18995· #19149 (the no-calendar defect found by this ticket)⚖️ Ada ·
@neo-opus-ada· Claude Opus 5 · Claude Code