diff --git a/.changeset/alert-dialog-fixture-premise-pin-7693.md b/.changeset/alert-dialog-fixture-premise-pin-7693.md
new file mode 100644
index 0000000000..82d6073136
--- /dev/null
+++ b/.changeset/alert-dialog-fixture-premise-pin-7693.md
@@ -0,0 +1,4 @@
+---
+---
+
+Test-only change in `@object-ui/types`: the pin that recorded objectui#7693's filed premise (the four alert-dialog schema-catalog fixtures authoring an `actions` array) is re-derived onto the read dialect now that the card has landed. No published behaviour changes — `overlay.ts` and `zod/overlay.zod.ts` are untouched, and the only file edited under `src/` is a test.
diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json
index a273a24973..ebc687d7da 100644
--- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json
+++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/basic-alert-dialog.json
@@ -6,16 +6,6 @@
"type": "button",
"label": "Delete Account"
},
- "actions": [
- {
- "type": "button",
- "label": "Cancel",
- "variant": "outline"
- },
- {
- "type": "button",
- "label": "Continue",
- "variant": "destructive"
- }
- ]
+ "cancelText": "Cancel",
+ "actionText": "Continue"
}
diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json
index ad076ac6dd..ff9eb7800e 100644
--- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json
+++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/confirmation-dialog.json
@@ -7,16 +7,6 @@
"label": "Close",
"variant": "outline"
},
- "actions": [
- {
- "type": "button",
- "label": "Don't Save",
- "variant": "ghost"
- },
- {
- "type": "button",
- "label": "Save",
- "variant": "default"
- }
- ]
+ "cancelText": "Don't Save",
+ "actionText": "Save"
}
diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json
index 3e660d74cf..2d56f44da2 100644
--- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json
+++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/custom-actions.json
@@ -7,16 +7,6 @@
"label": "Proceed",
"variant": "default"
},
- "actions": [
- {
- "type": "button",
- "label": "Go Back",
- "variant": "outline"
- },
- {
- "type": "button",
- "label": "Yes, Continue",
- "variant": "default"
- }
- ]
+ "cancelText": "Go Back",
+ "actionText": "Yes, Continue"
}
diff --git a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json
index c461f14391..cfd178b206 100644
--- a/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json
+++ b/examples/schema-catalog/src/schemas/components-overlay-alert-dialog/destructive-action.json
@@ -7,15 +7,6 @@
"label": "Delete",
"variant": "destructive"
},
- "actions": [
- {
- "type": "button",
- "label": "Cancel"
- },
- {
- "type": "button",
- "label": "Delete",
- "variant": "destructive"
- }
- ]
+ "cancelText": "Cancel",
+ "actionText": "Delete"
}
diff --git a/examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx b/examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx
new file mode 100644
index 0000000000..80993b2446
--- /dev/null
+++ b/examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx
@@ -0,0 +1,180 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectui#7693 — the four `alert-dialog` catalog fixtures authored an
+ * `actions` array that no surface carries, so the docs page's own examples
+ * rendered an EMPTY footer.
+ *
+ * ## Why nothing was red before this file
+ *
+ * `BaseSchema` is `.passthrough()` (`packages/types/src/zod/base.zod.ts:241`),
+ * so `actions` rode through `safeValidateSchema` unvalidated and every gate
+ * stayed green while the rendered dialog had no buttons at all. The renderer
+ * (`packages/components/src/renderers/overlay/alert-dialog.tsx`) draws
+ * `AlertDialogCancel` ONLY from `schema.cancelText` and `AlertDialogAction`
+ * ONLY from `schema.actionText`; `schema.actions` has zero read sites.
+ *
+ * ## The two pins, and why both
+ *
+ * The DOM pin is the one that names the SYMPTOM the card reported (an empty
+ * footer), and it is the one that would have gone red on `origin/main`: all
+ * four fixtures drew zero footer buttons. The fixture-key pin is the cheap
+ * companion that closes the CLASS — it walks every catalog entry, not just the
+ * four, so a future `alert-dialog` node re-authored under `actions` reds here
+ * even if nobody adds a render assertion for it.
+ *
+ * Each pin is paired with a live control, because a render that draws nothing
+ * satisfies a "no `actions` key" assertion trivially and a harness that renders
+ * nothing at all satisfies both:
+ *
+ * - `RENDERS_IN_THE_READ_DIALECT` — an inline node in the read dialect MUST
+ * draw two footer buttons. If the harness cannot see a footer button, this
+ * control reds first and the fixture rows below mean nothing.
+ * - `THE_PRE_REPAIR_SHAPE_DRAWS_NOTHING` — the same node authored the way the
+ * fixtures were authored (an `actions` array, no label keys) MUST draw zero
+ * footer buttons, and MUST still validate. That is the defect reproduced in
+ * one place, and it is what makes the fixture rows a measurement of the
+ * SPELLING rather than of some unrelated render failure.
+ *
+ * ⛔ Do NOT "fix" a red row here by declaring `actions` on `AlertDialogSchema`.
+ * Making a variant-carrying action list live is a renderer + types feature on
+ * the manual floor, explicitly ruled out of this card's scope; the fixtures
+ * follow the dialect the renderer READS.
+ */
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/react';
+import '@object-ui/components';
+import { SchemaRenderer, toRenderableSchema } from '@object-ui/react';
+import { safeValidateSchema } from '@object-ui/types/zod';
+import { allExamples, getExample } from '../src/index.js';
+
+/** The four entries the docs page embeds, in the order it embeds them. */
+const FIXTURE_IDS = [
+ 'components-overlay-alert-dialog/basic-alert-dialog',
+ 'components-overlay-alert-dialog/destructive-action',
+ 'components-overlay-alert-dialog/confirmation-dialog',
+ 'components-overlay-alert-dialog/custom-actions',
+] as const;
+
+/**
+ * Render one entry the way the docs gallery does, forced OPEN.
+ *
+ * `defaultOpen` is the only thing the test adds: the fixtures are authored
+ * closed (that is what the docs page wants — a trigger you click), and a closed
+ * Radix dialog mounts no content, so the footer could not be measured at all.
+ * Everything the assertions read comes from the fixture's own keys.
+ */
+function footerButtons(schema: unknown): string[] {
+ render(
+ ,
+ );
+ // Radix portals the content to document.body, so the RTL container is empty
+ // by construction — query the dialog itself. An `alert-dialog` has no close
+ // affordance of its own, so every button inside the content IS a footer
+ // button (the trigger stays behind, in the container).
+ const content = document.body.querySelector('[role="alertdialog"]');
+ if (!content) return [];
+ return Array.from(content.querySelectorAll('button')).map((b) => b.textContent ?? '');
+}
+
+/** Report the issues rather than `false`, so a red run says what broke. */
+function reasons(schema: unknown): string[] {
+ const r = safeValidateSchema(schema);
+ return r.success ? [] : r.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
+}
+
+const READ_DIALECT_NODE = {
+ type: 'alert-dialog',
+ title: 'Are you sure?',
+ trigger: { type: 'button', label: 'Open' },
+ cancelText: 'Cancel',
+ actionText: 'Continue',
+};
+
+const PRE_REPAIR_NODE = {
+ type: 'alert-dialog',
+ title: 'Are you sure?',
+ trigger: { type: 'button', label: 'Open' },
+ actions: [
+ { type: 'button', label: 'Cancel', variant: 'outline' },
+ { type: 'button', label: 'Continue', variant: 'destructive' },
+ ],
+};
+
+describe('objectui#7693 — controls: the harness can tell a drawn footer from an empty one', () => {
+ it('RENDERS_IN_THE_READ_DIALECT: `cancelText` + `actionText` draw two footer buttons', () => {
+ expect(footerButtons(READ_DIALECT_NODE)).toEqual(['Cancel', 'Continue']);
+ });
+
+ it('THE_PRE_REPAIR_SHAPE_DRAWS_NOTHING: an `actions` array draws an empty footer', () => {
+ // The defect, reproduced in one place. `actions` is not refused — it is
+ // simply never read, which is why every gate stayed green.
+ expect(footerButtons(PRE_REPAIR_NODE)).toEqual([]);
+ expect(reasons(PRE_REPAIR_NODE)).toEqual([]);
+ });
+});
+
+describe('objectui#7693 — every alert-dialog fixture renders a real footer', () => {
+ it.each(FIXTURE_IDS)('%s draws at least one footer button', (id) => {
+ const labels = footerButtons(getExample(id).schema);
+ expect(labels.length).toBeGreaterThanOrEqual(1);
+ // ...and each one carries text, so a button rendered from an empty label
+ // cannot satisfy the count.
+ for (const label of labels) expect(label.trim().length).toBeGreaterThan(0);
+ });
+
+ it.each(FIXTURE_IDS)('%s still validates under safeValidateSchema', (id) => {
+ expect(reasons(getExample(id).schema)).toEqual([]);
+ });
+});
+
+describe('objectui#7693 — no alert-dialog node in the catalog authors `actions`', () => {
+ /** Every object in an example's schema tree, the example's id carried along. */
+ function* nodes(value: unknown, id: string): Generator<[string, Record]> {
+ if (Array.isArray(value)) {
+ for (const item of value) yield* nodes(item, id);
+ } else if (value !== null && typeof value === 'object') {
+ yield [id, value as Record];
+ for (const child of Object.values(value as Record)) yield* nodes(child, id);
+ }
+ }
+
+ it('the whole catalog is clean, not just the four the docs page embeds', () => {
+ const offenders: string[] = [];
+ for (const example of allExamples()) {
+ for (const [id, node] of nodes(example.schema, example.id)) {
+ if (node.type === 'alert-dialog' && 'actions' in node) offenders.push(id);
+ }
+ }
+ expect(offenders).toEqual([]);
+ });
+
+ it('anti-vacuity: the walk really does reach alert-dialog nodes', () => {
+ // Without this, the assertion above passes just as well when the walk is
+ // broken, or when the catalog holds no alert-dialog at all.
+ const seen = new Set();
+ for (const example of allExamples()) {
+ for (const [id, node] of nodes(example.schema, example.id)) {
+ if (node.type === 'alert-dialog') seen.add(id);
+ }
+ }
+ expect([...seen].sort()).toEqual([...FIXTURE_IDS].sort());
+ });
+
+ it('anti-vacuity: the offender test would catch one', () => {
+ // The same predicate, over the pre-repair shape, must report it.
+ const offenders: string[] = [];
+ for (const [id, node] of nodes(PRE_REPAIR_NODE, 'inline/pre-repair')) {
+ if (node.type === 'alert-dialog' && 'actions' in node) offenders.push(id);
+ }
+ expect(offenders).toEqual(['inline/pre-repair']);
+ });
+});
diff --git a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
index 1d05d12765..11027573d9 100644
--- a/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
+++ b/examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx
@@ -121,7 +121,15 @@ const NODE_CENSUS: Readonly 118 with objectui#7693: the four `components-overlay-alert-dialog/*`
+ // fixtures each authored two `type: 'button'` nodes under an `actions` array
+ // the `alert-dialog` renderer never reads, so those eight nodes drew nothing
+ // and the demos showed an empty footer. They are now the two label keys the
+ // renderer DOES read (`cancelText` / `actionText`), which are strings rather
+ // than nodes and so leave this walk. Catalog-authored, no renderer touched —
+ // the case this table's header sanctions.
+ button: { rendered: 118, noElement: 0 },
input: { rendered: 48, noElement: 0 },
checkbox: { rendered: 12, noElement: 0 },
switch: { rendered: 7, noElement: 0 },
diff --git a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts
index 73b9762f56..6817845ca6 100644
--- a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts
+++ b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts
@@ -55,10 +55,18 @@
* and its own changeset grade (the objectui#7104 ruling); the pins below
* record today's state so that the PR which retires them re-derives these
* lines deliberately rather than passing unnoticed.
- * - The four schema-catalog fixtures author `actions`, a key no surface
- * carries, so the docs page's own examples render an empty footer —
- * objectui#7693. Pinned here as that card's filed premise; its fix goes red
- * here and re-derives the pin.
+ * - RESOLVED, and re-derived rather than deleted: the four schema-catalog
+ * fixtures used to author `actions`, a key no surface carries, so the docs
+ * page's own examples rendered an empty footer — objectui#7693. That card
+ * landed and this file went red exactly as predicted above, on all four
+ * membership legs. The block at the bottom is the SAME pin re-derived onto
+ * the other side of the flip: the fixtures now author the read dialect, and
+ * what it asserts is the TYPES-side reading (every key they author is a
+ * member of the mirror's shape), not the render-side one. The render-side
+ * half lives with the catalog, in
+ * `examples/schema-catalog/test/alert-dialog-footer-read-dialect-7693.test.tsx`
+ * — deliberately not duplicated here, because this package cannot see a
+ * renderer and that one cannot see the mirror's shape.
*/
import { describe, expect, it } from 'vitest';
@@ -318,17 +326,52 @@ describe('the docs page publishes the read dialect (objectui#7104)', () => {
});
});
-describe('the schema-catalog fixtures still author `actions` — objectui#7693, pinned as its filed premise', () => {
- it.each(FIXTURES)('%s.json carries an `actions` array and neither `cancelText` nor `actionText`', (name) => {
- const fixture = JSON.parse(read(`${FIXTURE_DIR}/${name}.json`)) as Record;
- expect(Array.isArray(fixture.actions)).toBe(true);
- expect(fixture).not.toHaveProperty('cancelText');
- expect(fixture).not.toHaveProperty('actionText');
+describe('the schema-catalog fixtures author the READ dialect — objectui#7693, the premise pin re-derived', () => {
+ const fixture = (name: string): Record =>
+ JSON.parse(read(`${FIXTURE_DIR}/${name}.json`)) as Record;
+
+ it.each(FIXTURES)('%s.json authors `cancelText` and `actionText` and no `actions` array', (name) => {
+ const json = fixture(name);
+ expect(json).not.toHaveProperty('actions');
+ expect(typeof json.cancelText).toBe('string');
+ expect(typeof json.actionText).toBe('string');
+ });
+
+ it('every key any fixture authors is a MEMBER of the mirror — the reading passthrough used to hide', () => {
+ // The types-side half of objectui#7693, and the only half this package can
+ // measure. `BaseSchema` is `.passthrough()`, so `safeParse` says nothing
+ // about whether an authored key is carried by anything; membership in the
+ // mirror's own shape is the question, and it is asked here rather than in
+ // the catalog because `AlertDialogZod.shape` is not visible from there.
+ const authored = new Set(FIXTURES.flatMap((name) => Object.keys(fixture(name))));
+ const undeclared = [...authored].filter((key) => !(key in shape)).sort();
+ expect(undeclared).toEqual([]);
+ });
+
+ it('control: `actions` is not a member, so the leg above really would catch it', () => {
+ // Without this, the assertion above passes just as well against a mirror
+ // that declares everything, and the four fixture legs read as a tautology.
+ expect('actions' in shape).toBe(false);
+ expect(['type', 'title', 'trigger', 'cancelText', 'actionText'].every((k) => k in shape)).toBe(true);
+ });
+
+ it('control: the pre-repair shape STILL parses green — passthrough did not change', () => {
+ // Why nothing red covered objectui#7693 before it landed, kept as a live
+ // reading rather than as history: re-authoring a fixture under `actions`
+ // would sail through `safeParse` again, which is why the membership leg
+ // above is the guard and `.success` is not.
+ const preRepair = {
+ type: 'alert-dialog',
+ title: 'Are you sure?',
+ trigger: { type: 'button', label: 'Open' },
+ actions: [{ type: 'button', label: 'Cancel' }, { type: 'button', label: 'Continue' }],
+ };
+ expect(AlertDialogZod.safeParse(preRepair).success).toBe(true);
});
- it('and every one of them parses GREEN regardless — passthrough is why nothing red covers objectui#7693', () => {
+ it('and every one of them parses GREEN — now with the keys the renderer reads', () => {
for (const name of FIXTURES) {
- expect(AlertDialogZod.safeParse(JSON.parse(read(`${FIXTURE_DIR}/${name}.json`))).success, name).toBe(true);
+ expect(AlertDialogZod.safeParse(fixture(name)).success, name).toBe(true);
}
});
});