From 5ba929ba54887d443d777370ebdac22e69243649 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 23:56:48 +0000 Subject: [PATCH] docs(components): name the overlay/feedback node slots at their declared type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six pages spelled a node slot `ComponentSchema` — a real shipped export (`packages/types/src/blocks.ts`) that is not a node slot, but the concrete `type: 'component'` block. The keys carry `SchemaNode` (`base.ts`). Seven of the nine rows are corrected, each re-derived from its own declaration rather than by a string replace, because the declarations disagree on whether the array form is admitted: `context-menu`'s `trigger` and both `content` rows take `SchemaNode | SchemaNode[]`; the rest are singular. `dropdown-menu`'s `trigger` stays SINGULAR. Its mirror, its sibling `ContextMenuSchema` and its own shipped `defaultProps` all use the array form and the published TS type refuses it — which side is right is #7081, open on the maintainer floor. Publishing the array form would have pre-empted that ruling. Two rows are NOT renamed and are reported instead: `AlertDialogSchema.actions` is declared nowhere and read by nothing, and `EmptySchema.action` is declared nowhere but read by the renderer through a cast that requires an object — `SchemaNode` admits primitives, so it would have been a new false claim. `overlay-node-slot-doc-types-7082.test.ts` pins the result against the TS declarations, with a type-level leg compiled by `tsconfig.test.json`. No gate parses a `plaintext` fence, so without it a green CI run would say only that nothing else broke. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM --- .../7082-overlay-node-slot-doc-type-names.md | 73 ++++ .../docs/components/overlay/alert-dialog.mdx | 2 +- .../docs/components/overlay/context-menu.mdx | 4 +- .../docs/components/overlay/dropdown-menu.mdx | 2 +- .../docs/components/overlay/hover-card.mdx | 8 +- content/docs/components/overlay/sheet.mdx | 8 +- .../overlay-node-slot-doc-types-7082.test.ts | 356 ++++++++++++++++++ 7 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 .changeset/7082-overlay-node-slot-doc-type-names.md create mode 100644 packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts diff --git a/.changeset/7082-overlay-node-slot-doc-type-names.md b/.changeset/7082-overlay-node-slot-doc-type-names.md new file mode 100644 index 0000000000..085c52e929 --- /dev/null +++ b/.changeset/7082-overlay-node-slot-doc-type-names.md @@ -0,0 +1,73 @@ +--- +--- + +Docs only, publishes nothing: six component pages spelled a node slot +`ComponentSchema`, a real shipped export (`packages/types/src/blocks.ts`) that +is **not** a node slot — it is the concrete `type: 'component'` block. The keys +carry `SchemaNode` (`packages/types/src/base.ts`), which also admits +`string | number | boolean | null | undefined`. A reader who looked the +published name up found a narrow, unrelated type. + +Every row is re-derived from its own declaration on `2c3cd1b`, per page — not +by a string replace, because the declarations disagree with each other on +whether the array form is admitted. + +| page row before | declaration on `main` | page after | +| --- | --- | --- | +| `AlertDialogSchema.trigger: ComponentSchema` | `trigger?: SchemaNode` (`overlay.ts:91`) | `SchemaNode` | +| `ContextMenuSchema.trigger?: ComponentSchema` | `trigger?: SchemaNode \| SchemaNode[]` (`overlay.ts:486`) | `SchemaNode \| SchemaNode[]` | +| `HoverCardSchema.trigger: ComponentSchema` | `trigger: SchemaNode` (`overlay.ts:296`) | `SchemaNode` | +| `HoverCardSchema.content: ComponentSchema` | `content: SchemaNode \| SchemaNode[]` (`overlay.ts:292`) | `SchemaNode \| SchemaNode[]` | +| `DropdownMenuSchema.trigger: ComponentSchema` | `trigger: SchemaNode` (`overlay.ts:433`) | `SchemaNode` | +| `SheetSchema.trigger: ComponentSchema` | `trigger?: SchemaNode` (`overlay.ts:150`) | `SchemaNode` | +| `SheetSchema.content: ComponentSchema` | `content?: SchemaNode \| SchemaNode[]` (`overlay.ts:146`) | `SchemaNode \| SchemaNode[]` | + +**`dropdown-menu` stays singular on purpose.** Its Zod mirror +(`zod/overlay.zod.ts:176`), its sibling `ContextMenuSchema`, and its own shipped +`defaultProps` (`renderers/overlay/dropdown-menu.tsx:130`) all use the array +form, and the published TS type refuses it. Which side is right is objectui#7081, +open and awaiting a maintainer decision on a published-type widening. Publishing +the array form here would have put a claim in the docs that the shipped type +rejects and pre-empted that ruling; the page therefore follows the type an +author's editor reads. The incoherence is real, and it *is* #7081. + +**Two of the nine rows the card named are NOT renamed**, because no honest +docs-only edit resolves them, and both are filed instead: + +- `AlertDialogSchema.actions?: ComponentSchema[]` — declared on neither the TS + interface (`overlay.ts:78-127`) nor the mirror (`zod/overlay.zod.ts:42-55`), + and read by nothing. Renaming it would have kept a phantom key alive under a + second wrong type. +- `EmptySchema.action?: ComponentSchema` — declared on neither + (`feedback.ts:210-224`, `zod/feedback.zod.ts:120-125`), but *read* by the + shipped renderer through a cast (`renderers/feedback/empty.tsx`), which + requires `typeof === 'object'`. `SchemaNode` admits `string | number | + boolean`, so publishing it there would have been a new false claim, not a + correction. + +Requiredness is likewise left alone and filed: `AlertDialogSchema.trigger`, +`SheetSchema.trigger` and `SheetSchema.content` are declared optional and +published required. That is objectui#7073's defect class, already fixed by it on +`context-menu.mdx`, and is out of this card's fence. + +`packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts` pins the +result, and this is a page where the pin is the only evidence there is: all +these rows sit in `plaintext` fences, `check:doc-snippets` compiles +`ts`/`tsx`/`typescript` only and `check:doc-types` reads only the `type` string +literals, so no gate parses them (objectui#5250, objectui#5867). A green CI run +here says "nothing else broke", not "the correction is right". + +The pin's authority is the **TS declaration**, not the Zod mirror — the one +deliberate departure from the objectui#6347 model, because on this tree the two +disagree on four `trigger` rows and pinning against the mirror would have +published #7081's answer. Its type-level leg is compiled by +`packages/types/tsconfig.test.json`, so widening `DropdownMenuSchema.trigger` +turns it red and whoever lands #7081 is told this page owes an update. The two +undeclared rows and the three requiredness divergences are pinned as +divergences, so a fix on either side of any of them also turns it red. + +No fence moved: each page holds one `plaintext` fence marker pair before and +after, so objectui#5867's shrink-only declared population is unchanged. No +declaration, mirror, renderer or fixture was edited. + +Part of objectui#7082. diff --git a/content/docs/components/overlay/alert-dialog.mdx b/content/docs/components/overlay/alert-dialog.mdx index d116a37288..c8c888230d 100644 --- a/content/docs/components/overlay/alert-dialog.mdx +++ b/content/docs/components/overlay/alert-dialog.mdx @@ -27,7 +27,7 @@ interface AlertDialogSchema { description: string; // Dialog description // Trigger - trigger: ComponentSchema; // Component that triggers the dialog + trigger: SchemaNode; // Component that triggers the dialog // Actions actions?: ComponentSchema[]; // Action buttons diff --git a/content/docs/components/overlay/context-menu.mdx b/content/docs/components/overlay/context-menu.mdx index 360fe13a6a..6d40496613 100644 --- a/content/docs/components/overlay/context-menu.mdx +++ b/content/docs/components/overlay/context-menu.mdx @@ -45,8 +45,8 @@ type ContextMenuItem = ContextMenuCommandItem | ContextMenuDividerItem; interface ContextMenuSchema { type: 'context-menu'; - trigger?: ComponentSchema; // Trigger element — a placeholder renders when absent - items: ContextMenuItem[]; // Menu items + trigger?: SchemaNode | SchemaNode[]; // Trigger element — a placeholder renders when absent + items: ContextMenuItem[]; // Menu items className?: string; } ``` diff --git a/content/docs/components/overlay/dropdown-menu.mdx b/content/docs/components/overlay/dropdown-menu.mdx index ea009becdd..9ac4e24ef1 100644 --- a/content/docs/components/overlay/dropdown-menu.mdx +++ b/content/docs/components/overlay/dropdown-menu.mdx @@ -48,7 +48,7 @@ type DropdownMenuItem = DropdownMenuCommandItem | DropdownMenuDividerItem; interface DropdownMenuSchema { type: 'dropdown-menu'; - trigger: ComponentSchema; // Trigger component + trigger: SchemaNode; // Trigger component items: DropdownMenuItem[]; // Menu items // Styling diff --git a/content/docs/components/overlay/hover-card.mdx b/content/docs/components/overlay/hover-card.mdx index 358d2cad88..c13eddd0a4 100644 --- a/content/docs/components/overlay/hover-card.mdx +++ b/content/docs/components/overlay/hover-card.mdx @@ -14,12 +14,12 @@ The Hover Card component displays rich content when hovering over an element. ```plaintext interface HoverCardSchema { type: 'hover-card'; - trigger: ComponentSchema; // Trigger element - content: ComponentSchema; // Card content + trigger: SchemaNode; // Trigger element + content: SchemaNode | SchemaNode[]; // Card content side?: 'top' | 'right' | 'bottom' | 'left'; align?: 'start' | 'center' | 'end'; - openDelay?: number; // Delay before opening (ms) - closeDelay?: number; // Delay before closing (ms) + openDelay?: number; // Delay before opening (ms) + closeDelay?: number; // Delay before closing (ms) className?: string; } ``` diff --git a/content/docs/components/overlay/sheet.mdx b/content/docs/components/overlay/sheet.mdx index 2d32374e21..ba78093b4c 100644 --- a/content/docs/components/overlay/sheet.mdx +++ b/content/docs/components/overlay/sheet.mdx @@ -23,10 +23,10 @@ The Sheet component displays content in a panel that slides in from the edge of ```plaintext interface SheetSchema { type: 'sheet'; - trigger: ComponentSchema; // Trigger component - title?: string; // Sheet title - description?: string; // Sheet description - content: ComponentSchema; // Sheet content + trigger: SchemaNode; // Trigger component + title?: string; // Sheet title + description?: string; // Sheet description + content: SchemaNode | SchemaNode[]; // Sheet content side?: 'left' | 'right' | 'top' | 'bottom'; className?: string; } diff --git a/packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts b/packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts new file mode 100644 index 0000000000..fffadbcdd2 --- /dev/null +++ b/packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts @@ -0,0 +1,356 @@ +/** + * 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. + */ + +/** + * Six component pages name their node slots at the type those keys DECLARE + * (objectui#7082). + * + * ## Why this file exists rather than "the docs gates went green" + * + * Every row this pins lives in a `plaintext` fence. `check:doc-snippets` + * compiles `ts`/`tsx`/`typescript` fences only (`TS_FENCE_LANGUAGES`, + * `scripts/check-doc-snippet-types.mjs`) and `check:doc-types` reads only the + * `type` STRING LITERALS out of docs code blocks. So a member row in these + * fences may name any type at all and every gate stays green -- the blindness + * objectui#5250 records and objectui#5867 declares the population of. A green + * CI run on the correction this file accompanies means "nothing else broke", + * NOT "the correction is right". Same reasoning, and the same shape, as + * `button-group-doc-surface-6347.test.ts` (PR #7078). + * + * ## The authority here is the TS declaration, NOT the Zod mirror + * + * That is the one deliberate departure from the #7078 model, and it is load + * bearing. On this tree the TS interface and its mirror DISAGREE on four + * `trigger` rows: `AlertDialogSchema`, `SheetSchema`, `HoverCardSchema` and + * `DropdownMenuSchema` all declare `trigger: SchemaNode` (singular) while + * `zod/overlay.zod.ts` mirrors each as `z.union([SchemaNodeSchema, + * z.array(SchemaNodeSchema)])`. That asymmetry is objectui#7081 -- OPEN, a + * published-type widening awaiting a maintainer decision. Pinning these pages + * against the mirror would publish the array form on all four and silently + * pre-empt that ruling, so the pin follows the type an author's editor reads. + * + * `DropdownMenuSchema.trigger` therefore stays SINGULAR on the page even + * though its mirror, its sibling `ContextMenuSchema` and its own shipped + * `defaultProps` (`renderers/overlay/dropdown-menu.tsx`) all use the array + * form. The incoherence is real; it IS #7081, and it is recorded below rather + * than resolved here. The type-level leg makes the boundary mechanical: widen + * the declaration and `tsc -p tsconfig.test.json` fails, so whoever lands + * #7081 is told the page owes an update. + * + * ## What the pages taught before, measured on `2c3cd1b` + * + * Nine rows across six pages spelled a node slot `ComponentSchema`. That is a + * real shipped export (`blocks.ts`) and it is NOT a node slot -- it is the + * concrete `type: 'component'` block. A reader who looked the name up found a + * narrow, unrelated type. Seven of the nine are corrected; the other two are + * NOT type-name defects at all and are recorded as divergences below, because + * no honest docs-only edit resolves them. + * + * ## Two rows this file records instead of asserting green + * + * `AlertDialogSchema.actions` and `EmptySchema.action` are documented but + * declared NOWHERE -- not on the TS interface, not in the mirror. Renaming + * either to `SchemaNode` would have swapped one false claim for another, so + * both keep their rows and are pinned as UNDECLARED. The day either is + * declared, this file goes red and the page is owed a row. + * + * Likewise the requiredness of `AlertDialogSchema.trigger`, `SheetSchema.trigger` + * and `SheetSchema.content`: all three are declared OPTIONAL and published + * REQUIRED. That is objectui#7073's defect class, not this card's, and it is + * fenced out of the diff -- but pinned here so it cannot be lost, and so a fix + * on either side turns this file red rather than passing unnoticed. + */ + +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import type { SchemaNode } from '../base'; +import type { + AlertDialogSchema, + ContextMenuSchema, + DropdownMenuSchema, + HoverCardSchema, + SheetSchema, +} from '../overlay'; +import type { ComponentSchema } from '../blocks'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(HERE, '..', '..', '..', '..'); + +const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), 'utf8'); + +/* -- Type-level leg: compiled by `tsc -p packages/types/tsconfig.test.json` -- */ + +type Equals = + (() => G extends A ? 1 : 2) extends (() => G extends B ? 1 : 2) ? true : false; +type Expect = T; +/** Does this slot admit the ARRAY form? The whole of #7081 in one operator. */ +type AdmitsArray = SchemaNode[] extends T ? true : false; + +// The seven corrected rows, asserted against the declarations themselves rather +// than against the page text the runtime leg reads. +export type _HoverCardTrigger = Expect>; +export type _HoverCardContent = Expect>; +// No `NonNullable` here on purpose: `SchemaNode` ALREADY admits `null | +// undefined`, so stripping them would compare against a type neither side has. +export type _SheetContent = Expect>; +export type _ContextMenuTrigger = Expect>; + +// The #7081 boundary, mechanically. `ContextMenuSchema` admits an array and its +// page says so; `DropdownMenuSchema` refuses one and its page says so too. +export type _ContextMenuAdmitsArray = Expect, true>>; +export type _DropdownAdmitsArray = Expect, false>>; +export type _AlertDialogAdmitsArray = Expect, false>>; +export type _SheetAdmitsArray = Expect, false>>; + +// `ComponentSchema` is a real export and NOT a node slot -- the premise the +// whole correction rests on. A `type: 'component'` block is one SchemaNode +// among many, never the slot type. +export type _ComponentSchemaIsANode = Expect>; +export type _NodeSlotIsNotAComponent = Expect>; + +/* -- Reading a member row, on both sides -- */ + +interface Member { + /** Spelled with a `?`. */ + readonly optional: boolean; + /** Everything between the colon and the terminating semicolon, trimmed. */ + readonly typeText: string; +} + +/** The one `plaintext` fence that carries a page's interface blocks. */ +function schemaFence(doc: string, path: string): string { + const fences = [...doc.matchAll(/```plaintext\n([\s\S]*?)```/g)].map((match) => match[1]); + if (fences.length !== 1) { + throw new Error(`expected exactly one \`plaintext\` fence in ${path}, found ${fences.length}`); + } + return fences[0]; +} + +/** The body of one `interface {` block, THROWING when absent. */ +function interfaceBody(source: string, opener: string, path: string): string { + const start = source.indexOf(opener); + if (start === -1) throw new Error(`no \`${opener}\` block in ${path}`); + const end = source.indexOf('\n}', start); + if (end === -1) throw new Error(`unterminated \`${opener}\` block in ${path}`); + return source.slice(start + opener.length, end); +} + +/** + * Member rows of an interface body, keyed by name. + * + * JSDoc is stripped first: the declaration bodies carry multi-paragraph doc + * comments, and a `@example` fence inside one holds lines that look exactly + * like member rows. + */ +function members(body: string): Map { + const bare = body.replace(/\/\*\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); + const found = new Map(); + for (const match of bare.matchAll(/^ {2}(\w+)(\?)?:\s*([^;]+);/gm)) { + found.set(match[1], { optional: match[2] === '?', typeText: match[3].trim() }); + } + return found; +} + +/* -- The six subjects -- */ + +interface Subject { + readonly name: string; + readonly docPath: string; + readonly declPath: string; +} + +const SUBJECTS: readonly Subject[] = [ + { name: 'AlertDialogSchema', docPath: 'content/docs/components/overlay/alert-dialog.mdx', declPath: 'packages/types/src/overlay.ts' }, + { name: 'ContextMenuSchema', docPath: 'content/docs/components/overlay/context-menu.mdx', declPath: 'packages/types/src/overlay.ts' }, + { name: 'HoverCardSchema', docPath: 'content/docs/components/overlay/hover-card.mdx', declPath: 'packages/types/src/overlay.ts' }, + { name: 'DropdownMenuSchema', docPath: 'content/docs/components/overlay/dropdown-menu.mdx', declPath: 'packages/types/src/overlay.ts' }, + { name: 'SheetSchema', docPath: 'content/docs/components/overlay/sheet.mdx', declPath: 'packages/types/src/overlay.ts' }, + { name: 'EmptySchema', docPath: 'content/docs/components/feedback/empty.mdx', declPath: 'packages/types/src/feedback.ts' }, +]; + +const documented = new Map>(); +const declared = new Map>(); +for (const subject of SUBJECTS) { + const doc = read(subject.docPath); + documented.set( + subject.name, + members(interfaceBody(schemaFence(doc, subject.docPath), `interface ${subject.name} {`, subject.docPath)), + ); + declared.set( + subject.name, + members( + interfaceBody( + read(subject.declPath), + `export interface ${subject.name} extends BaseSchema {`, + subject.declPath, + ), + ), + ); +} + +const docRow = (owner: string, key: string): Member | undefined => documented.get(owner)?.get(key); +const declRow = (owner: string, key: string): Member | undefined => declared.get(owner)?.get(key); + +/** The seven rows objectui#7082 corrects: owner, key, and the declared text. */ +const CORRECTED: ReadonlyArray = [ + ['AlertDialogSchema', 'trigger', 'SchemaNode'], + ['ContextMenuSchema', 'trigger', 'SchemaNode | SchemaNode[]'], + ['HoverCardSchema', 'trigger', 'SchemaNode'], + ['HoverCardSchema', 'content', 'SchemaNode | SchemaNode[]'], + ['DropdownMenuSchema', 'trigger', 'SchemaNode'], + ['SheetSchema', 'trigger', 'SchemaNode'], + ['SheetSchema', 'content', 'SchemaNode | SchemaNode[]'], +]; + +describe('six overlay/feedback pages name node slots at the declared type (objectui#7082)', () => { + it.each(CORRECTED)('%s.%s is published as the declaration spells it', (owner, key, expected) => { + // Both legs, so neither side can drift alone: the page says `expected`, and + // `expected` is still what the declaration says. + expect(docRow(owner, key)?.typeText).toBe(expected); + expect(declRow(owner, key)?.typeText).toBe(expected); + }); + + it.each(CORRECTED)('%s.%s no longer names `ComponentSchema`', (owner, key) => { + expect(docRow(owner, key)?.typeText).not.toContain('ComponentSchema'); + }); + + it('`ComponentSchema` is a distinct shipped export, which is why the old rows were wrong', () => { + const blocks = read('packages/types/src/blocks.ts'); + expect(blocks).toContain('export interface ComponentSchema extends BaseSchema {'); + // It is the concrete `type: 'component'` block, not a slot type. + expect(members(interfaceBody(blocks, 'export interface ComponentSchema extends BaseSchema {', 'blocks.ts')).get('type')?.typeText).toBe("'component'"); + // And `SchemaNode` is the slot type these keys actually carry. + expect(read('packages/types/src/base.ts')).toContain( + 'export type SchemaNode = BaseSchema | string | number | boolean | null | undefined;', + ); + }); +}); + +describe('objectui#7081 is NOT pre-empted: the singular rows stay singular (objectui#7082)', () => { + it('`DropdownMenuSchema.trigger` is declared singular, so the page says singular', () => { + expect(declRow('DropdownMenuSchema', 'trigger')?.typeText).toBe('SchemaNode'); + expect(docRow('DropdownMenuSchema', 'trigger')?.typeText).toBe('SchemaNode'); + }); + + it('its Zod mirror still says otherwise -- the asymmetry #7081 exists to rule on', () => { + // Recorded, not resolved. When #7081 lands, whichever side moves, one of + // these two assertions fails and the page is re-derived deliberately. + const mirror = read('packages/types/src/zod/overlay.zod.ts'); + expect(mirror).toContain( + "trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Menu trigger')", + ); + }); + + it('its sibling `ContextMenuSchema` really does declare the union -- the two differ', () => { + expect(declRow('ContextMenuSchema', 'trigger')?.typeText).not.toBe( + declRow('DropdownMenuSchema', 'trigger')?.typeText, + ); + }); + + it("the renderer's shipped `defaultProps.trigger` is still an array", () => { + expect(read('packages/components/src/renderers/overlay/dropdown-menu.tsx')).toMatch( + /trigger:\s*\[\s*\{\s*type:\s*'button'/, + ); + }); +}); + +describe('rows a docs-only edit cannot honestly resolve, recorded rather than renamed (objectui#7082)', () => { + it.each([ + ['AlertDialogSchema', 'actions', 'content/docs/components/overlay/alert-dialog.mdx'], + ['EmptySchema', 'action', 'content/docs/components/feedback/empty.mdx'], + ])('%s.%s is documented but declared nowhere', (owner, key) => { + expect(docRow(owner, key)).toBeDefined(); + expect(declRow(owner, key)).toBeUndefined(); + // Nor in the mirror, which is where a "declared elsewhere" reading would hide. + const mirrorPath = + owner === 'EmptySchema' ? 'packages/types/src/zod/feedback.zod.ts' : 'packages/types/src/zod/overlay.zod.ts'; + const mirrorBody = interfaceBody(read(mirrorPath), `export const ${owner} = BaseSchema.extend({`, mirrorPath); + expect(mirrorBody).not.toMatch(new RegExp(`^\\s*${key}:`, 'm')); + }); + + it('`EmptySchema.action` is nevertheless READ by the shipped renderer, through a cast', () => { + // Undeclared-but-consumed, the objectui#6150 class. It is also why the row + // was not renamed to `SchemaNode`: the renderer requires an OBJECT, so + // `SchemaNode` -- which admits `string | number | boolean` -- would have + // been a new false claim rather than a correction. + const renderer = read('packages/components/src/renderers/feedback/empty.tsx'); + expect(renderer).toContain("(schema as any).action as ComponentSchema | undefined"); + expect(renderer).toContain("typeof actionSchema === 'object'"); + }); + + it('`AlertDialogSchema.actions` is read by nothing at all', () => { + const renderer = read('packages/components/src/renderers/overlay/alert-dialog.tsx'); + expect(renderer).not.toMatch(/schema\.actions/); + // Control: this IS the renderer, and the scan can find things in it. + expect(renderer).toContain("ComponentRegistry.register('alert-dialog'"); + expect(renderer).toContain('renderChildren(schema.trigger)'); + }); +}); + +describe('requiredness divergences left to the objectui#7073 class, pinned so they cannot be lost', () => { + it.each([ + ['AlertDialogSchema', 'trigger'], + ['SheetSchema', 'trigger'], + ['SheetSchema', 'content'], + ])('%s.%s is declared optional and still published required', (owner, key) => { + expect(declRow(owner, key)?.optional).toBe(true); + expect(docRow(owner, key)?.optional).toBe(false); + }); + + it('`ContextMenuSchema.trigger` is the one already corrected, by #7073 -- the control', () => { + expect(declRow('ContextMenuSchema', 'trigger')?.optional).toBe(true); + expect(docRow('ContextMenuSchema', 'trigger')?.optional).toBe(true); + }); +}); + +describe('counter-probes: the readers above can still fail (objectui#7082)', () => { + it('`interfaceBody` throws rather than returning an empty body', () => { + expect(() => interfaceBody('nothing here', 'export interface SheetSchema extends BaseSchema {', 'x.ts')).toThrow( + /no `export interface SheetSchema extends BaseSchema \{` block/, + ); + }); + + it('`schemaFence` throws when a page stops holding exactly one fence', () => { + expect(() => schemaFence('```plaintext\na\n```\n```plaintext\nb\n```\n', 'x.mdx')).toThrow(/found 2/); + }); + + it('the extractors really parsed rows, not empty match sets', () => { + for (const subject of SUBJECTS) { + expect(documented.get(subject.name)?.size).toBeGreaterThan(1); + expect(declared.get(subject.name)?.size).toBeGreaterThan(1); + } + }); + + it('the pre-fix spelling would be caught', () => { + const regressed = members(' trigger: ComponentSchema; // Component that triggers the dialog'); + expect(regressed.get('trigger')?.typeText).toBe('ComponentSchema'); + expect(regressed.get('trigger')?.typeText).not.toBe('SchemaNode'); + }); + + it('a blind string replace to the array form would be caught on dropdown-menu', () => { + // The exact regression the card and #7081 warn about. + const regressed = members(' trigger: SchemaNode | SchemaNode[]; // Trigger component'); + expect(regressed.get('trigger')?.typeText).not.toBe(declRow('DropdownMenuSchema', 'trigger')?.typeText); + }); + + it('JSDoc stripping does not eat real rows, and `@example` rows do not become fake ones', () => { + const parsed = members( + '\n /**\n * @example\n * ```ts\n * fake: NotAMember;\n * ```\n */\n real?: SchemaNode;\n', + ); + expect([...parsed.keys()]).toEqual(['real']); + expect(parsed.get('real')).toEqual({ optional: true, typeText: 'SchemaNode' }); + }); + + it('optionality is read, not assumed -- the two directions differ in this very file', () => { + expect(declRow('HoverCardSchema', 'trigger')?.optional).toBe(false); + expect(declRow('SheetSchema', 'trigger')?.optional).toBe(true); + }); +});