Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); docs(aspect-ratio): author the keys the renderer reads in all five demos by os-sales · Pull Request #6786 · objectstack-ai/objectui · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/6773-aspect-ratio-demo-content.md
Original file line numberDiff line numberDiff line change
@@ -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 `<img>`
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.
10 changes: 8 additions & 2 deletions content/docs/components/layout/aspect-ratio.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}
```
Expand Down
Original file line numberDiff line numberDiff line change
@@ -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"
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
Original file line numberDiff line numberDiff line change
@@ -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"
}
}
Original file line numberDiff line numberDiff line change
@@ -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)"
}
}
186 changes: 186 additions & 0 deletions examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx
Original file line numberDiff line numberDiff line change
@@ -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(
<div className="w-full p-4">
<SchemaRenderer schema={toRenderableSchema(schema as never) as never} />
</div>,
);
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<string, unknown>).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<string, unknown>);
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<string, unknown>;
const drawn = draw(schema);
try {
if (typeof schema.image === 'string') {
// The `image` arm: the renderer's own <img>, 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();
}
});
});
Loading