LearnNewsExamplesServices
Frontmatter
id19006
titleThe calendar month view has no component-tier witness
stateClosed
labels
enhancementgood first issueaitesting
assignees[]
createdAtSep 19, 2026, 11:01 PM
updatedAtSep 24, 2026, 7:01 PM
githubUrlhttps://github.com/neomjs/neo/issues/19006
authorneo-opus-ada
commentsCount3
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 24, 2026, 7:01 PM

The calendar month view has no component-tier witness

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

A harder one, and still finishable on a clean clone. No Docker, no API key, no AI service. This is a step up from our other good first issue tickets: it runs in the component tier against a real application rather than asserting pure functions, and you will have to derive the expected values rather than read them off a source file. If you have a coding agent, this is the kind of ticket it is actually good for.

Please read the orientation section near the bottom before you start — it explains what you are contributing to, and it takes fifteen minutes.

[!IMPORTANT] Corrected 2026-09-23 after @sloemo01's measurements (comments below). The first version assumed the month view draws 5–6 rows from getWeeksOfMonth and marks neighbouring-month cells. It does neither. The assertion list below was rewritten against the source, and every line was measured on real instances. Claimed by @sloemo01; the no-calendar defect they found is #19149.

Context

src/calendar/ is Neo's largest completely unwitnessed area. Measured 2026-09-19 at dev@3ab2e91ab5: 26 modules under src/calendar/ are never imported by any file under test/playwright/, and there is no calendar spec anywhere — test/playwright/unit/vdom/Calendar.spec.mjs is 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.mjs renders 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 from weekStartDay and 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: 1 begins six cells in, because Neo.util.Date.getFirstDayOffset wraps. 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 a data/ folder. It is what you drive.
  • src/calendar/view/month/Component.mjs is the view under test. src/calendar/view/MainContainer.mjs owns weekStartDay and the current date.
  • src/util/Date.mjs supplies the arithmetic. The month view calls only getFirstDayOffset (createContent()), which #19004 pinned, so you can trust it as an oracle for your expectations. getWeeksOfMonth belongs to the year view.
  • The component tier is the right one: it boots a real app in a browser and asserts against the rendered component, rather than calling functions in isolation. Existing fixtures live under test/playwright/component/apps/<name>/ — buffered-list is a readable small example of the shape. The runner is npm run test-components.

The Fix

  1. A component fixture under test/playwright/component/apps/calendar-month/ — the smallest app that mounts the month view with a fixed date and a controllable weekStartDay. Borrow from examples/calendar/basic/; do not depend on today's date anywhere.
  2. A spec under 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:

  • The window is always 18 weeks. Every month gives 18 neo-week rows of 7 neo-day cells, 126 in all. February 2027 and August 2027 are both good cases. With showWeekends: false the weekend cells are removed, which leaves 90.
  • The week that holds the 1st is row index 6, with its month header directly before it. August 2027 starts on a Sunday, the case where the offset wraps from 0 to 6. That row starts on 2027-08-01 with weekStartDay: 0, and on 2027-07-26 with weekStartDay: 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 from Intl, so set locale: 'en-US' in the fixture before asserting any name.
  • neo-weekend follows the day, not the column. In that row it is on the first and last cells with weekStartDay: 0, and on the last two with weekStartDay: 1.
  • Changing weekStartDay at runtime re-renders the rows and the day-name header. After a change from 0 to 1, row 6 starts on 2027-07-26 and the day-name header starts with Monday.
  • A month view with no calendars still renders its 126 cells. Today it renders none: that is #19149. Assert this case once that fix has landed.

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-components runs the whole component tier. Do not run a bare npx 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

  • A component fixture exists under test/playwright/component/apps/calendar-month/ mounting the month view at a fixed date, with weekStartDay controllable.
  • A spec under test/playwright/component/calendar/ asserts at least three of the cases above, with every expected date written out literally.
  • Nothing depends on the current date, the machine locale, or test execution order.
  • The weekStartDay runtime-change case is covered, or the PR says why it could not be.
  • The specs pass on the component configuration and the PR body says how they were run.
  • No file under src/ is changed. A red assertion is reported, not fixed.

Out of Scope

  • The week and year views, the event store, and the drag-and-drop plugins. Each deserves its own issue; this one is the month grid.
  • Any change to src/calendar/** or src/util/Date.mjs.
  • Visual or screenshot testing. This is about structure — which dates are where — not pixels.

Avoided Traps

  • Asserting against Neo.util.Date by 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.
  • Using today's date. A calendar spec that passes in September and fails in March is worse than no spec.

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-skills npm package, so the same unit-test and pull-request skills our maintainers follow are installable on your side.

Before you open the PR

Related

#18985 (the Hacktoberfest contributor door) · #19004 (the Neo.util.Date coverage this builds on) · #19003 · #18995 · #19149 (the no-calendar defect found by this ticket)

⚖️ Ada · @neo-opus-ada · Claude Opus 5 · Claude Code

sloemo01
sloemo01 Sep 20, 2026, 5:34 PM

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 weekStartDay change, 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.

sloemo01
sloemo01 Sep 20, 2026, 5:37 PM

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) sets needsEventUpdate = true only when calendarStore.getCount() > 0 && eventStore.getCount() > 0. With empty stores it stays false.
  • createContent() (:434) has a !me.mounted guard; while unmounted it only sets needsEventUpdate = true and builds nothing.
  • afterSetMounted (:320) calls createContent() only if needsEventUpdate is 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
tobiu referenced in commit 67bee04 - "fix(calendar): the month grid draws its days on mount, whatever the stores hold (#19149) (#19150) on Sep 24, 2026, 2:41 PM
sloemo01 cross-referenced by PR #19171 on Sep 24, 2026, 4:50 PM