diff --git a/.changeset/drop-test-setup-shadow-stubs.md b/.changeset/drop-test-setup-shadow-stubs.md new file mode 100644 index 000000000..52cb1fd57 --- /dev/null +++ b/.changeset/drop-test-setup-shadow-stubs.md @@ -0,0 +1,34 @@ +--- +"@object-ui/components": patch +--- + +fix(test-setup): stop shadowing ten real registrations, and declare page:header's inputs + +`vitest.setup.dom.tsx` re-registered `text`, `email`, `password`, `textarea`, +`image`, `html`, `avatar`, `select`, `slider` and `grid` by hand — ~380 lines of +renderer copied out of @object-ui/components — to undo bare-name fallbacks that +@object-ui/fields and the plugins claimed by loading after it. + +Both sides now register under their own namespace with `skipFallback: true`, so +nothing overwrites the `ui:` originals and the workaround is obsolete. It was +not free: the copies carried no `inputs` and no `defaultProps`, so inside the +test environment four curated public blocks reported an empty configuration +surface while the real registrations declare one. `apps/console`'s contract +test reads that registry, so its picture of the contract was fiction for those +tags — a guard that measures a fixture instead of the product. + +Deleting the block restores what the app actually boots with. Verified: the ten +tags keep their namespace and canonical type, and their declared surface comes +back — `text` 1 input, `email` 6, `password` 6, `textarea` 6, `image` 3, +`html` 1, `avatar` 4, `select` 6, `slider` 5, `grid` 7, plus `defaultProps`. +The heavy DOM setup also got roughly twice as fast (~545s to ~235s of setup +time across the suite), since every file in that project was paying to evaluate +the duplicated renderers. + +With the shadowing gone, `page:header` was left as a genuine gap: a curated +public block whose renderer reads `title`, `subtitle`, `actions`, `breadcrumb`, +`recordChrome`, `showStar` and `showCopyId`, with none of them declared. Now +declared. + +`element:divider` keeps zero inputs on purpose — its renderer reads only +`className`, so there is nothing to author. diff --git a/packages/components/src/renderers/layout/containers.tsx b/packages/components/src/renderers/layout/containers.tsx index 38ad4fcb3..8f154a051 100644 --- a/packages/components/src/renderers/layout/containers.tsx +++ b/packages/components/src/renderers/layout/containers.tsx @@ -1306,6 +1306,20 @@ ComponentRegistry.register('header', PageHeaderRenderer, { skipFallback: true, label: 'Page Header', category: 'layout', + // The renderer reads all of these off the schema (or off `properties.*` — + // the spec bridge may inline or preserve the bag), so an author configures + // the header through them. `className` is left out as a styling + // pass-through, and the host-injected `RecordContext.headerSystemActions` + // is not authored here at all. + inputs: [ + { name: 'title', type: 'string', label: 'Title', description: 'Supports {field} interpolation and inline translation maps; falls back to the record title' }, + { name: 'subtitle', type: 'string', label: 'Subtitle', description: 'Same interpolation as Title' }, + { name: 'actions', type: 'array', label: 'Actions', description: 'Action buttons rendered in the header, before any host-injected system actions' }, + { name: 'breadcrumb', type: 'boolean', label: 'Breadcrumb', defaultValue: true }, + { name: 'recordChrome', type: 'boolean', label: 'Record Chrome', defaultValue: true, description: 'Set false for the bare h1 header on non-record pages' }, + { name: 'showStar', type: 'boolean', label: 'Show Follow Star', defaultValue: true }, + { name: 'showCopyId', type: 'boolean', label: 'Show Copy-ID', defaultValue: true }, + ], }); // --------------------------------------------------------------------------- diff --git a/vitest.setup.dom.tsx b/vitest.setup.dom.tsx index 2a6d3c6ff..be02d9312 100644 --- a/vitest.setup.dom.tsx +++ b/vitest.setup.dom.tsx @@ -1,11 +1,10 @@ /** * ObjectUI — heavy DOM test setup (the `dom-heavy` project + apps/console) * - * For tests that render through the ComponentRegistry. Registers ObjectUI - * component widgets (text, email, password, textarea, image, html, avatar, - * select, slider, grid) and pulls in @object-ui/components, @object-ui/fields, - * @object-ui/plugin-dashboard, @object-ui/plugin-grid for their side-effect - * registrations. + * For tests that render through the ComponentRegistry. Pulls in + * @object-ui/components, @object-ui/fields, @object-ui/plugin-dashboard and + * @object-ui/plugin-grid for their side-effect registrations — and registers + * nothing itself, so what a test sees is what the app boots with. * * This graph is expensive (~3.3s/file under `isolate: true`), so it is NOT the * default. Most `dom` tests use the trimmed `vitest.setup.dom-light.tsx`; only @@ -15,7 +14,6 @@ import './vitest.setup.base'; import '@testing-library/jest-dom'; -import React from 'react'; import { afterEach } from 'vitest'; import { cleanup } from '@testing-library/react'; @@ -42,390 +40,21 @@ import '@object-ui/fields'; // Register field widgets import '@object-ui/plugin-dashboard'; // Register dashboard components import '@object-ui/plugin-grid'; // Register grid components -// Manually re-register UI components to override field widgets and plugin components -// This is necessary because @object-ui/fields and plugins have @object-ui/components as a dependency, -// so components gets loaded BEFORE fields/plugins register their widgets, causing them to overwrite. -import { ComponentRegistry } from '@object-ui/core'; -import type { TextSchema, InputSchema, TextareaSchema, ImageSchema, HtmlSchema, AvatarSchema, SelectSchema, SliderSchema, GridSchema } from '@object-ui/types'; -import { Input, Textarea, Label, Avatar, AvatarImage, AvatarFallback, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Slider } from '@object-ui/components'; -import { cn, renderChildren } from '@object-ui/components'; +// This file used to re-register `text`, `email`, `password`, `textarea`, +// `image`, `html`, `avatar`, `select`, `slider` and `grid` by hand — ~380 lines +// of renderer copied from @object-ui/components — to undo bare-name fallbacks +// that @object-ui/fields and the plugins were claiming as a side effect of +// loading after it. +// +// Both sides now register those under their own namespace with +// `skipFallback: true`, so nothing overwrites the `ui:` originals and the +// re-registration is obsolete. Keeping it was not free: the copies carried no +// `inputs` and no `defaultProps`, so inside this environment four curated +// public blocks (`text`, `image`, `html`, `grid`) reported an empty +// configuration surface while the real registrations declare one. Any +// assertion about the contract read that as fact. +// +// If a bare name needs overriding again, override it — but carry the original +// meta across, or the registry that tests see stops matching the one that +// ships. -// Re-register text component -ComponentRegistry.register('text', - ({ schema, ...props }: { schema: TextSchema; [key: string]: any }) => { - const { - 'data-obj-id': dataObjId, - 'data-obj-type': dataObjType, - style, - ...rest - } = props; - - if (dataObjId || schema.className || rest.className) { - return ( - - {schema.content || schema.value} - - ); - } - - return <>{schema.content || schema.value}; - }, - { - namespace: 'ui', - label: 'Text', - } -); - -// Re-register email component -const InputRenderer = ({ schema, className, onChange, value, ...props }: { schema: InputSchema; className?: string; onChange?: (val: any) => void; value?: any; [key: string]: any }) => { - const handleChange = (e: React.ChangeEvent) => { - if (onChange) { - onChange(e.target.value); - } - }; - - const { - 'data-obj-id': dataObjId, - 'data-obj-type': dataObjType, - style, - ...inputProps - } = props; - - return ( -
- {schema.label && } - -
- ); -}; - -ComponentRegistry.register('email', - (props: any) => , - { - namespace: 'ui', - label: 'Email Input', - } -); - -// Re-register password component -ComponentRegistry.register('password', - (props: any) => , - { - namespace: 'ui', - label: 'Password Input', - } -); - -// Re-register textarea component -ComponentRegistry.register('textarea', - ({ schema, className, onChange, value, ...props }: { schema: TextareaSchema; className?: string; onChange?: (val: any) => void; value?: any; [key: string]: any }) => { - const handleChange = (e: React.ChangeEvent) => { - if (onChange) { - onChange(e.target.value); - } - }; - - const { - 'data-obj-id': dataObjId, - 'data-obj-type': dataObjType, - style, - ...inputProps - } = props; - - return ( -
- {schema.label && } -