From b2207a461102f00f396fe7afde4ca9ed67d8ba62 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 19:17:47 +0000 Subject: [PATCH] fix(plugin-calendar): parse the authored ISO currentDate into a Date at the renderer boundary (#4452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `plugin-calendar:calendar-view` declares `currentDate` as `type: 'string'` ("ISO date string for initial calendar date") while `CalendarViewProps.currentDate` is a `Date`. Nothing converted between them: the authored string rode the trailing `{...props}` spread into `useState`'s initial `selectedDate`, and the header's `selectedDate.toLocaleDateString(…)` threw `selectedDate.toLocaleDateString is not a function`. The one spelling the input documents was the one spelling that could not work. The renderer now owes the conversion at its own boundary: `currentDate` is destructured out so the spread cannot carry the raw value (the #4433 consumed-key pattern), parsed once per authored value, and handed to `CalendarView` as the `Date` its prop type declares. Off-spec input gets the same answer as an absent key — the component default — and an `Invalid Date` is never manufactured and passed on. A `Date` instance passes through untouched. The parse is memoised on the authored value, because `CalendarView` re-seeds its `selectedDate` from this prop in an effect keyed on the prop's identity. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --- ...ndar-view-authored-currentdate-iso-4452.md | 11 + ...alendar-view-renderer.currentDate.test.tsx | 344 ++++++++++++++++++ .../src/calendar-view-renderer.tsx | 57 +++ 3 files changed, 412 insertions(+) create mode 100644 .changeset/calendar-view-authored-currentdate-iso-4452.md create mode 100644 packages/plugin-calendar/src/calendar-view-renderer.currentDate.test.tsx diff --git a/.changeset/calendar-view-authored-currentdate-iso-4452.md b/.changeset/calendar-view-authored-currentdate-iso-4452.md new file mode 100644 index 000000000..e663ed599 --- /dev/null +++ b/.changeset/calendar-view-authored-currentdate-iso-4452.md @@ -0,0 +1,11 @@ +--- +'@object-ui/plugin-calendar': patch +--- + +authored ISO `currentDate` reaches the calendar as a `Date`; unparseable input falls back to the default instead of crashing + +`plugin-calendar:calendar-view` declares the input `{ name: 'currentDate', type: 'string', description: 'ISO date string for initial calendar date' }`, while `CalendarViewProps.currentDate` is a `Date`. Nothing converted between the two: the authored string rode the renderer's trailing `{...props}` spread into `useState`'s initial `selectedDate`, and the header's `selectedDate.toLocaleDateString(…)` threw `selectedDate.toLocaleDateString is not a function` — the error boundary instead of the calendar. Writing the one spelling the input documents was the one spelling that could not work, and there was no correct authored value at all, since `type: 'string'` cannot express a `Date`. + +The renderer now owes the conversion, at its own boundary. `currentDate` is destructured out of the incoming props so the spread can no longer carry the raw value (the consumed-key pattern from the `events` collision fix), parsed once per authored value, and passed to `CalendarView` as the `Date` its prop type declares. Off-spec input — an unparseable string, or any non-string that is not already a `Date` — gets the same answer as an absent key: the component's own default date. An `Invalid Date` is never manufactured and handed on; it does not throw, it renders the literal text "Invalid Date" into the header and the date picker, which is a silent wrong answer where the default is a usable calendar. + +A `Date` instance passes through untouched, so a React host handing the widget its real declared prop type is unaffected. diff --git a/packages/plugin-calendar/src/calendar-view-renderer.currentDate.test.tsx b/packages/plugin-calendar/src/calendar-view-renderer.currentDate.test.tsx new file mode 100644 index 000000000..f7589dcf0 --- /dev/null +++ b/packages/plugin-calendar/src/calendar-view-renderer.currentDate.test.tsx @@ -0,0 +1,344 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `plugin-calendar:calendar-view` — the declared `currentDate` input never + * converted (objectui#4452). + * + * The registry declares the input as + * `{ name: 'currentDate', type: 'string', description: 'ISO date string for + * initial calendar date' }`, while `CalendarViewProps.currentDate` is a `Date`. + * The renderer converted nothing: the authored string rode the trailing + * `{...props}` spread into `CalendarView`, became `useState`'s initial + * `selectedDate`, and the header called `selectedDate.toLocaleDateString(…)` on + * a string — `selectedDate.toLocaleDateString is not a function`, the error + * boundary instead of the calendar. + * + * This is the mirror of objectui#4433's collision, which the sibling file pins. + * There an UNDOCUMENTED authored key overwrote a computed prop; here the + * component documents the key, its type AND its format, and writing the one + * spelling an author is told to write is what breaks it. There was no correct + * authored value at all: `type: 'string'` cannot express a `Date`. + * + * The fix converts at the renderer boundary and destructures the key out of the + * spread (the objectui#4433 consumed-key pattern), so `CalendarView` only ever + * receives the `Date` its prop type declares. + * + * ## Two dates, deliberately + * + * The first case authors the card's verbatim repro string, + * `2026-08-12T08:00:00.000Z`. It pins the crash — and ONLY the crash: that + * instant is in the month this card was filed in, so a fix that quietly dropped + * the authored date to "today" would still satisfy it. The honouring assertions + * therefore use {@link farFutureDate}, 14 months out, where "the authored date + * reached the calendar" and "the calendar fell back to today" are visibly + * different renders. + * + * ## Why the assertions read year DIGITS, not a month name + * + * `CalendarView` formats its header with `toLocaleDateString(locale, …)` under + * the ambient i18n language, so pinning "October 2027" would pin the test to a + * locale rather than to the date. 14 months is more than a year, so the + * authored month is always in a DIFFERENT calendar year than today: the year + * digits alone separate honoured from dropped, in any Gregorian locale. The + * event-visibility assertion is the locale-free half of the same fact — a month + * grid only renders the events of the month it is showing. + */ + +import { describe, it, expect, vi } from 'vitest'; +import React from 'react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { SchemaRenderer } from '@object-ui/react'; +// Module scope: the registration side effect this file renders through +// (AGENTS.md 测试纪律 — never inside a hook). +import './index'; + +/** The text `SchemaErrorBoundary` renders when a widget throws. */ +const ERROR_BOUNDARY_MARKER = 'failed to render'; + +/** The card's verbatim repro value. */ +const ISSUE_REPRO_ISO = '2026-08-12T08:00:00.000Z'; + +/** What a `Date`-typed header renders when it is handed an unparseable value. */ +const INVALID_DATE_MARKER = 'Invalid Date'; + +/** + * A date 14 months from today at local noon on the 15th. + * + * 14 > 12, so this is always in a different calendar YEAR than today — the + * property the header assertions rest on. The 15th keeps it in the middle of + * the month grid, well away from the leading/trailing spill days a month view + * borrows from its neighbours. + */ +function farFutureDate(): Date { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth() + 14, 15, 12, 0, 0, 0); +} + +function calendarRegion(): Element | null { + return document.body.querySelector('[role="region"][aria-label="Calendar"]'); +} + +/** + * The header's date label. `CalendarView` renders it into the popover + * trigger's accessible name (`Current date: