From 8d0a3502e3c097cb4f668a9ee60261180884e365 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 13:32:22 +0000 Subject: [PATCH] docs(plugin-grid): make the two catalog entries real object-grid nodes The `plugin-grid` category was the last `plugin-*` category whose entries were pictures of the component rather than the component. `product-inventory-grid` and `team-members-grid` authored only `badge button card flex stack text` and sat on `content/docs/plugins/plugin-grid.mdx` inside a `PluginLoader plugins={['grid']}` wrapper neither of them used. Replaced by two entries that query the docs gallery's demo data source through the registered `object-grid` renderer, the way objectui#5113 did for `object-view`. The two mock-ups are re-seated into `components-layout-card` rather than deleted, so none of the three corpus-wide counters moves. objectui#5113's pin is extended to `plugin-grid` via an explicit two-entry category/type map, keeping both halves: structure and the gallery-only record. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L --- .../5856-plugin-grid-catalog-entries.md | 26 +++ content/docs/plugins/plugin-grid.mdx | 18 +- examples/schema-catalog/src/catalog-meta.json | 40 ++++ examples/schema-catalog/src/index.ts | 48 +++- .../inventory-table-card.json} | 0 .../team-roster-card.json} | 0 .../plugin-grid/object-grid-columns.json | 15 ++ .../object-grid-selection-summaries.json | 16 ++ .../test/catalog-gallery-render.test.tsx | 219 +++++++++++------- 9 files changed, 285 insertions(+), 97 deletions(-) create mode 100644 .changeset/5856-plugin-grid-catalog-entries.md rename examples/schema-catalog/src/schemas/{plugin-grid/product-inventory-grid.json => components-layout-card/inventory-table-card.json} (100%) rename examples/schema-catalog/src/schemas/{plugin-grid/team-members-grid.json => components-layout-card/team-roster-card.json} (100%) create mode 100644 examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json create mode 100644 examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json diff --git a/.changeset/5856-plugin-grid-catalog-entries.md b/.changeset/5856-plugin-grid-catalog-entries.md new file mode 100644 index 0000000000..e44709e0e4 --- /dev/null +++ b/.changeset/5856-plugin-grid-catalog-entries.md @@ -0,0 +1,26 @@ +--- +--- + +Docs and catalog fixtures only, no shipped code touched: the two `plugin-grid` catalog +entries are now real `object-grid` nodes instead of hand-built static card layouts. +`content/docs/plugins/plugin-grid.mdx` mounted `product-inventory-grid` and +`team-members-grid` under `PluginLoader plugins={['grid']}` while both authored only +`badge button card flex stack text` — pictures of a grid, not a grid. They are replaced +by `object-grid-columns` (a `ListColumn` set with `sort`, `searchableFields` and +`pagination`) and `object-grid-selection-summaries` (multi-row `selection`, named +`rowActions` / `bulkActions`, per-column footer `summary` roll-ups), both querying the +docs gallery's demo data source the same way the `plugin-view` entries have since +objectui#5113. + +The two mock-ups are legitimate static layouts filed under the wrong plugin, so they are +re-seated into `components-layout-card` as `inventory-table-card` and `team-roster-card` +rather than deleted — deleting a catalog entry moves three corpus-wide counters +(`NODE_CENSUS` in `layout-dom-leak-5574.test.tsx`, and the `className`-carrying layout +node and `stack` node floors in `layout-props-conversion.test.tsx`), and a floor that +moves because a fixture was deleted is indistinguishable later from one that moved +because coverage regressed. All three are unchanged, with no floor edited. + +objectui#5113's pin in `catalog-gallery-render.test.tsx` is extended to cover +`plugin-grid` through an explicit two-entry category/type map, keeping both of its halves +— every entry authors the type its own package registers, and the rendered tile shows a +record that exists only in the gallery's data source. diff --git a/content/docs/plugins/plugin-grid.mdx b/content/docs/plugins/plugin-grid.mdx index 87ec12d642..72fa9d6402 100644 --- a/content/docs/plugins/plugin-grid.mdx +++ b/content/docs/plugins/plugin-grid.mdx @@ -17,9 +17,23 @@ npm install @object-ui/plugin-grid ## Interactive Examples - +Each preview below **is** an `object-grid` node drawn by this plugin — the JSON +in the Code tab is the whole example, and the rows on screen came out of a +`find()` call, not out of that JSON. The records are served by the docs site's +demo data source, because `dataSource` is not a schema key: it is the prop the +registered renderer pulls off `SchemaRendererProvider` context (see +[ObjectQL Integration](#objectql-integration) below), so in your own app these +same nodes read whatever object your data source serves. That fixture answers +search and sort but not filters, which is the only reason no `filter` is +authored here. - +### Columns, sort and search + + + +### Selection, row actions and column summaries + + ## Features diff --git a/examples/schema-catalog/src/catalog-meta.json b/examples/schema-catalog/src/catalog-meta.json index 26d74e0a78..83157eedd9 100644 --- a/examples/schema-catalog/src/catalog-meta.json +++ b/examples/schema-catalog/src/catalog-meta.json @@ -36,6 +36,16 @@ "verification" ] }, + "components-layout-card/inventory-table-card": { + "title": "Inventory Table Card", + "description": "A card that draws a product inventory table by hand — toolbar buttons, header row, stock-status badges. For a table bound to an object, see the `plugin-grid` examples.", + "tags": [ + "card", + "table", + "badge", + "layout" + ] + }, "components-layout-card/profile-detail-card": { "title": "Profile Detail Card", "description": "A card that draws one record by hand — avatar header, label/value rows, edit and delete footer. For a record surface bound to an object, see the `plugin-view` examples.", @@ -46,6 +56,16 @@ "avatar" ] }, + "components-layout-card/team-roster-card": { + "title": "Team Roster Card", + "description": "A card that draws a team roster table by hand — invite action, header row, presence badges. For a table bound to an object, see the `plugin-grid` examples.", + "tags": [ + "card", + "table", + "badge", + "roster" + ] + }, "components-layout-card/user-list-card": { "title": "User List Card", "description": "A card that draws a directory table by hand — header row, striped rows, status badges. For a table bound to an object, see the `plugin-view` / `plugin-grid` examples.", @@ -89,6 +109,26 @@ "title": "Filtered Dashboard — Target Widgets Allow-list", "description": "Legacy targetWidgets allow-list: only listed widgets get the default binding; an explicit filterBindings entry still wins" }, + "plugin-grid/object-grid-columns": { + "title": "Object Grid — Column Definitions", + "description": "object-grid rendering the users object: `ListColumn` objects carrying label, width, align, cell `type` and `link`, with `sort`, `searchableFields` and `pagination` shaping the query the host's data source answers.", + "tags": [ + "object-grid", + "columns", + "sort", + "objectql" + ] + }, + "plugin-grid/object-grid-selection-summaries": { + "title": "Object Grid — Selection, Row Actions and Summaries", + "description": "The same object with multi-row `selection`, named `rowActions` / `bulkActions`, and per-column footer `summary` roll-ups computed over the rows the data source returns.", + "tags": [ + "object-grid", + "selection", + "rowActions", + "summary" + ] + }, "plugin-view/object-view-list": { "title": "Object View — List Surface", "description": "object-view rendering the users object: the columns declared in `table`, with search and sort served by the host's data source.", diff --git a/examples/schema-catalog/src/index.ts b/examples/schema-catalog/src/index.ts index 22e4f23856..11fd014e35 100644 --- a/examples/schema-catalog/src/index.ts +++ b/examples/schema-catalog/src/index.ts @@ -253,7 +253,9 @@ import components_layout_aspect_ratio_square from './schemas/components-layout-a import components_layout_aspect_ratio_ultrawide from './schemas/components-layout-aspect-ratio/ultrawide.json' with { type: 'json' }; import components_layout_aspect_ratio_video_aspect_ratio from './schemas/components-layout-aspect-ratio/video-aspect-ratio.json' with { type: 'json' }; import components_layout_card_basic_card from './schemas/components-layout-card/basic-card.json' with { type: 'json' }; +import components_layout_card_inventory_table_card from './schemas/components-layout-card/inventory-table-card.json' with { type: 'json' }; import components_layout_card_profile_detail_card from './schemas/components-layout-card/profile-detail-card.json' with { type: 'json' }; +import components_layout_card_team_roster_card from './schemas/components-layout-card/team-roster-card.json' with { type: 'json' }; import components_layout_card_user_list_card from './schemas/components-layout-card/user-list-card.json' with { type: 'json' }; import components_layout_card_with_footer from './schemas/components-layout-card/with-footer.json' with { type: 'json' }; import components_layout_container_basic_container from './schemas/components-layout-container/basic-container.json' with { type: 'json' }; @@ -417,8 +419,8 @@ import plugin_form_contact_form from './schemas/plugin-form/contact-form.json' w import plugin_gantt_construction_project_phases from './schemas/plugin-gantt/construction-project-phases.json' with { type: 'json' }; import plugin_gantt_project_timeline_with_dependencies from './schemas/plugin-gantt/project-timeline-with-dependencies.json' with { type: 'json' }; import plugin_gantt_sprint_development_timeline from './schemas/plugin-gantt/sprint-development-timeline.json' with { type: 'json' }; -import plugin_grid_product_inventory_grid from './schemas/plugin-grid/product-inventory-grid.json' with { type: 'json' }; -import plugin_grid_team_members_grid from './schemas/plugin-grid/team-members-grid.json' with { type: 'json' }; +import plugin_grid_object_grid_columns from './schemas/plugin-grid/object-grid-columns.json' with { type: 'json' }; +import plugin_grid_object_grid_selection_summaries from './schemas/plugin-grid/object-grid-selection-summaries.json' with { type: 'json' }; import plugin_kanban_advanced_kanban_with_badges_and_limits from './schemas/plugin-kanban/advanced-kanban-with-badges-and-limits.json' with { type: 'json' }; import plugin_kanban_basic_kanban_board from './schemas/plugin-kanban/basic-kanban-board.json' with { type: 'json' }; import plugin_map_event_venue_finder from './schemas/plugin-map/event-venue-finder.json' with { type: 'json' }; @@ -2623,6 +2625,16 @@ const REGISTRY: Record = { }, schema: components_layout_card_basic_card, }, + 'components-layout-card/inventory-table-card': { + id: 'components-layout-card/inventory-table-card', + meta: { + title: "Inventory Table Card", + description: "A card that draws a product inventory table by hand — toolbar buttons, header row, stock-status badges. For a table bound to an object, see the `plugin-grid` examples.", + category: 'components-layout-card', + tags: ["card", "table", "badge", "layout"], + }, + schema: components_layout_card_inventory_table_card, + }, 'components-layout-card/profile-detail-card': { id: 'components-layout-card/profile-detail-card', meta: { @@ -2633,6 +2645,16 @@ const REGISTRY: Record = { }, schema: components_layout_card_profile_detail_card, }, + 'components-layout-card/team-roster-card': { + id: 'components-layout-card/team-roster-card', + meta: { + title: "Team Roster Card", + description: "A card that draws a team roster table by hand — invite action, header row, presence badges. For a table bound to an object, see the `plugin-grid` examples.", + category: 'components-layout-card', + tags: ["card", "table", "badge", "roster"], + }, + schema: components_layout_card_team_roster_card, + }, 'components-layout-card/user-list-card': { id: 'components-layout-card/user-list-card', meta: { @@ -4102,23 +4124,25 @@ const REGISTRY: Record = { }, schema: plugin_gantt_sprint_development_timeline, }, - 'plugin-grid/product-inventory-grid': { - id: 'plugin-grid/product-inventory-grid', + 'plugin-grid/object-grid-columns': { + id: 'plugin-grid/object-grid-columns', meta: { - title: "Product Inventory Grid", - description: "", + title: "Object Grid — Column Definitions", + description: "object-grid rendering the users object: `ListColumn` objects carrying label, width, align, cell `type` and `link`, with `sort`, `searchableFields` and `pagination` shaping the query the host's data source answers.", category: 'plugin-grid', + tags: ["object-grid", "columns", "sort", "objectql"], }, - schema: plugin_grid_product_inventory_grid, + schema: plugin_grid_object_grid_columns, }, - 'plugin-grid/team-members-grid': { - id: 'plugin-grid/team-members-grid', + 'plugin-grid/object-grid-selection-summaries': { + id: 'plugin-grid/object-grid-selection-summaries', meta: { - title: "Team Members Grid", - description: "", + title: "Object Grid — Selection, Row Actions and Summaries", + description: "The same object with multi-row `selection`, named `rowActions` / `bulkActions`, and per-column footer `summary` roll-ups computed over the rows the data source returns.", category: 'plugin-grid', + tags: ["object-grid", "selection", "rowActions", "summary"], }, - schema: plugin_grid_team_members_grid, + schema: plugin_grid_object_grid_selection_summaries, }, 'plugin-kanban/advanced-kanban-with-badges-and-limits': { id: 'plugin-kanban/advanced-kanban-with-badges-and-limits', diff --git a/examples/schema-catalog/src/schemas/plugin-grid/product-inventory-grid.json b/examples/schema-catalog/src/schemas/components-layout-card/inventory-table-card.json similarity index 100% rename from examples/schema-catalog/src/schemas/plugin-grid/product-inventory-grid.json rename to examples/schema-catalog/src/schemas/components-layout-card/inventory-table-card.json diff --git a/examples/schema-catalog/src/schemas/plugin-grid/team-members-grid.json b/examples/schema-catalog/src/schemas/components-layout-card/team-roster-card.json similarity index 100% rename from examples/schema-catalog/src/schemas/plugin-grid/team-members-grid.json rename to examples/schema-catalog/src/schemas/components-layout-card/team-roster-card.json diff --git a/examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json b/examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json new file mode 100644 index 0000000000..4f90efa6b7 --- /dev/null +++ b/examples/schema-catalog/src/schemas/plugin-grid/object-grid-columns.json @@ -0,0 +1,15 @@ +{ + "type": "object-grid", + "objectName": "users", + "label": "User Directory", + "columns": [ + { "field": "name", "label": "Full Name", "width": 200, "link": true }, + { "field": "email", "label": "Email" }, + { "field": "role", "type": "select" }, + { "field": "department", "label": "Department" }, + { "field": "status", "type": "select", "align": "right" } + ], + "sort": [{ "field": "name", "order": "asc" }], + "searchableFields": ["name", "email", "department"], + "pagination": { "pageSize": 5 } +} diff --git a/examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json b/examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json new file mode 100644 index 0000000000..51f58bd0a6 --- /dev/null +++ b/examples/schema-catalog/src/schemas/plugin-grid/object-grid-selection-summaries.json @@ -0,0 +1,16 @@ +{ + "type": "object-grid", + "objectName": "users", + "label": "User Directory", + "columns": [ + { "field": "name", "label": "Full Name", "summary": "count_filled" }, + { "field": "email", "label": "Email" }, + { "field": "department", "label": "Department", "summary": { "type": "count_unique" } }, + { "field": "role", "type": "select" }, + { "field": "status", "type": "select" } + ], + "selection": { "type": "multiple" }, + "rowActions": ["view", "edit"], + "bulkActions": ["export"], + "pagination": { "pageSize": 5 } +} diff --git a/examples/schema-catalog/test/catalog-gallery-render.test.tsx b/examples/schema-catalog/test/catalog-gallery-render.test.tsx index f042097e2b..eb6b543874 100644 --- a/examples/schema-catalog/test/catalog-gallery-render.test.tsx +++ b/examples/schema-catalog/test/catalog-gallery-render.test.tsx @@ -274,12 +274,13 @@ const AUTHORED_TEXT_EXEMPT: Record = {}; * * objectui#5113 added the object surface (`getObjectSchema` / `find` / the * writes) to the host fixture, because `object-view` reaches its data through - * exactly this context value — `dataSource` is not a schema key. The three - * `plugin-view` entries render through it, which is what - * `THE PLUGIN-VIEW ENTRIES` below asserts. What the mirror reproduces is the - * host's SURFACE and its rows; the query semantics ($search / $orderby / - * windowing) are the host's, and the parity case pins the method names rather - * than re-deriving them here. + * exactly this context value — `dataSource` is not a schema key. The same is + * true of `object-grid`, which is how objectui#5856 could give `plugin-grid` + * real entries without touching the fixture at all. Both categories' entries + * render through it, which is what `CATEGORY_OWN_TYPE` below asserts. What the + * mirror reproduces is the host's SURFACE and its rows; the query semantics + * ($search / $orderby / windowing) are the host's, and the parity case pins the + * method names rather than re-deriving them here. */ const USERS_ROWS = [ { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'admin', department: 'Engineering', status: 'active', created_at: '2024-01-14' }, @@ -606,100 +607,152 @@ describe('objectui#4616 — every catalog entry renders in the docs gallery', () }); /** - * THE PLUGIN-VIEW ENTRIES ACTUALLY USE THE PLUGIN (objectui#5113). + * THESE PLUGIN CATEGORIES' ENTRIES ACTUALLY USE THEIR PLUGIN + * (objectui#5113 for `plugin-view`, objectui#5856 for `plugin-grid`). * * The sweep above answers "does every tile draw". It cannot answer the * question objectui#5113 was filed on: whether an example mounted under a - * PLUGIN's docs page exercises that plugin. The three `plugin-view` entries - * used to be hand-built static card layouts — `card` / `flex` / `text` / - * `badge`, no `object-view` node anywhere — sitting on - * `content/docs/plugins/plugin-view.mdx` under an "Interactive Examples" + * PLUGIN's docs page exercises that plugin. Both categories below failed it in + * exactly the same way, one card apart. + * + * `plugin-view`'s three entries used to be hand-built static card layouts — + * `card` / `flex` / `text` / `badge`, no `object-view` node anywhere — sitting + * on `content/docs/plugins/plugin-view.mdx` under an "Interactive Examples" * heading and inside a `PluginLoader plugins={['view']}` wrapper none of them - * used. Every check in the repo was green on them: the types they named ARE - * registered, and the tiles DID draw. + * used. `plugin-grid`'s two were the same defect on + * `content/docs/plugins/plugin-grid.mdx` under `PluginLoader plugins={['grid']}` + * — measured on this card's own merge-base, both authored exactly + * `badge button card flex stack text` and no `object-grid`. Every check in the + * repo was green on all five: the types they named ARE registered + * (`check-doc-component-types` asks only that), and the tiles DID draw (the + * sweep above asks only that). Neither gate can see this defect, which is why + * this one exists. * - * Two facts are pinned here, and the second is the one that cannot be - * satisfied by a picture of a view: + * Two facts are pinned per category, and the second is the one that cannot be + * satisfied by a picture of the component: * - * 1. STRUCTURE — every entry in the category authors an `object-view` node. + * 1. STRUCTURE — every entry in the category authors a node of the type its + * own package registers. * 2. RENDER — the tile shows a record that exists only in the gallery's data - * source, so the rows on screen came through `ObjectViewRenderer` → - * `ObjectGrid` → `dataSource.find`, not out of the entry's own JSON. An - * entry that went back to drawing its own table would keep (1) satisfiable - * by a stray node and would fail (2) outright. + * source, so the rows on screen came through the registered renderer → + * `dataSource.find`, not out of the entry's own JSON. An entry that went + * back to drawing its own table would keep (1) satisfiable by a stray node + * and would fail (2) outright. + * + * (2) is the half that does the work, and it is discriminating rather than + * incidental: an `object-grid` node whose rows come from an inline + * `data: { provider: 'value', items: [...] }` satisfies (1), renders a + * perfectly good table — measured, 51 elements — and contains no gallery + * record at all, so it fails (2). A grid wired to nothing cannot pass here. + * + * ## Why an explicit two-entry map and not a rule over `plugin-*` * - * Deliberately scoped to `plugin-view` rather than generalized to every - * `plugin-*` category: the general rule needs a per-plugin map of which types - * each package registers, and other categories (`plugin-grid`'s two entries, - * for one) are hand-built mock-ups of exactly this kind today. That is a - * separate card, not a silent exemption list here. + * `CATEGORY_OWN_TYPE` is enumerated, not derived. objectui#5113 scoped its pin + * to one category because the general rule needs a per-plugin map of which + * types each package registers, and it named `plugin-grid`'s two entries as + * the specific reason the rule could not simply be turned on for everything. + * objectui#5856 removed that obstacle — `plugin-grid` was the last `plugin-*` + * category whose entries were pictures of the component rather than the + * component — so generalizing is now possible and is deliberately left to its + * own card. Adding a category here is a two-line change; what a reader must + * NOT do is convert this into a loop over every `plugin-*` category with an + * exemption list, which would turn each remaining gap into a silent entry in a + * table rather than a card someone owns. + * + * The types are asserted to be REGISTERED first, separately from the render, + * because the gallery's registration of `object-grid` is transitive: the host + * list (`HOST_PACKAGES`) does not name `@object-ui/plugin-grid`, and the type + * arrives because `@object-ui/plugin-view` imports `ObjectGrid` from it, which + * runs that package's `register` calls. That is load-bearing and invisible, so + * it gets its own assertion — a `plugin-view` that stopped importing + * `ObjectGrid` would otherwise turn the render case red with "Unknown + * component type" and no explanation of why. */ -describe('objectui#5113 — the plugin-view entries render THROUGH object-view', () => { - const pluginViewEntries = entries.filter((e) => e.meta.category === 'plugin-view'); - - /** Every `type` string anywhere in a schema tree. */ - function nodeTypes(node: unknown, acc: Set = new Set()): Set { - if (Array.isArray(node)) { - for (const n of node) nodeTypes(n, acc); - return acc; - } - if (node && typeof node === 'object') { - for (const [k, v] of Object.entries(node as Record)) { - if (k === 'type' && typeof v === 'string') acc.add(v); - nodeTypes(v, acc); - } - } - return acc; - } - - it('the category is populated (guard is not vacuous)', () => { - expect(pluginViewEntries.length).toBeGreaterThanOrEqual(3); - }); +const CATEGORY_OWN_TYPE: ReadonlyArray< + readonly [category: string, ownType: string, minEntries: number] +> = [ + ['plugin-view', 'object-view', 3], + ['plugin-grid', 'object-grid', 2], +]; - it.each(pluginViewEntries.map((e) => [e.id, e.schema] as const))( - '%s authors an object-view node', - (_id, schema) => { - expect([...nodeTypes(schema)]).toContain('object-view'); - }, - ); +describe.each(CATEGORY_OWN_TYPE)( + 'objectui#5113/#5856 — the %s entries render THROUGH %s', + (category, ownType, minEntries) => { + const categoryEntries = entries.filter((e) => e.meta.category === category); - it.each(pluginViewEntries.map((e) => [e.id, e.schema] as const))( - '%s puts data from the gallery data source on screen', - async (_id, schema) => { - const r = await renderEntry(schema); - try { - // Not authored anywhere in the catalog — it exists only in the fixture. - expect(r.text).toContain('Alice Johnson'); - } finally { - teardown(r); + /** Every `type` string anywhere in a schema tree. */ + function nodeTypes(node: unknown, acc: Set = new Set()): Set { + if (Array.isArray(node)) { + for (const n of node) nodeTypes(n, acc); + return acc; } - }, - ); + if (node && typeof node === 'object') { + for (const [k, v] of Object.entries(node as Record)) { + if (k === 'type' && typeof v === 'string') acc.add(v); + nodeTypes(v, acc); + } + } + return acc; + } - /** - * HOST PARITY for the fixture, same technique and same reason as the - * registration-set parity above: the mirror at the top of this file is what - * the assertions run against, so a host fixture that lost `find` would leave - * this file green while the docs page went back to an empty view. - */ - describe('the docs-site hosts supply the same fixture', () => { - const siteDir = path.join(process.cwd(), 'apps/site/app/components'); - const read = (f: string) => fs.readFileSync(path.join(siteDir, f), 'utf8'); + it('the category is populated (guard is not vacuous)', () => { + expect(categoryEntries.length).toBeGreaterThanOrEqual(minEntries); + }); - it('the host fixture exposes every method this mirror implements', () => { - const source = read('galleryDataSource.ts'); - const missing = GALLERY_DATA_SOURCE_METHODS.filter( - (method) => !new RegExp(`\\basync ${method}\\s*\\(`).test(source), - ); - expect(missing).toEqual([]); + it(`${ownType} is registered in the gallery's registration set`, () => { + expect( + ComponentRegistry.get(ownType), + `${ownType} resolves to no renderer, so the render case below would fail ` + + 'with the OBJUI-001 panel rather than with anything about the entries. ' + + 'For `object-grid` this is transitive — see the header.', + ).toBeTruthy(); }); - it.each(['SchemaThumbnail.tsx', 'InteractiveDemo.tsx'])( - '%s hands it to the renderer', - (host) => { - expect(read(host)).toMatch(/^import \{ galleryDataSource \} from '\.\/galleryDataSource';$/m); - expect(read(host)).toContain('dataSource: galleryDataSource'); + it.each(categoryEntries.map((e) => [e.id, e.schema] as const))( + `%s authors a ${ownType} node`, + (_id, schema) => { + expect([...nodeTypes(schema)]).toContain(ownType); + }, + ); + + it.each(categoryEntries.map((e) => [e.id, e.schema] as const))( + '%s puts data from the gallery data source on screen', + async (_id, schema) => { + const r = await renderEntry(schema); + try { + // Not authored anywhere in the catalog — it exists only in the fixture. + expect(r.text).toContain('Alice Johnson'); + } finally { + teardown(r); + } }, ); + }, +); + +/** + * HOST PARITY for the fixture, same technique and same reason as the + * registration-set parity above: the mirror at the top of this file is what + * the assertions run against, so a host fixture that lost `find` would leave + * this file green while the docs page went back to an empty view. + */ +describe('objectui#5113 — the docs-site hosts supply the same fixture', () => { + const siteDir = path.join(process.cwd(), 'apps/site/app/components'); + const read = (f: string) => fs.readFileSync(path.join(siteDir, f), 'utf8'); + + it('the host fixture exposes every method this mirror implements', () => { + const source = read('galleryDataSource.ts'); + const missing = GALLERY_DATA_SOURCE_METHODS.filter( + (method) => !new RegExp(`\\basync ${method}\\s*\\(`).test(source), + ); + expect(missing).toEqual([]); }); + + it.each(['SchemaThumbnail.tsx', 'InteractiveDemo.tsx'])( + '%s hands it to the renderer', + (host) => { + expect(read(host)).toMatch(/^import \{ galleryDataSource \} from '\.\/galleryDataSource';$/m); + expect(read(host)).toContain('dataSource: galleryDataSource'); + }, + ); });