Skip to content

Commit 1986d25

Browse files
fix(app-shell): the dashboard's Add-widget menu and widget Type select offer every spec widget type, from one registry (#26967)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8AjnN3rHbxAJgDGvm5z2nh4)_ Fixes #26745 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:仪表盘的「添加组件」菜单和组件检查器的「类型」下拉,改为读同一个注册表,而注册表的类型集合直接取自 spec 的 `ChartTypeSchema`;两处都从 11/10 种变成 spec 的全部 20 种。补了新类型的中英文名称,并改写了指向不存在控件的数据集提示文案。 - 为什么改:两个选择器各自手写清单,互相不一致,也和 spec 不一致,9 种平台能渲染的图表在 Studio 里没有入口。页面 block 选择器缺的 8 个经实测全部是有书面理由的排除,本 PR 不动。 - 风险与回滚:只改 Studio 编辑器提供的选项和文案,渲染器与 spec 词汇原样不动;两个选择器的排列改为 spec 顺序。有问题直接 revert 本 PR。 - 先看:`previews/widget-types.ts` 的 `WIDGET_TYPE_IDS` 与 `WIDGET_TYPE_PRESENTATION`(`satisfies`),然后 `DashboardWidgetInspector.tsx` 的 `WIDGET_TYPES`,最后 `AddWidgetPicker.oneRegistry-26745.test.tsx`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/previews/AddWidgetPicker.oneRegistry-26745.test.tsx`,预期 7 个测试全绿。 - 需要你做的:无,席位落地。 ## What changes Base `aa00362f`. - **`previews/widget-types.ts` is the one registry.** The offered set is `WIDGET_TYPE_IDS = ChartTypeSchema.options`, read from `@objectstack/spec/ui` at runtime (the module's consumers already load `@objectstack/spec/ui`), so the file restates no list of type names. What stays per type is presentation (English `label`, the two catalogue keys, category, icon) in `WIDGET_TYPE_PRESENTATION`, checked with `satisfies Record<ChartType, …>`: a type the spec adds or retires fails `tsc` until its row is added or removed. `WIDGET_TYPE_META` and `WIDGETS_BY_CATEGORY` keep their shapes, so `AddWidgetPicker`, `DashboardDefaultInspector` and `DashboardPreview` read the registry unchanged. - **`inspectors/DashboardWidgetInspector.tsx`.** The Type select's hand-kept `WIDGET_TYPES` list (10 entries, no `scatter`) is replaced by a map over `WIDGET_TYPE_IDS`, labelled from the registry's `selectLabelKey`. The docblock that said "A hand-written SUBSET is fine" now says the opposite. - **Newly offered types:** `column`, `scatter` (Type select only; the menu already had it), `treemap`, `sankey`, `combo`, `gauge`, `solid-gauge`, `kpi`, `bullet`, `radar`. `list` and `custom` stay out: they are objectui-only families with no `ChartTypeSchema` member, so a widget authored with them cannot publish. - **i18n (`studio` namespace, en + zh).** `engine.inspector.widget.type.*` for the ten types the Type select did not name; `engine.widgetPicker.type.*` for the nine the Add menu did not name. No row in `metadata-admin/i18n.ts` is added or edited (the dispatch's shared-file rule while #26942 is open). `engine.inspector.widget.datasetHint` is reworded in en and zh: it no longer points at "the inline object query above", which the inspector has not drawn since ADR-0021, and describes the dataset, dimension and measure controls it does draw. - **Page block palette: no change.** Measured against `PageComponentType` (34 types) on `aa00362f`: the palette offers 26, and the 8 it does not offer are exactly the 8 entries of `PALETTE_EXCLUSIONS`, each with a recorded reason. Per the dispatch, a block excluded with a recorded reason stays excluded: - `record:chatter`: compatibility alias of the offered `record:discussion` (maintainer ruling on objectui#5495). - `app:launcher`, `global:notifications`: shell singletons, not page content. - `element:record_picker`, `element:text_input`: render, but are field widgets, not page blocks. - `record:approvals`, `record:attachments`, `record:approval_decision`: render; recorded as "not yet offered, offering it is a Studio decision". Left for that decision (see the report's open question). - Changeset: patch for `@object-ui/i18n`; `@object-ui/app-shell` is `private: true`. ## Tests `previews/AddWidgetPicker.oneRegistry-26745.test.tsx`, seven cases, each read off the rendered control: - the spec enum reads non-empty (the non-vacuity duty of `enumOptions`); - the registry equals `ChartTypeSchema`'s vocabulary, in the spec order; - the Add-widget menu lists exactly the spec types (read from each entry's stored-type `code`); - the Type select lists exactly the spec types, each named by its registry key; - every offered type renders through the real `DashboardRenderer` (via `DashboardPreview`), with no "not supported yet" placeholder and no red unknown-component panel; - control: the same probe sees the placeholder for `heatmap`, a family with no renderer; - every type has both names in en and zh, and the Add menu's en name equals the English `label` the default title is written from. `inspectorStrings.i18n-10804` and `inspectorStrings.i18n-10748` used hand-kept type lists of their own; both now read the registry, so their per-type zh/en checks cover all 20 types. Ablations (both through `scripts/ablation-replace.mjs`, restore verified blob == HEAD and `git diff HEAD` empty): - Type select given back a hand-kept 10-type list: the Type select case goes red (1 failed, 6 passed). - Add menu registry given back a hand-kept 11-type list: the registry, Add menu and Type select cases go red (3 failed, 4 passed). ## Acceptance notes - The two catalogue families name each type differently ("Bar chart" in the menu, "Bar Chart" in the select; "Data table" vs "Table"). Collapsing them to one family is a naming decision this card does not make; the menu's older rows still live in `metadata-admin/i18n.ts` and move with #25922. - Both pickers now follow the spec's order. The menu still groups by category, so its "Single value" group opens with `gauge` instead of `metric`, and the select lists `bar` first instead of `metric`. - A widget added from the menu is titled `New TYPE` from the English `label`, as before; the new types get `New column chart`, `New treemap` and so on. ## Verification All local runs are on head `65768a23` (base `aa00362f`). - Closure build `pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2`: 63/63 tasks, lock VERDICT command-exit 0. - New file: 7/7 passed. Neighbours (34 app-shell test files that read the widget registry, its catalogue rows or the dashboard inspectors/preview, plus `packages/i18n/`): 123 files, 1773 passed / 13 skipped, exit 0. - `pnpm --filter @object-ui/i18n --filter @object-ui/app-shell run type-check`: both `Done`, exit 0. - `pnpm check:new-line-citations` (run from `ui/`): 0 new citations, exit 0. - `dispatch-gates --ran`: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. `node scripts/check-sdui-manifest.mjs` is among them (exit 0). <details><summary>Derived gates, exit codes recorded before any pipe</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> CI families are read from this PR's check-runs; none was re-run locally beyond the derived list. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the dispatch's required Chinese summary). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbafngYyQTJq78seG4v --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbafngYyQTJq78seG4v)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 31a608e commit 1986d25

