From e25327f003101cebef36514bc115ddd1c942eb58 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:46:35 +0000 Subject: [PATCH 1/3] fix(examples): author the four alert-dialog fixtures in the dialect the renderer reads (objectui#7693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four `components-overlay-alert-dialog/*` fixtures authored an `actions` array of two button nodes. Nothing carries that key: the renderer (`packages/components/src/renderers/overlay/alert-dialog.tsx:37-38`) draws `AlertDialogCancel` ONLY from `schema.cancelText` and `AlertDialogAction` ONLY from `schema.actionText`, and `schema.actions` has zero read sites in the repo. `AlertDialogSchema` and its zod twin do not declare it either. So the four demos the docs page embeds rendered a dialog with an EMPTY footer — the one thing an alert dialog exists for was missing from every one of its examples. Nothing was red because `BaseSchema` is `.passthrough()` (`packages/types/src/zod/base.zod.ts:241`): `actions` rode through `safeValidateSchema` unvalidated, and no gate parses a JSON fixture as a schema. Measured on this base, the keys `AlertDialogSchema` declares AND the renderer reads: `type`, `title`, `description`, `content`, `trigger`, `defaultOpen`, `open`, `cancelText`, `actionText`, `className` (plus the runtime slots `onAction` / `onOpenChange`, which have no JSON spelling and are refused by name). `cancelLabel`, `confirmLabel` and `confirmVariant` are still DECLARED but read by nothing, so no fixture is moved onto them. Conversion, per fixture: `actions[0]` -> `cancelText`, `actions[1]` -> `actionText`. The per-button `variant` is DROPPED — the read dialect has no spelling for it, and the footer's styling is fixed by the primitives (`AlertDialogCancel` is always `buttonVariants({ variant: 'outline' })`, `AlertDialogAction` always the default). Giving `confirmVariant` a read site is a renderer + types change on the manual floor and is deliberately not ridden along here. The pin, `test/alert-dialog-footer-read-dialect-7693.test.tsx`, has two halves, each with a live control: a DOM half that renders every fixture through the real registry and requires at least one footer button, and a fixture-key half that walks the WHOLE catalog and requires no `alert-dialog` node anywhere to carry `actions`. Both went red on this base (4 + 1 failures) while both controls stayed green, so the red is a reading about the spelling and not about the harness. `form-control-dom-leak-5632.test.tsx`'s `NODE_CENSUS` moves `button` 126 -> 118: the eight action-array button nodes are now two strings, so they leave a `type`-keyed walk. That table's own header sanctions the update for a catalog-authored diff that touches no renderer, which this one is. Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M Co-authored-by: Claude --- .../basic-alert-dialog.json | 14 +- .../confirmation-dialog.json | 14 +- .../custom-actions.json | 14 +- .../destructive-action.json | 13 +- ...t-dialog-footer-read-dialect-7693.test.tsx | 180 ++++++++++++++++++ .../test/form-control-dom-leak-5632.test.tsx | 10 +- 6 files changed, 197 insertions(+), 48 deletions(-) create mode 100644 examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json index a273a24973..ebc687d7da 100644 --- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json +++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json @@ -6,16 +6,6 @@ "type": "button", "label": "Delete Account" }, - "actions": [ - { - "type": "button", - "label": "Cancel", - "variant": "outline" - }, - { - "type": "button", - "label": "Continue", - "variant": "destructive" - } - ] + "cancelText": "Cancel", + "actionText": "Continue" } diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json index ad076ac6dd..ff9eb7800e 100644 --- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json +++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json @@ -7,16 +7,6 @@ "label": "Close", "variant": "outline" }, - "actions": [ - { - "type": "button", - "label": "Don't Save", - "variant": "ghost" - }, - { - "type": "button", - "label": "Save", - "variant": "default" - } - ] + "cancelText": "Don't Save", + "actionText": "Save" } diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json index 3e660d74cf..2d56f44da2 100644 --- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json +++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json @@ -7,16 +7,6 @@ "label": "Proceed", "variant": "default" }, - "actions": [ - { - "type": "button", - "label": "Go Back", - "variant": "outline" - }, - { - "type": "button", - "label": "Yes, Continue", - "variant": "default" - } - ] + "cancelText": "Go Back", + "actionText": "Yes, Continue" } diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json index c461f14391..cfd178b206 100644 --- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json +++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json @@ -7,15 +7,6 @@ "label": "Delete", "variant": "destructive" }, - "actions": [ - { - "type": "button", - "label": "Cancel" - }, - { - "type": "button", - "label": "Delete", - "variant": "destructive" - } - ] + "cancelText": "Cancel", + "actionText": "Delete" } diff --git a/examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx b/examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx new file mode 100644 index 0000000000..80993b2446 --- /dev/null +++ b/examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx @@ -0,0 +1,180 @@ +/** + * 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. + */ + +/** + * objectui#7693 — the four `alert-dialog` catalog fixtures authored an + * `actions` array that no surface carries, so the docs page's own examples + * rendered an EMPTY footer. + * + * ## Why nothing was red before this file + * + * `BaseSchema` is `.passthrough()` (`packages/types/src/zod/base.zod.ts:241`), + * so `actions` rode through `safeValidateSchema` unvalidated and every gate + * stayed green while the rendered dialog had no buttons at all. The renderer + * (`packages/components/src/renderers/overlay/alert-dialog.tsx`) draws + * `AlertDialogCancel` ONLY from `schema.cancelText` and `AlertDialogAction` + * ONLY from `schema.actionText`; `schema.actions` has zero read sites. + * + * ## The two pins, and why both + * + * The DOM pin is the one that names the SYMPTOM the card reported (an empty + * footer), and it is the one that would have gone red on `origin/main`: all + * four fixtures drew zero footer buttons. The fixture-key pin is the cheap + * companion that closes the CLASS — it walks every catalog entry, not just the + * four, so a future `alert-dialog` node re-authored under `actions` reds here + * even if nobody adds a render assertion for it. + * + * Each pin is paired with a live control, because a render that draws nothing + * satisfies a "no `actions` key" assertion trivially and a harness that renders + * nothing at all satisfies both: + * + * - `RENDERS_IN_THE_READ_DIALECT` — an inline node in the read dialect MUST + * draw two footer buttons. If the harness cannot see a footer button, this + * control reds first and the fixture rows below mean nothing. + * - `THE_PRE_REPAIR_SHAPE_DRAWS_NOTHING` — the same node authored the way the + * fixtures were authored (an `actions` array, no label keys) MUST draw zero + * footer buttons, and MUST still validate. That is the defect reproduced in + * one place, and it is what makes the fixture rows a measurement of the + * SPELLING rather than of some unrelated render failure. + * + * ⛔ Do NOT "fix" a red row here by declaring `actions` on `AlertDialogSchema`. + * Making a variant-carrying action list live is a renderer + types feature on + * the manual floor, explicitly ruled out of this card's scope; the fixtures + * follow the dialect the renderer READS. + */ +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import '@object-ui/components'; +import { SchemaRenderer, toRenderableSchema } from '@object-ui/react'; +import { safeValidateSchema } from '@object-ui/types/zod'; +import { allExamples, getExample } from '../src/index.js'; + +/** The four entries the docs page embeds, in the order it embeds them. */ +const FIXTURE_IDS = [ + 'components-overlay-alert-dialog/basic-alert-dialog', + 'components-overlay-alert-dialog/destructive-action', + 'components-overlay-alert-dialog/confirmation-dialog', + 'components-overlay-alert-dialog/custom-actions', +] as const; + +/** + * Render one entry the way the docs gallery does, forced OPEN. + * + * `defaultOpen` is the only thing the test adds: the fixtures are authored + * closed (that is what the docs page wants — a trigger you click), and a closed + * Radix dialog mounts no content, so the footer could not be measured at all. + * Everything the assertions read comes from the fixture's own keys. + */ +function footerButtons(schema: unknown): string[] { + render( + , + ); + // Radix portals the content to document.body, so the RTL container is empty + // by construction — query the dialog itself. An `alert-dialog` has no close + // affordance of its own, so every button inside the content IS a footer + // button (the trigger stays behind, in the container). + const content = document.body.querySelector('[role="alertdialog"]'); + if (!content) return []; + return Array.from(content.querySelectorAll('button')).map((b) => b.textContent ?? ''); +} + +/** Report the issues rather than `false`, so a red run says what broke. */ +function reasons(schema: unknown): string[] { + const r = safeValidateSchema(schema); + return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`); +} + +const READ_DIALECT_NODE = { + type: 'alert-dialog', + title: 'Are you sure?', + trigger: { type: 'button', label: 'Open' }, + cancelText: 'Cancel', + actionText: 'Continue', +}; + +const PRE_REPAIR_NODE = { + type: 'alert-dialog', + title: 'Are you sure?', + trigger: { type: 'button', label: 'Open' }, + actions: [ + { type: 'button', label: 'Cancel', variant: 'outline' }, + { type: 'button', label: 'Continue', variant: 'destructive' }, + ], +}; + +describe('objectui#7693 — controls: the harness can tell a drawn footer from an empty one', () => { + it('RENDERS_IN_THE_READ_DIALECT: `cancelText` + `actionText` draw two footer buttons', () => { + expect(footerButtons(READ_DIALECT_NODE)).toEqual(['Cancel', 'Continue']); + }); + + it('THE_PRE_REPAIR_SHAPE_DRAWS_NOTHING: an `actions` array draws an empty footer', () => { + // The defect, reproduced in one place. `actions` is not refused — it is + // simply never read, which is why every gate stayed green. + expect(footerButtons(PRE_REPAIR_NODE)).toEqual([]); + expect(reasons(PRE_REPAIR_NODE)).toEqual([]); + }); +}); + +describe('objectui#7693 — every alert-dialog fixture renders a real footer', () => { + it.each(FIXTURE_IDS)('%s draws at least one footer button', (id) => { + const labels = footerButtons(getExample(id).schema); + expect(labels.length).toBeGreaterThanOrEqual(1); + // ...and each one carries text, so a button rendered from an empty label + // cannot satisfy the count. + for (const label of labels) expect(label.trim().length).toBeGreaterThan(0); + }); + + it.each(FIXTURE_IDS)('%s still validates under safeValidateSchema', (id) => { + expect(reasons(getExample(id).schema)).toEqual([]); + }); +}); + +describe('objectui#7693 — no alert-dialog node in the catalog authors `actions`', () => { + /** Every object in an example's schema tree, the example's id carried along. */ + function* nodes(value: unknown, id: string): Generator<[string, Record]> { + if (Array.isArray(value)) { + for (const item of value) yield* nodes(item, id); + } else if (value !== null && typeof value === 'object') { + yield [id, value as Record]; + for (const child of Object.values(value as Record)) yield* nodes(child, id); + } + } + + it('the whole catalog is clean, not just the four the docs page embeds', () => { + const offenders: string[] = []; + for (const example of allExamples()) { + for (const [id, node] of nodes(example.schema, example.id)) { + if (node.type === 'alert-dialog' && 'actions' in node) offenders.push(id); + } + } + expect(offenders).toEqual([]); + }); + + it('anti-vacuity: the walk really does reach alert-dialog nodes', () => { + // Without this, the assertion above passes just as well when the walk is + // broken, or when the catalog holds no alert-dialog at all. + const seen = new Set(); + for (const example of allExamples()) { + for (const [id, node] of nodes(example.schema, example.id)) { + if (node.type === 'alert-dialog') seen.add(id); + } + } + expect([...seen].sort()).toEqual([...FIXTURE_IDS].sort()); + }); + + it('anti-vacuity: the offender test would catch one', () => { + // The same predicate, over the pre-repair shape, must report it. + const offenders: string[] = []; + for (const [id, node] of nodes(PRE_REPAIR_NODE, 'inline/pre-repair')) { + if (node.type === 'alert-dialog' && 'actions' in node) offenders.push(id); + } + expect(offenders).toEqual(['inline/pre-repair']); + }); +}); diff --git a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx index 1d05d12765..11027573d9 100644 --- a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx +++ b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx @@ -121,7 +121,15 @@ const NODE_CENSUS: Readonly 118 with objectui#7693: the four `components-overlay-alert-dialog/*` + // fixtures each authored two `type: 'button'` nodes under an `actions` array + // the `alert-dialog` renderer never reads, so those eight nodes drew nothing + // and the demos showed an empty footer. They are now the two label keys the + // renderer DOES read (`cancelText` / `actionText`), which are strings rather + // than nodes and so leave this walk. Catalog-authored, no renderer touched — + // the case this table's header sanctions. + button: { rendered: 118, noElement: 0 }, input: { rendered: 48, noElement: 0 }, checkbox: { rendered: 12, noElement: 0 }, switch: { rendered: 7, noElement: 0 }, From 9ce6857ec8e17b5f7873ddb13728c4cc73e02c19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 05:06:56 +0000 Subject: [PATCH 2/3] test(types): re-derive the alert-dialog fixture premise pin onto the read dialect (objectui#7693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `alert-dialog-read-dialect-7104.test.ts` pinned the FILED PREMISE of objectui#7693 — that all four schema-catalog fixtures author `actions` and neither label key — precisely so the card's fix would have to come back here and re-derive it. Converting the fixtures did exactly that: the four membership legs went red on CI shard 4/4. This is the other side of that flip. The file's own header prescribes re-derivation, not deletion, so the block is inverted rather than dropped, and the header bullet that listed this premise as UNRESOLVED is rewritten to record that it resolved and how. What the re-derived block asserts is the TYPES-side reading, not a second copy of the catalog-side one: - each fixture authors `cancelText` and `actionText` as strings and no `actions` array; - the UNION of every key the four fixtures author is a subset of `AlertDialogZod.shape` — membership in the mirror, which is the question `.success` cannot answer because `BaseSchema` is `.passthrough()`, and which is not askable from the catalog package at all; - two live controls: that `actions` is genuinely absent from `shape` (so the membership leg is not a tautology), and that the pre-repair document STILL parses green (so the reason nothing red covered this card stays a live reading rather than a historical note). The render-side half — footer buttons actually drawn, and a catalog-wide walk for `actions` on any alert-dialog node — stays with the catalog in `examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx`. Neither package can host both: this one cannot see a renderer, that one cannot see the mirror's shape. No face change: `packages/types/src/overlay.ts` and `packages/types/src/zod/overlay.zod.ts` are untouched. Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M Co-authored-by: Claude --- .../alert-dialog-read-dialect-7104.test.ts | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts index 73b9762f56..6817845ca6 100644 --- a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts +++ b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts @@ -55,10 +55,18 @@ * and its own changeset grade (the objectui#7104 ruling); the pins below * record today's state so that the PR which retires them re-derives these * lines deliberately rather than passing unnoticed. - * - The four schema-catalog fixtures author `actions`, a key no surface - * carries, so the docs page's own examples render an empty footer — - * objectui#7693. Pinned here as that card's filed premise; its fix goes red - * here and re-derives the pin. + * - RESOLVED, and re-derived rather than deleted: the four schema-catalog + * fixtures used to author `actions`, a key no surface carries, so the docs + * page's own examples rendered an empty footer — objectui#7693. That card + * landed and this file went red exactly as predicted above, on all four + * membership legs. The block at the bottom is the SAME pin re-derived onto + * the other side of the flip: the fixtures now author the read dialect, and + * what it asserts is the TYPES-side reading (every key they author is a + * member of the mirror's shape), not the render-side one. The render-side + * half lives with the catalog, in + * `examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx` + * — deliberately not duplicated here, because this package cannot see a + * renderer and that one cannot see the mirror's shape. */ import { describe, expect, it } from 'vitest'; @@ -318,17 +326,52 @@ describe('the docs page publishes the read dialect (objectui#7104)', () => { }); }); -describe('the schema-catalog fixtures still author `actions` — objectui#7693, pinned as its filed premise', () => { - it.each(FIXTURES)('%s.json carries an `actions` array and neither `cancelText` nor `actionText`', (name) => { - const fixture = JSON.parse(read(`${FIXTURE_DIR}/${name}.json`)) as Record; - expect(Array.isArray(fixture.actions)).toBe(true); - expect(fixture).not.toHaveProperty('cancelText'); - expect(fixture).not.toHaveProperty('actionText'); +describe('the schema-catalog fixtures author the READ dialect — objectui#7693, the premise pin re-derived', () => { + const fixture = (name: string): Record => + JSON.parse(read(`${FIXTURE_DIR}/${name}.json`)) as Record; + + it.each(FIXTURES)('%s.json authors `cancelText` and `actionText` and no `actions` array', (name) => { + const json = fixture(name); + expect(json).not.toHaveProperty('actions'); + expect(typeof json.cancelText).toBe('string'); + expect(typeof json.actionText).toBe('string'); + }); + + it('every key any fixture authors is a MEMBER of the mirror — the reading passthrough used to hide', () => { + // The types-side half of objectui#7693, and the only half this package can + // measure. `BaseSchema` is `.passthrough()`, so `safeParse` says nothing + // about whether an authored key is carried by anything; membership in the + // mirror's own shape is the question, and it is asked here rather than in + // the catalog because `AlertDialogZod.shape` is not visible from there. + const authored = new Set(FIXTURES.flatMap((name) => Object.keys(fixture(name)))); + const undeclared = [...authored].filter((key) => !(key in shape)).sort(); + expect(undeclared).toEqual([]); + }); + + it('control: `actions` is not a member, so the leg above really would catch it', () => { + // Without this, the assertion above passes just as well against a mirror + // that declares everything, and the four fixture legs read as a tautology. + expect('actions' in shape).toBe(false); + expect(['type', 'title', 'trigger', 'cancelText', 'actionText'].every((k) => k in shape)).toBe(true); + }); + + it('control: the pre-repair shape STILL parses green — passthrough did not change', () => { + // Why nothing red covered objectui#7693 before it landed, kept as a live + // reading rather than as history: re-authoring a fixture under `actions` + // would sail through `safeParse` again, which is why the membership leg + // above is the guard and `.success` is not. + const preRepair = { + type: 'alert-dialog', + title: 'Are you sure?', + trigger: { type: 'button', label: 'Open' }, + actions: [{ type: 'button', label: 'Cancel' }, { type: 'button', label: 'Continue' }], + }; + expect(AlertDialogZod.safeParse(preRepair).success).toBe(true); }); - it('and every one of them parses GREEN regardless — passthrough is why nothing red covers objectui#7693', () => { + it('and every one of them parses GREEN — now with the keys the renderer reads', () => { for (const name of FIXTURES) { - expect(AlertDialogZod.safeParse(JSON.parse(read(`${FIXTURE_DIR}/${name}.json`))).success, name).toBe(true); + expect(AlertDialogZod.safeParse(fixture(name)).success, name).toBe(true); } }); }); From c070119cd4397c40862af4135a66318bb9e20c22 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 05:10:47 +0000 Subject: [PATCH 3/3] chore(changeset): declare the types-side pin flip as releasing nothing (objectui#7693) Editing any file under a released package's `src/` owes a changeset, and the re-derived premise pin lives at `packages/types/src/__tests__/`. The change publishes nothing: the declaration and its zod twin are untouched and the only edited file is a test, so this is the empty-frontmatter form the gate names as its explicit exemption rather than a version bump. Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M Co-authored-by: Claude --- .changeset/alert-dialog-fixture-premise-pin-7693.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/alert-dialog-fixture-premise-pin-7693.md diff --git a/.changeset/alert-dialog-fixture-premise-pin-7693.md b/.changeset/alert-dialog-fixture-premise-pin-7693.md new file mode 100644 index 0000000000..82d6073136 --- /dev/null +++ b/.changeset/alert-dialog-fixture-premise-pin-7693.md @@ -0,0 +1,4 @@ +--- +--- + +Test-only change in `@object-ui/types`: the pin that recorded objectui#7693's filed premise (the four alert-dialog schema-catalog fixtures authoring an `actions` array) is re-derived onto the read dialect now that the card has landed. No published behaviour changes — `overlay.ts` and `zod/overlay.zod.ts` are untouched, and the only file edited under `src/` is a test.