Skip to content

MetricWidget / MetricCard spread ...props onto the DOM, emitting a schema="[object Object]" attribute on every KPI card #4357

Description

@yinlianghui

Observation-class finding, recorded while writing the #4032 render pins. Nothing a user sees; filed so the next agent asserting not.toContain('[object Object]') on a dashboard container knows why it fails and does not "fix" it by widening the wrong assertion.

What happens

MetricWidget (packages/plugin-dashboard/src/MetricWidget.tsx) ends its prop list with ...props and spreads it onto the Shadcn Card:

export const MetricWidget = ({ label, value, /* … */ ...props }: MetricWidgetProps) => {
  
  <Card className={} onClick={} {...props}>

When the widget is reached through SchemaRenderer (every dashboard KPI tile), the renderer hands it the widget schema, so schema lands in ...props and React — which passes unknown lowercase attributes straight through — writes it to the DOM stringified:

<div class="rounded-lg border bg-card …" schema="[object Object]" data-obj-type="metric">

MetricCard has the same {...props} spread and the same exposure.

Why it is worth writing down even though it is invisible

It is a trap for exactly the assertion this area attracts. #4163 pinned expect(container.innerHTML).not.toContain('[object Object]') on DashboardGridLayout, and that is the natural thing to write for any I18nLabel render site. On the metric path that assertion fails for a reason unrelated to labels, in both directions:

  • before an i18n fix, it fails and looks like it caught the label bug;
  • after the fix, it still fails, and the tempting repair is to delete or loosen the assertion — which throws away the real pin.

#4032's suite therefore asserts on the card heading element rather than the container, with a comment. That works, but it is a workaround for a defect that should just not be there.

Fix shape

Destructure the schema-shaped props out before the spread (or allow-list what reaches the DOM), the way a component that is both an SDUI block and a React component normally does. Low risk, but it touches the public prop contract of two exported components, so it is not a drive-by.

No pm:queue — nothing a user hits today.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions