From 8c093f6b56cf2a46722820ab313c813e12be603c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:19:42 +0000 Subject: [PATCH] docs(aspect-ratio): author the keys the renderer reads in all five demos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every shipped `components-layout-aspect-ratio` demo authored `content`, which `packages/components/src/renderers/layout/aspect-ratio.tsx` never reads: it reads `ratio`, `className`, `image` (with `alt`) and `children || body`. Measured on c6732825d by rendering each entry through the real SchemaRenderer the way the docs gallery does — 3 elements, empty text, zero images for all five: the Radix wrapper for the ratio itself and nothing inside it. The photo demo now authors `image` + `alt`, the renderer's own declared inputs, whose img element is sized to fill the box. The four card demos author `children` at BOTH levels — the nested `card` renderer reads `children || body`, `title`, `description`, `header` and `footer`, never `content`, so moving only the outer key would have traded an empty box for an empty card. The page's Schema block published `content` as contract while omitting `image`/`alt`; it now documents the keys the renderer reads. Adds `examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx`: category scope rather than a list of five ids, with a counter-probe that renders the pre-fix shape and proves the assertion can still fail. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 --- .changeset/6773-aspect-ratio-demo-content.md | 21 ++ .../docs/components/layout/aspect-ratio.mdx | 10 +- .../16-9-aspect-ratio.json | 7 +- .../components-layout-aspect-ratio/4-3.json | 5 +- .../square.json | 5 +- .../ultrawide.json | 5 +- .../video-aspect-ratio.json | 6 +- .../aspect-ratio-demo-content-6773.test.tsx | 186 ++++++++++++++++++ 8 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 .changeset/6773-aspect-ratio-demo-content.md create mode 100644 examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx diff --git a/.changeset/6773-aspect-ratio-demo-content.md b/.changeset/6773-aspect-ratio-demo-content.md new file mode 100644 index 000000000..67468c550 --- /dev/null +++ b/.changeset/6773-aspect-ratio-demo-content.md @@ -0,0 +1,21 @@ +--- +--- + +The five `aspect-ratio` docs demos now author the keys the renderer reads, so every demo on +the published page draws its content instead of an empty ratio box (objectui#6773). + +`packages/components/src/renderers/layout/aspect-ratio.tsx` reads `ratio`, `className`, +`image` (with `alt`) and `children || body` — never `content`, which is what all five +entries authored and nothing else. Measured on `c6732825d`, each rendered 3 elements, no +text and no `img`: the Radix wrapper for the ratio itself, with nothing inside it. The +photo demo now authors `image` + `alt`, the renderer's own declared inputs, whose `` +is sized to fill the box; the four card demos author `children` at BOTH levels — the +nested `card` never read `content` either, so moving only the outer key would have traded +an empty box for an empty card. The page's Schema block published `content` as contract +while omitting `image`/`alt`; it now documents what the renderer reads. + +Nothing publishes from this change — a docs page plus `@object-ui/example-schema-catalog` +fixtures, both outside the release — hence the empty frontmatter. The regression control is +`examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx`: category scope, not +a list of five ids, with a counter-probe that renders the pre-fix shape and proves the +assertion can still fail. diff --git a/content/docs/components/layout/aspect-ratio.mdx b/content/docs/components/layout/aspect-ratio.mdx index 4ba8ec3d5..8e798d76d 100644 --- a/content/docs/components/layout/aspect-ratio.mdx +++ b/content/docs/components/layout/aspect-ratio.mdx @@ -21,11 +21,17 @@ The Aspect Ratio component maintains a consistent width to height ratio for cont ## Schema +The box holds its content one of two ways, and the renderer reads exactly one of them: +set `image` for a photo, which is drawn to fill the box; otherwise the box renders +`children`. + ```plaintext interface AspectRatioSchema { type: 'aspect-ratio'; - ratio: number; // Width / Height ratio - content: ComponentSchema; // Content to display + ratio?: number; // Width / Height ratio (default: 16 / 9) + image?: string; // Image URL, drawn to fill the box + alt?: string; // Alt text for `image` + children?: SchemaNode | SchemaNode[]; // Content, when no `image` is set className?: string; } ``` diff --git a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/16-9-aspect-ratio.json b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/16-9-aspect-ratio.json index f9ce39057..6fee34949 100644 --- a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/16-9-aspect-ratio.json +++ b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/16-9-aspect-ratio.json @@ -1,9 +1,6 @@ { "type": "aspect-ratio", "ratio": 1.7777777777777777, - "content": { - "type": "image", - "src": "https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd", - "alt": "Photo" - } + "image": "https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd", + "alt": "Photo" } diff --git a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/4-3.json b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/4-3.json index 000a066cb..d02ff1594 100644 --- a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/4-3.json +++ b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/4-3.json @@ -1,8 +1,9 @@ { "type": "aspect-ratio", "ratio": 1.3333333333333333, - "content": { + "children": { "type": "card", - "content": "4:3 Ratio" + "className": "flex h-full items-center justify-center", + "children": "4:3 Ratio" } } diff --git a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/square.json b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/square.json index 1bf7a0ade..08807c616 100644 --- a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/square.json +++ b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/square.json @@ -1,8 +1,9 @@ { "type": "aspect-ratio", "ratio": 1, - "content": { + "children": { "type": "card", - "content": "Square (1:1)" + "className": "flex h-full items-center justify-center", + "children": "Square (1:1)" } } diff --git a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/ultrawide.json b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/ultrawide.json index 3b97bf22e..881f2c009 100644 --- a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/ultrawide.json +++ b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/ultrawide.json @@ -1,8 +1,9 @@ { "type": "aspect-ratio", "ratio": 2.3333333333333335, - "content": { + "children": { "type": "card", - "content": "21:9 Ratio" + "className": "flex h-full items-center justify-center", + "children": "21:9 Ratio" } } diff --git a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/video-aspect-ratio.json b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/video-aspect-ratio.json index a1b08c5e7..8cb10ed6b 100644 --- a/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/video-aspect-ratio.json +++ b/examples/schema-catalog/src/schemas/components-layout-aspect-ratio/video-aspect-ratio.json @@ -1,9 +1,9 @@ { "type": "aspect-ratio", "ratio": 1.7777777777777777, - "content": { + "children": { "type": "card", - "className": "bg-muted flex items-center justify-center", - "content": "Video Player (16:9)" + "className": "bg-muted flex h-full items-center justify-center", + "children": "Video Player (16:9)" } } diff --git a/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx b/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx new file mode 100644 index 000000000..d74976f33 --- /dev/null +++ b/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx @@ -0,0 +1,186 @@ +/** + * 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#6773 — every `components-layout-aspect-ratio` demo puts its own + * authored content inside the ratio box. + * + * ## The reading this file was written against + * + * All five shipped demos authored `content`, and + * `packages/components/src/renderers/layout/aspect-ratio.tsx` reads four keys — + * `ratio`, `className`, `image` (with `alt`), and `children || body` — none of + * them `content`. Measured on `origin/main` @ `c6732825d`, rendering each entry + * through the real `SchemaRenderer` the way the docs gallery does: + * + * entry elements text img + * components-layout-aspect-ratio/16-9-… 3 "" 0 + * components-layout-aspect-ratio/square 3 "" 0 + * components-layout-aspect-ratio/4-3 3 "" 0 + * components-layout-aspect-ratio/ultrawide 3 "" 0 + * components-layout-aspect-ratio/video-… 3 "" 0 + * + * Three elements is the harness wrapper plus the two Radix AspectRatio emits. + * The authored `content` reached the DOM only as the leaked host attribute + * `content="[object Object]"` — the objectui#5574 class, and `ui:aspect-ratio` + * is already ledgered for it in + * `packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx`. + * + * ## Why nothing already red covered it + * + * `catalog-gallery-render.test.tsx` DOES render all five, and passes: its + * non-vacuity control is `drewSomething(elements > WRAPPER_ELEMENTS || text)`, + * and an empty ratio box clears it on the wrapper Radix draws for the ratio + * itself. Its stronger control — the entry's own authored strings on screen — + * is scoped to `NEWLY_REGISTERED_CATEGORIES`, which this family is not in. + * Nor could a parse have caught it: `BaseSchema` is `.passthrough()` and + * carries `[key: string]: any`, so `content` is accepted by zod and by tsc + * alike (the objectui#6157 class-3 shape). `check-doc-component-types.mjs` + * rules the question out by name — "NOT in scope, deliberately: whether the + * snippet's OTHER keys are read by the renderer the type resolves to". + * + * So the control that was missing is the one below, and it is asserted at + * CATEGORY scope rather than over a list of five ids: a sixth demo authoring + * the phantom key again fails here without anyone remembering to add it. + * + * Each assertion is paired with a counter-probe that proves it can still fail + * — the objectui#6157 discipline. The counter-probe renders the exact pre-fix + * shape, so what is pinned is not "the text is somewhere on screen" but "the + * key that carries it is one the renderer reads". + * + * Module-scope import of `@object-ui/components`, not `beforeAll` (AGENTS.md + * §测试纪律): registering the renderers is an unbounded module load and must + * not be billed to a bounded hook timeout. + */ +import { describe, it, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import '@object-ui/components'; +import { SchemaRenderer, toRenderableSchema } from '@object-ui/react'; +import { allExamples } from '../src/index.js'; + +const CATEGORY = 'components-layout-aspect-ratio'; + +/** + * The keys `aspect-ratio.tsx` reads off the schema, plus the `type` + * discriminator. COPIED as literals rather than imported: they are the + * contract this file is about, and a renderer that starts reading a new key + * should turn this red for review rather than silently widen the set. + */ +const READ_KEYS = ['type', 'ratio', 'className', 'image', 'alt', 'children', 'body']; + +/** Keys that carry visible content in the nodes this family authors. */ +const TEXT_SLOTS = ['children', 'body', 'title', 'description']; + +/** Render one entry the way `SchemaThumbnail` does. */ +function draw(schema: unknown) { + const { container, unmount } = render( +
+ +
, + ); + return { + text: container.textContent ?? '', + images: Array.from(container.querySelectorAll('img')).map((el) => ({ + src: el.getAttribute('src') ?? '', + alt: el.getAttribute('alt') ?? '', + })), + unmount, + }; +} + +/** Every string this node authors in a content slot, at any depth. */ +function authoredText(node: unknown, inSlot = false): string[] { + if (typeof node === 'string') return inSlot ? [node] : []; + if (Array.isArray(node)) return node.flatMap((child) => authoredText(child, inSlot)); + if (!node || typeof node !== 'object') return []; + return Object.entries(node as Record).flatMap(([key, value]) => + authoredText(value, TEXT_SLOTS.includes(key)), + ); +} + +const entries = allExamples().filter((e) => e.meta.category === CATEGORY); + +describe(`${CATEGORY} demos render their authored content (objectui#6773)`, () => { + it('the category is not empty — a vacuous sweep would pass every case below', () => { + expect(entries.length).toBeGreaterThanOrEqual(5); + }); + + it.each(entries.map((e) => [e.id, e] as const))( + '%s authors only keys the renderer reads', + (_id, entry) => { + const authored = Object.keys(entry.schema as Record); + expect(authored.filter((key) => !READ_KEYS.includes(key))).toEqual([]); + }, + ); + + it.each(entries.map((e) => [e.id, e] as const))( + '%s puts its authored content inside the box', + (_id, entry) => { + const schema = entry.schema as Record; + const drawn = draw(schema); + try { + if (typeof schema.image === 'string') { + // The `image` arm: the renderer's own , not a nested node. + expect(drawn.images).toEqual([ + { src: schema.image, alt: (schema.alt as string) ?? '' }, + ]); + } + const texts = authoredText(schema); + expect(texts.length + drawn.images.length).toBeGreaterThan(0); + for (const text of texts) expect(drawn.text).toContain(text); + } finally { + drawn.unmount(); + } + }, + ); + + it('counter-probe: the pre-#6773 `content` shape draws an EMPTY box', () => { + // Verbatim the shape `square.json` and `16-9-aspect-ratio.json` carried + // before this change. Both assertions above are satisfied by it only if + // they have stopped measuring anything. + const card = draw({ + type: 'aspect-ratio', + ratio: 1, + content: { type: 'card', content: 'Square (1:1)' }, + }); + try { + expect(card.text.trim()).toBe(''); + expect(card.images).toEqual([]); + } finally { + card.unmount(); + } + + const photo = draw({ + type: 'aspect-ratio', + ratio: 16 / 9, + content: { type: 'image', src: 'https://example.test/photo.jpg', alt: 'Photo' }, + }); + try { + expect(photo.images).toEqual([]); + expect(photo.text.trim()).toBe(''); + } finally { + photo.unmount(); + } + }); + + it('counter-probe: the judge sees content that IS authored under a read key', () => { + const drawn = draw({ + type: 'aspect-ratio', + ratio: 1, + children: { type: 'card', children: 'Square (1:1)' }, + }); + try { + expect(drawn.text).toContain('Square (1:1)'); + expect(authoredText({ children: { type: 'card', children: 'Square (1:1)' } })).toEqual([ + 'Square (1:1)', + ]); + } finally { + drawn.unmount(); + } + }); +});