8 files changed

Lines changed: 351 additions & 76 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
fix(app-shell): the dashboard's Add-widget menu and the widget Type select offer every widget type the spec accepts, from one registry
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Studio's two dashboard widget-type pickers kept their own lists and disagreed with each other and with the spec: the Add-widget menu offered 11 types, the inspector's Type select 10 (no `scatter`), and `ChartTypeSchema` accepts 20, all of which a dashboard draws. Both pickers now read one registry derived from `ChartTypeSchema`, so they offer the same 20 types and follow the spec when it changes.
11+
12+
- The `studio` namespace gains the names of the newly offered types: `engine.inspector.widget.type.*` for `column`, `scatter`, `treemap`, `sankey`, `combo`, `gauge`, `solidGauge`, `kpi`, `bullet` and `radar`, and `engine.widgetPicker.type.*` for the same types except `scatter`, in en and zh.
13+
- `engine.inspector.widget.datasetHint` no longer mentions an "inline object query above", which the widget inspector has not had since ADR-0021; it describes the dataset, dimension and measure controls the inspector shows.

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,27 @@ import {
5050
import { useDatasetCatalog, useDatasetSemantics } from '../previews/useDatasetCatalog.js';
5151
import type { ObjectFieldInfo } from '../previews/useObjectFields.js';
5252
import { widgetMissingInputs } from '../../studio-design/metadataError.js';
53+
import { WIDGET_TYPE_IDS, WIDGET_TYPE_META } from '../previews/widget-types.js';
5354

5455
// ADR-0021: dashboard widgets author the semantic-layer dataset shape only
5556
// (dataset + dimensions + values). The pre-ADR-0021 inline single-object query
5657
// (object / valueField / categoryField / aggregate) was removed from the spec
5758
// at @objectstack/spec 9.0.0 and is no longer authored here — its fields are
5859
// gone so no Studio surface can emit the dead shape (framework#3251).
5960
/**
60-
* The widget families this inspector offers. TYPED to `DashboardWidgetTypeName`
61-
* (objectui#4600, which closed the widget `type` vocabulary) so the inspector
62-
* cannot offer a type validation refuses — every entry here is a spec
63-
* visualization family. A hand-written SUBSET is fine; offering something
64-
* outside the vocabulary is not.
61+
* The widget families this inspector's Type select offers: the ONE registry
62+
* the Add-widget menu reads too (`../previews/widget-types`, objectstack#26745),
63+
* which derives the set from the spec's `ChartTypeSchema` instead of keeping a
64+
* list here. The two pickers used to keep their own lists and disagreed (10
65+
* here, 11 in the menu, 20 in the spec). ⛔ No hand-kept list in this file.
6566
*
6667
* Each label is a catalogue key, resolved in the designer locale at render
6768
* (objectui#10748, the objectui#10586 `KeyedOption` shape); the stored `value`
6869
* never moves.
6970
*/
70-
const WIDGET_TYPES: ReadonlyArray<{ value: DashboardWidgetTypeName; labelKey: string }> = [
71-
{ value: 'metric', labelKey: 'engine.inspector.widget.type.metric' },
72-
{ value: 'bar', labelKey: 'engine.inspector.widget.type.bar' },
73-
{ value: 'horizontal-bar', labelKey: 'engine.inspector.widget.type.horizontalBar' },
74-
{ value: 'line', labelKey: 'engine.inspector.widget.type.line' },
75-
{ value: 'area', labelKey: 'engine.inspector.widget.type.area' },
76-
{ value: 'pie', labelKey: 'engine.inspector.widget.type.pie' },
77-
{ value: 'donut', labelKey: 'engine.inspector.widget.type.donut' },
78-
{ value: 'funnel', labelKey: 'engine.inspector.widget.type.funnel' },
79-
{ value: 'table', labelKey: 'engine.inspector.widget.type.table' },
80-
{ value: 'pivot', labelKey: 'engine.inspector.widget.type.pivot' },
81-
];
71+
const WIDGET_TYPES: ReadonlyArray<{ value: DashboardWidgetTypeName; labelKey: string }> = WIDGET_TYPE_IDS.map(
72+
(id) => ({ value: id, labelKey: WIDGET_TYPE_META[id].selectLabelKey }),
73+
);
8274

8375
const COLORS = [
8476
'default',

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10748.test.tsx‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { ObjectFieldInspector } from './ObjectFieldInspector';
7878
import { FlowEdgeInspector } from './FlowEdgeInspector';
7979
import { FlowNodeInspector } from './FlowNodeInspector';
8080
import { encodeNestedNodeId, NESTED_NODE_KIND } from './flow-nested-selection';
81+
import { WIDGET_TYPE_META } from '../previews/widget-types';
8182
// The designer's `designer.*` / `perm.*` rows live in `@object-ui/i18n`'s `studio`
8283
// namespace, whose `zh` catalogue is fetched on demand (objectstack#25922).
8384
// Resident before any case, so the zh rows below read synchronously.
@@ -139,19 +140,14 @@ function mountWidget(lang: Lang, draft: Record<string, unknown>, selection: { ki
139140
));
140141
}
141142

142-
/** Stored widget `type` → the row its option label reads. */
143-
const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = [
144-
['metric', 'engine.inspector.widget.type.metric'],
145-
['bar', 'engine.inspector.widget.type.bar'],
146-
['horizontal-bar', 'engine.inspector.widget.type.horizontalBar'],
147-
['line', 'engine.inspector.widget.type.line'],
148-
['area', 'engine.inspector.widget.type.area'],
149-
['pie', 'engine.inspector.widget.type.pie'],
150-
['donut', 'engine.inspector.widget.type.donut'],
151-
['funnel', 'engine.inspector.widget.type.funnel'],
152-
['table', 'engine.inspector.widget.type.table'],
153-
['pivot', 'engine.inspector.widget.type.pivot'],
154-
];
143+
/**
144+
* Stored widget `type` → the row its option label reads — read off the one
145+
* widget registry the select derives its options from (objectstack#26745).
146+
*/
147+
const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = Object.values(WIDGET_TYPE_META).map((m) => [
148+
m.id,
149+
m.selectLabelKey,
150+
]);
155151

156152
/** The widget-type select's trigger text, for a widget stored with `type`. */
157153
function widgetTypeTrigger(lang: Lang, type: string): string | null {

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/inspectorStrings.i18n-10804.test.tsx‎

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -383,20 +383,12 @@ describe('the Problems panel — expression messages (objectui#10804)', () => {
383383

384384
// ─── Site 4: the dashboard add-widget picker (objectui#10805) ────────────────
385385

386-
/** Stored `type` → the catalogue row its picker entry displays. */
387-
const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = [
388-
['metric', 'engine.widgetPicker.type.metric'],
389-
['bar', 'engine.widgetPicker.type.bar'],
390-
['horizontal-bar', 'engine.widgetPicker.type.horizontalBar'],
391-
['line', 'engine.widgetPicker.type.line'],
392-
['area', 'engine.widgetPicker.type.area'],
393-
['pie', 'engine.widgetPicker.type.pie'],
394-
['donut', 'engine.widgetPicker.type.donut'],
395-
['scatter', 'engine.widgetPicker.type.scatter'],
396-
['funnel', 'engine.widgetPicker.type.funnel'],
397-
['table', 'engine.widgetPicker.type.table'],
398-
['pivot', 'engine.widgetPicker.type.pivot'],
399-
];
386+
/**
387+
* Stored `type` → the catalogue row its picker entry displays — read off the
388+
* one widget registry (objectstack#26745), which derives the offered set from
389+
* the spec, so this suite covers every type the picker offers.
390+
*/
391+
const WIDGET_TYPE_KEYS: ReadonlyArray<[string, string]> = Object.values(WIDGET_TYPE_META).map((m) => [m.id, m.labelKey]);
400392
const CATEGORIES = ['kpi', 'chart', 'data'] as const;
401393

402394
/** Mount the dashboard inspector with no widgets and open its add-widget picker. */
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#26745 — the dashboard's two widget-type pickers read ONE registry
5+
* of the renderable spec types.
6+
*
7+
* Measured on `main` before this change: the designer's Add-widget menu offered
8+
* 11 types, the widget inspector's Type select 10 (no `scatter`), and the spec's
9+
* `ChartTypeSchema` accepts 20 — every one of which the dashboard draws when
10+
* written through the API. Each picker kept its own hand-written list.
11+
*
12+
* Pinned here, each through the rendered control:
13+
* - the registry IS the spec vocabulary (`WIDGET_TYPE_IDS` = `ChartTypeSchema`);
14+
* - the Add menu offers exactly that set, and so does the Type select;
15+
* - every offered type renders through the real `DashboardRenderer` — no
16+
* "not supported yet" placeholder, no red unknown-component panel;
17+
* - every offered type has its two names in both catalogues (en + zh).
18+
*/
19+
20+
import '@testing-library/jest-dom/vitest';
21+
import * as React from 'react';
22+
import { afterEach, describe, expect, it, vi } from 'vitest';
23+
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
24+
import { ChartTypeSchema } from '@objectstack/spec/ui';
25+
import { enumOptions } from '@object-ui/test-support';
26+
// Module-scope import of the lazily loaded renderer, so the preview's
27+
// `React.lazy` factory resolves at once instead of racing the test's wait
28+
// window (AGENTS.md, flaky-test discipline).
29+
import '@object-ui/plugin-dashboard';
30+
// The console registers the chart renderer the series families draw through.
31+
import '@object-ui/plugin-charts';
32+
33+
vi.mock('@object-ui/react', async (importOriginal) => {
34+
const mod = await importOriginal<typeof import('@object-ui/react')>();
35+
return { ...mod, useAdapter: () => ({}) };
36+
});
37+
// Network-free dataset catalog for the widget inspector.
38+
vi.mock('./useDatasetCatalog', () => ({
39+
useDatasetCatalog: () => ({ datasets: [], loading: false, error: null }),
40+
useDatasetSemantics: () => ({ dimensions: [], measures: [], loading: false, error: null }),
41+
}));
42+
43+
import { t } from '../i18n';
44+
import { WIDGET_TYPE_IDS, WIDGET_TYPE_META } from './widget-types';
45+
import { DashboardPreview } from './DashboardPreview';
46+
import { DashboardDefaultInspector } from '../inspectors/DashboardDefaultInspector';
47+
import { DashboardWidgetInspector } from '../inspectors/DashboardWidgetInspector';
48+
// The `engine.inspector.*` rows live in `@object-ui/i18n`'s `studio` namespace,
49+
// whose `zh` catalogue is fetched on demand (objectstack#25922). Resident before
50+
// any case, so the zh rows below read synchronously.
51+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
52+
await loadStudioCatalogue('zh');
53+
54+
// Radix Select reads pointer capture, which jsdom does not implement.
55+
for (const m of ['hasPointerCapture', 'setPointerCapture', 'releasePointerCapture'] as const) {
56+
if (!(m in Element.prototype)) {
57+
(Element.prototype as unknown as Record<string, unknown>)[m] = m === 'hasPointerCapture' ? () => false : () => {};
58+
}
59+
}
60+
if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {};
61+
62+
afterEach(cleanup);
63+
64+
const specTypes: string[] = enumOptions(ChartTypeSchema);
65+
66+
async function flush() {
67+
await act(async () => {
68+
await new Promise((r) => setTimeout(r, 0));
69+
});
70+
}
71+
72+
/** The stored types the Add-widget menu lists, read off its rendered entries. */
73+
async function addMenuTypes(): Promise<string[]> {
74+
render(
75+
<DashboardDefaultInspector
76+
type="dashboard"
77+
name="sales"
78+
locale="en-US"
79+
draft={{ name: 'sales', label: 'Sales', widgets: [] }}
80+
onPatch={() => {}}
81+
onSelectionChange={() => {}}
82+
readOnly={false}
83+
/>,
84+
);
85+
fireEvent.click(screen.getByRole('button', { name: t('engine.inspector.add.widget', 'en-US') }));
86+
await flush();
87+
const picker = screen.getByRole('dialog');
88+
return Array.from(picker.querySelectorAll('button code')).map((c) => c.textContent ?? '');
89+
}
90+
91+
/** The option names the widget inspector's Type select lists. */
92+
async function typeSelectOptions(): Promise<string[]> {
93+
render(
94+
<DashboardWidgetInspector
95+
type="dashboard"
96+
name="sales"
97+
locale="en-US"
98+
draft={{ widgets: [{ id: 'w1', type: 'bar', title: 'Revenue' }] }}
99+
selection={{ kind: 'widget', id: 'w1' }}
100+
onPatch={() => {}}
101+
onClearSelection={() => {}}
102+
onSelectionChange={() => {}}
103+
readOnly={false}
104+
/>,
105+
);
106+
fireEvent.keyDown(document.getElementById('widget-type')!, { key: 'ArrowDown' });
107+
const listbox = await screen.findByRole('listbox');
108+
return within(listbox)
109+
.getAllByRole('option')
110+
.map((o) => o.textContent ?? '');
111+
}
112+
113+
describe('one widget-type registry, derived from the spec (objectstack#26745)', () => {
114+
it('reads a non-empty enum from the spec', () => {
115+
expect(specTypes, 'could not read ChartTypeSchema.options from the spec').not.toEqual([]);
116+
});
117+
118+
it('the registry is the spec vocabulary, in the spec order', () => {
119+
expect([...WIDGET_TYPE_IDS]).toEqual(specTypes);
120+
expect(Object.keys(WIDGET_TYPE_META).sort()).toEqual([...specTypes].sort());
121+
});
122+
123+
it('the Add-widget menu offers exactly the spec types', async () => {
124+
expect((await addMenuTypes()).sort()).toEqual([...specTypes].sort());
125+
});
126+
127+
it('the Type select offers exactly the spec types, named from the same registry', async () => {
128+
expect(await typeSelectOptions()).toEqual(
129+
specTypes.map((id) => t(WIDGET_TYPE_META[id].selectLabelKey, 'en-US')),
130+
);
131+
});
132+
133+
/** Render one widget per type through the real renderer; the drawn text once every cell is laid out. */
134+
async function renderedText(types: readonly string[]): Promise<string> {
135+
const widgets = types.map((type, i) => ({
136+
id: `w_${i}`,
137+
type,
138+
title: `Widget ${type}`,
139+
layout: { x: (i % 4) * 3, y: Math.floor(i / 4) * 2, w: 3, h: 2 },
140+
}));
141+
const { container } = render(
142+
<DashboardPreview
143+
type="dashboard"
144+
name="all_types"
145+
draft={{ name: 'all_types', label: 'All types', widgets }}
146+
editing={false}
147+
selection={null}
148+
onSelectionChange={() => {}}
149+
onPatch={() => {}}
150+
locale="en-US"
151+
/>,
152+
);
153+
await waitFor(() => {
154+
const cells = container.querySelectorAll('[style*="grid-column"]');
155+
if (cells.length !== widgets.length) throw new Error(`expected ${widgets.length} widget cells, saw ${cells.length}`);
156+
});
157+
return container.textContent ?? '';
158+
}
159+
160+
it('every offered type renders through the real DashboardRenderer — no placeholder, no red panel', async () => {
161+
const text = await renderedText(WIDGET_TYPE_IDS);
162+
expect(text).not.toMatch(/chart type is not supported yet/);
163+
expect(text).not.toMatch(/Unknown component type/);
164+
});
165+
166+
it('control: the same probe sees the placeholder a family with no renderer draws', async () => {
167+
expect(await renderedText(['heatmap'])).toMatch(/chart type is not supported yet/);
168+
});
169+
170+
it('every offered type has its Add-menu name and its Type-select name in en and zh', () => {
171+
for (const meta of Object.values(WIDGET_TYPE_META)) {
172+
for (const key of [meta.labelKey, meta.selectLabelKey]) {
173+
expect(t(key, 'en-US'), `${key}: missing en row`).not.toBe(key);
174+
// A missing zh row reads the en one back.
175+
expect(t(key, 'zh-CN'), `${key}: missing zh row`).not.toBe(t(key, 'en-US'));
176+
}
177+
// The Add menu's en name is the English the stored default title is written from.
178+
expect(t(meta.labelKey, 'en-US'), meta.id).toBe(meta.label);
179+
}
180+
});
181+
});

0 commit comments

Comments
 (0)