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 (
-