diff --git a/.changeset/6024-catalog-own-plugin-pin-generalized.md b/.changeset/6024-catalog-own-plugin-pin-generalized.md new file mode 100644 index 0000000000..3d85725589 --- /dev/null +++ b/.changeset/6024-catalog-own-plugin-pin-generalized.md @@ -0,0 +1,36 @@ +--- +--- + +Tests and test-config only, no shipped code touched: the catalog "entries use their own +plugin" pin in `examples/schema-catalog/test/catalog-gallery-render.test.tsx` is +generalized from the two hand-enumerated categories objectui#5113 and objectui#5856 left +it at (`plugin-view`, `plugin-grid`) to all thirteen `plugin-*` catalog categories. + +The `category -> type` map is now DERIVED from the `register()` calls, by reusing +`deriveRegistryKeys` from `scripts/check-doc-component-types.mjs` and joining its +registration sites on the `packages/` that owns them — the same derivation +`scripts/regenerate-known-schema-types.mjs` already consumes. The value is a SET, which +`plugin-charts` requires (its entries author `chart` and `bar-chart`), and the rule stays +per entry rather than per category. + +objectui#5113's RENDER half (a record that exists only in the gallery data source reaches +the screen) does not transfer to the eleven categories that author their data inline, so +it is replaced for them by a MOUNT half rather than degraded to the STRUCTURE half: +substituting every own-plugin node with an inert probe must put the probe's marker on +screen, which a stray node, a `type` that is not a node, and a node behind a satisfied +`hidden` cannot do. Data provenance is kept, unweakened, for the entries that bind to an +object the gallery fixture serves — derived per entry from `objectName`, not declared. The +gate states its own coverage split so breadth cannot be read as depth: 5 entries carry +structure + mount + data, 34 carry structure + mount, and `plugin-editor` / `plugin-map` +lose nothing to their `EXCLUSIONS` because the probe replaces the renderer that needs +Monaco and WebGL2. + +The derivation earned its keep on its first run: the filing's hand-written table read +`plugin-form form`, and `form` is registered by `packages/components`, not by +`@object-ui/plugin-form`. Both `plugin-form` entries are the third instance of +objectui#5113's class, ledgered against objectui#6167 with a case asserting they still +fail, so the ledger cannot rot green. + +`examples/schema-catalog/tsconfig.test.json` gains `allowJs` (with `checkJs` off) and a +repo-root `rootDir`, the same answer `tsconfig.scripts.json` reached in objectui#3494 for +importing a plain-JS CI helper into a typed pin test. diff --git a/examples/schema-catalog/test/catalog-gallery-render.test.tsx b/examples/schema-catalog/test/catalog-gallery-render.test.tsx index eb6b543874..34ab74741f 100644 --- a/examples/schema-catalog/test/catalog-gallery-render.test.tsx +++ b/examples/schema-catalog/test/catalog-gallery-render.test.tsx @@ -123,7 +123,7 @@ * entries render `role="alert"` because that is what an Alert IS — the * assertion is correct for a dashboard tile and wrong for the corpus. */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, afterAll } from 'vitest'; import { render, waitFor } from '@testing-library/react'; import '@object-ui/components'; // Mirrors apps/site/app/components/registerCatalogBlocks.ts, in its order. @@ -145,6 +145,12 @@ import { SchemaRenderer, SchemaRendererContext, toRenderableSchema } from '@obje import fs from 'node:fs'; import path from 'node:path'; import { allExamples } from '../src/index.js'; +// Plain-JS CI helper; types are inferred from the `.mjs` source (`allowJs`), the +// same route `scripts/__tests__/known-schema-types-derivation-5115.test.ts` +// takes. objectui#6024 reuses this derivation rather than re-deriving: a second +// copy of "which package registers which key" is the enumeration this pin +// exists to stop keeping by hand. +import { deriveRegistryKeys } from '../../../scripts/check-doc-component-types.mjs'; registerLayout(); @@ -607,126 +613,423 @@ describe('objectui#4616 — every catalog entry renders in the docs gallery', () }); /** - * 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. 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. `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 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 a node of the type its - * own package registers. - * 2. RENDER — the tile shows a record that exists only in the gallery's data + * EVERY `plugin-*` CATEGORY'S ENTRIES ACTUALLY USE THEIR PLUGIN + * (objectui#5113 for `plugin-view`, objectui#5856 for `plugin-grid`, + * objectui#6024 for the generalization to all thirteen). + * + * 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. Two categories 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. `plugin-grid`'s two were the same defect on + * `content/docs/plugins/plugin-grid.mdx`. 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. + * + * ## The two halves, and which entries carry which + * + * The card that generalized this named the hazard it had to avoid, and it is + * the reason the classification below is COUNTED rather than implied: a pin + * turned on for thirteen categories that silently applies only its cheap half + * to eleven of them is WORSE than the honest two-category pin it replaced — + * the coverage number goes up, the assurance goes down, and the number is what + * gets quoted. So every entry's tier is derived, printed by + * `it('states its own coverage split…')`, and pinned there as a literal that a + * new category or a re-tiered entry turns red. + * + * 1. STRUCTURE (every entry) — the entry authors a node whose `type` is in the + * set its OWN package registers. + * 2. MOUNT (every entry) — that node sits where the renderer actually paints. + * Measured by substitution: replace every own-type node with an inert probe + * and re-render; the probe's marker has to reach the DOM. This is the half + * that generalizes objectui#5113's RENDER assertion, and it is the answer + * to the specific hole in STRUCTURE — a STRAY node. A `type` that is not a + * node at all (a field's `type: 'select'`, a validation rule's + * discriminant), a node under a branch the parent never renders, or one + * behind a satisfied `hidden` expression all satisfy STRUCTURE and all fail + * MOUNT. + * 3. DATA PROVENANCE (only entries that bind to an object the gallery fixture + * serves) — the tile shows a record that exists ONLY in the gallery's data * 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-*` - * - * `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 + * `dataSource.find`, not out of the entry's own JSON. This is + * objectui#5113's original second fact, unweakened. + * + * (3) is the strongest of the three and it does NOT generalize, for a reason + * that is a property of the entries rather than a gap in this file: the other + * eleven categories author their data INLINE (`data`, `staticData`, `messages`, + * `columns`, `content`), so there is no record that could only have come from + * the fixture. Measured, entry by entry, on this card's merge-base: exactly the + * five `plugin-view` / `plugin-grid` entries author `objectName` at all. Rather + * than declare that in a table, `DATA_BOUND` is derived from each entry's own + * JSON against the fixture's own object names — a category that becomes + * object-bound picks the half up with no edit here, and one that loses it moves + * the printed split and turns the summary red. + * + * ## What MOUNT does and does not claim + * + * It claims the entry's own-plugin node occupies a painted position in the + * tile. It does not claim the plugin's renderer produced any particular pixel: + * that is what (3) claims where it applies. Two weaker formulations were built + * and MEASURED before being rejected, both as "the live tile shows content the + * probe-substituted tile does not": + * + * - by TEXT: `plugin-charts` renders recharts into a container that happy-dom + * gives zero size, so two of its three tiles paint no text at all + * (`advanced-line-chart`, `simple-bar-chart` — 0 new tokens against the + * probe render). The assertion would be red on entries that are correct. + * - by ELEMENT COUNT: same two tiles measured 3 elements live against 3 + * probe-substituted, i.e. no margin at all, and the `React.lazy` categories + * (`kanban`, `markdown`, `code-editor`) make the count depend on whether + * the dynamic import has landed. + * + * A discriminator that is red on correct entries gets the gate deleted, so the + * honest claim is the one asserted, and the split says how far it goes. + * + * ## The map is DERIVED from the `register()` calls, never enumerated + * + * A hard-coded `category → type` table is the same enumeration the `register()` + * calls already own, and it rots the first time a plugin renames a type. So + * `CATEGORY_OWN_TYPES` comes out of `deriveRegistryKeys` — the same derivation + * `scripts/check-doc-component-types.mjs` runs for its own universe and + * `scripts/regenerate-known-schema-types.mjs` for its generated list — keyed by + * the `packages//…` site each key was registered from. Catalog category + * and package directory are the same string (`plugin-view` ↔ + * `packages/plugin-view`), which is what makes the join a derivation rather + * than a second table. + * + * That it is a derivation is not a claim to take on trust — the rot it exists + * to prevent had ALREADY happened by the time it was written. The card that + * asked for this carried a hand-written table reading `plugin-form form`, and + * measured against the register calls that is wrong: `form` is registered by + * `packages/components/src/renderers/form/form.tsx`, not by + * `@object-ui/plugin-form`, whose own keys are `object-form`, + * `embeddable-form`, `form-analytics` and `object-master-detail-form`. The two + * `plugin-form` entries are therefore the third instance of the #5113 defect, + * ledgered in `OWN_PLUGIN_DEBT` below against the card that owns them. An + * enumerated table would have inherited that mistake and reported them green. + * + * ## Why the set, not a single type + * + * `plugin-charts` breaks one-type-per-category: its entries author `chart` AND + * `bar-chart`, both registered by `packages/plugin-charts`. The value is a SET, + * and the rule stays PER ENTRY — *every* entry authors a node whose type is in + * the set its own package registers. Deliberately NOT "the category authors at + * least one type this package registers", which one conforming entry satisfies + * while every other entry in the category drifts. + * + * The types are asserted to be REGISTERED 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. + * it gets its own assertion. + * + * ## No environment exclusions, and that is a result rather than an oversight + * + * `plugin-editor` and `plugin-map` are in `EXCLUSIONS` above because Monaco + * wants a CDN loader and maplibre wants WebGL2 and a live tile host. Neither + * half here needs them to render: MOUNT replaces the own-type node with the + * probe, so `code-editor` and `object-map` never mount, and DATA PROVENANCE + * does not apply to either (no `objectName`). Both categories carry the same + * assertions as every other non-object-bound category — the environment limit + * costs them nothing in this pin. */ -const CATEGORY_OWN_TYPE: ReadonlyArray< - readonly [category: string, ownType: string, minEntries: number] -> = [ - ['plugin-view', 'object-view', 3], - ['plugin-grid', 'object-grid', 2], -]; +/** Catalog categories that sit on a plugin's docs page. */ +const PLUGIN_CATEGORIES = [ + ...new Set(entries.map((e) => e.meta.category).filter((c) => c.startsWith('plugin-'))), +].sort(); -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); +/** + * `process.cwd()` is the repo root by construction — `scripts/vitest- + * invocation-guard.mjs` refuses any run whose Vitest root is not it. + */ +const derivedRegistry = deriveRegistryKeys(process.cwd()); - /** 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; +/** + * category → the key set `packages/` registers, joined on the + * registration SITE. Both spellings a package produces are kept (the bare + * `object-view` and the namespaced `plugin-view:object-view`), because an entry + * may legitimately author either. + */ +const CATEGORY_OWN_TYPES = new Map>(); +for (const [key, sites] of derivedRegistry.keys as Map) { + for (const site of sites) { + const owner = /^packages\/([^/]+)\//.exec(site)?.[1]; + if (!owner || !PLUGIN_CATEGORIES.includes(owner)) continue; + if (!CATEGORY_OWN_TYPES.has(owner)) CATEGORY_OWN_TYPES.set(owner, new Set()); + CATEGORY_OWN_TYPES.get(owner)!.add(key); + } +} + +/** 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; +} + +const ownTypesIn = (schema: unknown, own: Set) => + [...nodeTypes(schema)].filter((t) => own.has(t)).sort(); + +/** + * The inert stand-in MOUNT substitutes for the entry's own-plugin nodes. It is + * namespaced with `skipFallback` so it claims no bare key any catalog entry + * could name, and it is unregistered on teardown because the registry is a + * process-level singleton. + */ +const PROBE_TYPE = 'catalog-pin:own-plugin-probe'; +const PROBE_MARK = 'OWN-PLUGIN-NODE-PAINTED-HERE'; +ComponentRegistry.register( + 'own-plugin-probe', + () => {PROBE_MARK}, + { namespace: 'catalog-pin', skipFallback: true }, +); +afterAll(() => { + ComponentRegistry.unregister('own-plugin-probe', 'catalog-pin'); +}); + +/** The entry's schema with every own-plugin node swapped for the probe. */ +function substituteOwnNodes(node: unknown, own: Set): unknown { + if (Array.isArray(node)) return node.map((n) => substituteOwnNodes(n, own)); + if (node && typeof node === 'object') { + const out: Record = {}; + for (const [k, v] of Object.entries(node as Record)) { + out[k] = + k === 'type' && typeof v === 'string' && own.has(v) + ? PROBE_TYPE + : substituteOwnNodes(v, own); } + return out; + } + return node; +} + +/** + * The objects the gallery fixture actually serves rows for. Read off the + * fixture above rather than restated, so an entry binding to some other object + * is classified as not-data-bound instead of being asserted against rows the + * fixture would never return. + */ +const FIXTURE_OBJECTS = new Set([USERS_SCHEMA.name]); + +/** The record that exists ONLY in the fixture — the provenance token. */ +const FIXTURE_ONLY_RECORD = USERS_ROWS[0].name; + +/** Does the entry bind to an object the fixture serves? Read off its own JSON. */ +function bindsFixtureObject(node: unknown): boolean { + if (Array.isArray(node)) return node.some((n) => bindsFixtureObject(n)); + if (node && typeof node === 'object') { + for (const [k, v] of Object.entries(node as Record)) { + if (k === 'objectName' && typeof v === 'string' && FIXTURE_OBJECTS.has(v)) return true; + if (bindsFixtureObject(v)) return true; + } + } + return false; +} + +/** + * Entries that author NO node of their own package's types — the #5113 defect, + * still open. Ledgered rather than skipped, keyed to the card that owns the + * fix, and asserted below to STILL FAIL: an entry that starts conforming fails + * this file until its line is deleted, so the ledger cannot rot green. + * + * ⛔ This is not an exemption list. Nothing may be added here to make a red + * turn green — a new entry that does not use its own plugin is a defect in that + * entry, and the fix is the entry. + */ +const OWN_PLUGIN_DEBT: Record = { + 'plugin-form/basic-form': + 'objectui#6167 — authors `form`, which `packages/components` registers, not the ' + + '`object-form` of `@object-ui/plugin-form`. The gallery host does not load ' + + '`@object-ui/plugin-form` at all, so the fix spans the entries and ' + + '`registerCatalogBlocks.ts`.', + 'plugin-form/contact-form': + 'objectui#6167 — same defect as `basic-form`; see that entry.', +}; + +const pluginEntries = entries.filter((e) => PLUGIN_CATEGORIES.includes(e.meta.category)); +const isDataBound = (e: (typeof pluginEntries)[number]) => bindsFixtureObject(e.schema); + +describe('objectui#6024 — the derivation this pin is built on', () => { + it('resolves every registration site — an unresolved one would shrink the sets silently', () => { + expect(derivedRegistry.findings).toEqual([]); + }); + + it('is not vacuous: every plugin category resolved to a non-empty key set', () => { + const empty = PLUGIN_CATEGORIES.filter((c) => !(CATEGORY_OWN_TYPES.get(c)?.size ?? 0)); + expect(empty).toEqual([]); + expect(derivedRegistry.counters.resolved).toBeGreaterThan(100); + }); + + /** + * The provenance token is only provenance if the entries it is asserted + * against cannot supply it. Asserted rather than asserted-in-a-comment: an + * entry that happened to author the fixture's first record would turn the + * DATA half into a tautology — green forever, checking nothing. + * + * Scoped to the `plugin-*` corpus, which is this pin's universe, and that + * scope is measured rather than assumed: two entries elsewhere in the catalog + * DO author the string (`components-layout-card/profile-detail-card` and + * `components-layout-card/user-list-card`, found by this case on the first + * run). They are hand-built cards about a person named like the fixture's + * first row, they are not held to the DATA half, and a corpus-wide assertion + * would have been red on two correct entries. + */ + it(`no plugin-* entry authors ${FIXTURE_ONLY_RECORD} — for them it exists only in the fixture`, () => { + const authoring = pluginEntries + .filter((e) => JSON.stringify(e.schema).includes(FIXTURE_ONLY_RECORD)) + .map((e) => e.id); + expect(authoring).toEqual([]); + }); + + /** + * THE SPLIT, STATED BY THE GATE ITSELF. Breadth is not depth: this case is + * what stops "the pin covers all thirteen categories" from being read as + * "all thirteen carry the strong half". The literal is measured, and a new + * category, a new entry, or an entry changing tier turns it red for review. + */ + it('states its own coverage split — how many entries carry which half', () => { + const tierOf = (e: (typeof pluginEntries)[number]) => + OWN_PLUGIN_DEBT[e.id] ? 'ledgered-debt' : isDataBound(e) ? 'structure+mount+data' : 'structure+mount'; + const byTier: Record = {}; + for (const e of pluginEntries) (byTier[tierOf(e)] ??= []).push(e.id); + const categoriesIn = (tier: string) => + [...new Set((byTier[tier] ?? []).map((id) => id.split('/')[0]))].sort(); + + expect({ + categories: PLUGIN_CATEGORIES.length, + entries: pluginEntries.length, + 'structure+mount+data': { + categories: categoriesIn('structure+mount+data'), + entries: (byTier['structure+mount+data'] ?? []).length, + }, + 'structure+mount': { + categories: categoriesIn('structure+mount'), + entries: (byTier['structure+mount'] ?? []).length, + }, + 'ledgered-debt': byTier['ledgered-debt'] ?? [], + }).toEqual({ + categories: 13, + entries: 41, + // The strong half — the tile shows a record only the fixture holds. + // These are the two categories whose entries bind to `users`. + 'structure+mount+data': { categories: ['plugin-grid', 'plugin-view'], entries: 5 }, + // The other eleven author their data inline, so no fixture-only record + // can reach their tiles. They carry STRUCTURE and MOUNT, and nothing here + // claims otherwise. + 'structure+mount': { + categories: [ + 'plugin-calendar', + 'plugin-charts', + 'plugin-chatbot', + 'plugin-dashboard', + 'plugin-editor', + 'plugin-gantt', + 'plugin-kanban', + 'plugin-map', + 'plugin-markdown', + 'plugin-timeline', + ], + entries: 34, + }, + // Open defects with an owning card, NOT exemptions. See OWN_PLUGIN_DEBT. + 'ledgered-debt': ['plugin-form/basic-form', 'plugin-form/contact-form'], + }); + }); + + it('every ledgered entry still exists and still fails — the ledger cannot rot green', () => { + const ids = new Set(entries.map((e) => e.id)); + for (const [id, reason] of Object.entries(OWN_PLUGIN_DEBT)) { + expect(ids.has(id), `${id} is ledgered but no longer exists in the catalog`).toBe(true); + expect(reason).toMatch(/objectui#\d+/); + const entry = entries.find((e) => e.id === id)!; + const own = CATEGORY_OWN_TYPES.get(entry.meta.category) ?? new Set(); + expect( + ownTypesIn(entry.schema, own), + `${id} now authors a node its own package registers — delete its OWN_PLUGIN_DEBT ` + + 'line so the entry is held to the rule like every other one.', + ).toEqual([]); + } + }); +}); + +describe.each(PLUGIN_CATEGORIES)( + 'objectui#5113/#5856/#6024 — the %s entries use their own plugin', + (category) => { + const own = CATEGORY_OWN_TYPES.get(category) ?? new Set(); + const categoryEntries = pluginEntries.filter((e) => e.meta.category === category); + const held = categoryEntries.filter((e) => !OWN_PLUGIN_DEBT[e.id]); it('the category is populated (guard is not vacuous)', () => { - expect(categoryEntries.length).toBeGreaterThanOrEqual(minEntries); + expect(categoryEntries.length).toBeGreaterThanOrEqual(2); }); - it(`${ownType} is registered in the gallery's registration set`, () => { + it(`every type packages/${category} registers resolves in the gallery's registration set`, () => { + const unresolved = [...own].filter((t) => !ComponentRegistry.get(t)); 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(); + unresolved, + `these types resolve to no renderer, so a 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.', + ).toEqual([]); }); - 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(held.map((e) => [e.id, e.schema] as const))( + '%s authors a node whose type its own package registers', + (id, schema) => { + expect( + ownTypesIn(schema, own), + `${id} authors none of ${[...own].sort().join(', ')} — it is a picture of the ` + + 'component rather than the component. Fix the entry, never this list.', + ).not.toEqual([]); }, ); - 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); + it.each(held.map((e) => [e.id, e.schema] as const))( + '%s mounts that node where the tile actually paints', + async (id, schema) => { + const r = await renderEntry(substituteOwnNodes(schema, own)); try { - // Not authored anywhere in the catalog — it exists only in the fixture. - expect(r.text).toContain('Alice Johnson'); + expect( + r.text, + `${id} authors a node its own package registers, but replacing it with an inert ` + + 'probe changes nothing on screen — the node never reached the DOM, so it is a ' + + 'stray rather than the thing the tile is made of.', + ).toContain(PROBE_MARK); } finally { teardown(r); } }, ); + + const dataBound = held.filter((e) => isDataBound(e)); + if (dataBound.length > 0) { + it.each(dataBound.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, which the case above pins. + expect(r.text).toContain(FIXTURE_ONLY_RECORD); + } finally { + teardown(r); + } + }, + ); + } }, ); diff --git a/examples/schema-catalog/tsconfig.test.json b/examples/schema-catalog/tsconfig.test.json index 7fadb07d3b..92c3ba56ec 100644 --- a/examples/schema-catalog/tsconfig.test.json +++ b/examples/schema-catalog/tsconfig.test.json @@ -17,9 +17,13 @@ "compilerOptions": { "noEmit": true, // The package build emits `dist` from `src`; this project emits nothing, so - // it must not inherit the emit options — and `rootDir` has to widen to the - // package root because the tests sit beside `src`, not inside it. - "rootDir": ".", + // it must not inherit the emit options — and `rootDir` has to widen past the + // package build's `./src` because the tests sit beside `src`, not inside it. + // Widened again to the REPO root by objectui#6024: `catalog-gallery-render. + // test.tsx` imports `scripts/check-doc-component-types.mjs`, and `rootDir` + // is checked against every file in the program, imports included (TS6059), + // even though `noEmit` makes it emit nothing. Nothing else reads it here. + "rootDir": "../..", "declaration": false, "declarationMap": false, "composite": false, @@ -34,7 +38,23 @@ // json`). Narrowing to a list is safe here: no catalog test uses a // jest-dom matcher, and `react`/`react-dom` types arrive through the // packages that re-export them. - "types": ["node"] + "types": ["node"], + // `allowJs` with `checkJs` off, the same answer and for the same reason as + // `tsconfig.scripts.json` (objectui#3494): `catalog-gallery-render.test.tsx` + // imports `deriveRegistryKeys` from `scripts/check-doc-component-types.mjs` + // — a plain-JS CI helper — so the pin's category/type map is DERIVED from + // the `register()` calls rather than enumerated a second time + // (objectui#6024). Without it that import is TS7016 and the module comes in + // as a blanket `any`; the alternative, a hand-written `.d.mts` beside the + // `.mjs`, is a second source of truth free to drift silently. With it the + // types are INFERRED FROM THE HELPER ITSELF, so changing the helper's + // exported signature turns this test red instead of nothing. + // + // `checkJs` stays FALSE on purpose: this project consumes the helper's + // inferred types, it does not take ownership of type-cleanliness inside a + // 1000-line plain-JS gate. + "allowJs": true, + "checkJs": false }, // `test/`, not `src/`: that is where this package keeps its tests, and it is // the layout the gate could not see.