Skip to content

Commit f505397

Browse files
os-zhuangclaude
andauthored
test(dogfood): designer-shaped (layout-less) dashboard saves + publishes (#2260)
Golden regression for the P1 found dogfooding the Studio dashboard designer: the designer's addWidget creates widgets WITHOUT a `layout`, but the spec required it, so every designer-authored dashboard returned 422 on draft save and Publish stayed disabled — yet it passed every static gate because code-authored example dashboards always specify a layout. Boots the real showcase stack and drives the actual create -> save -> publish metadata path with a layout-less, designer-shaped dashboard: - PUT /meta/dashboard/:name?mode=draft -> 200 (was 422) - POST /meta/dashboard/:name/publish -> 200 - GET /meta/dashboard/:name -> widgets persisted, layout-less Closes the "renders at design time != persists" coverage gap that hid the bug. Pairs with the spec fix (#2247) that made DashboardWidget.layout optional. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8801c02 commit f505397

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// GOLDEN REGRESSION — "a dashboard authored in the Studio designer round-trips
4+
// through draft save -> publish", exercised end-to-end through the real HTTP +
5+
// metadata stack.
6+
//
7+
// The Studio dashboard designer's `addWidget` creates widgets with NO `layout`
8+
// (objectui's DashboardGridLayout auto-flows them). Before the fix that made
9+
// `DashboardWidget.layout` optional, the draft save returned 422
10+
// ("widgets: Invalid type: expected object, received undefined"), so EVERY
11+
// designer-authored dashboard was unsavable and Publish stayed disabled — even
12+
// though the widget rendered correctly in the canvas.
13+
//
14+
// This bug passed every static gate: code-authored example dashboards ALWAYS
15+
// specify a layout, so nothing exercised the layout-less shape. Only driving the
16+
// real create -> save -> publish path with a designer-shaped (layout-less)
17+
// dashboard surfaces it. This is the test that would have caught it before merge.
18+
19+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
20+
import showcaseStack from '@objectstack/example-showcase';
21+
import { bootStack, type VerifyStack } from '@objectstack/verify';
22+
23+
// Designer-shaped: each widget binds a dataset + dimensions/values but carries
24+
// NO `layout` — exactly what the Studio designer writes. `columns` is set, as
25+
// the designer's Layout panel does, which is what made the layout-less widgets
26+
// render in a positioned grid (and previously fail validation on save).
27+
const DASH = 'dogfood_designer_roundtrip';
28+
const designerDashboard = {
29+
name: DASH,
30+
label: 'Dogfood Designer Roundtrip',
31+
description: 'Layout-less widgets, exactly as authored in the Studio designer.',
32+
columns: 12,
33+
widgets: [
34+
{ id: 'kpi_tasks', type: 'metric', title: 'Total Tasks', dataset: 'showcase_task_metrics', values: ['task_count'] },
35+
{ id: 'by_status', type: 'bar', title: 'Tasks by Status', dataset: 'showcase_task_metrics', dimensions: ['status'], values: ['task_count'] },
36+
{ id: 'priority_split', type: 'donut', title: 'Priority Split', dataset: 'showcase_task_metrics', dimensions: ['priority'], values: ['task_count'] },
37+
],
38+
};
39+
40+
describe('dogfood: a Studio-designer-shaped (layout-less) dashboard saves + publishes', () => {
41+
let stack: VerifyStack;
42+
let token: string;
43+
44+
beforeAll(async () => {
45+
stack = await bootStack(showcaseStack);
46+
token = await stack.signIn();
47+
}, 90_000);
48+
49+
afterAll(async () => {
50+
await stack?.stop();
51+
});
52+
53+
it('saves the layout-less draft (was 422 before DashboardWidget.layout became optional)', async () => {
54+
const res = await stack.apiAs(token, 'PUT', `/meta/dashboard/${DASH}?mode=draft`, designerDashboard);
55+
// The regression: a widget with no `layout` made this 422
56+
// ("widgets: Invalid type: expected object, received undefined").
57+
expect(res.status).toBe(200);
58+
});
59+
60+
it('publishes the saved draft to live (Publish was disabled while the draft could not save)', async () => {
61+
const res = await stack.apiAs(token, 'POST', `/meta/dashboard/${DASH}/publish`, {});
62+
expect(res.status).toBe(200);
63+
});
64+
65+
it('reads the published dashboard back with its layout-less widgets intact', async () => {
66+
const res = await stack.apiAs(token, 'GET', `/meta/dashboard/${DASH}`);
67+
expect(res.status).toBe(200);
68+
const body = (await res.json()) as
69+
| { widgets?: Array<{ id: string; layout?: unknown }> }
70+
| { item?: { widgets?: Array<{ id: string; layout?: unknown }> } };
71+
const widgets =
72+
(body as { widgets?: Array<{ id: string; layout?: unknown }> }).widgets ??
73+
(body as { item?: { widgets?: Array<{ id: string; layout?: unknown }> } }).item?.widgets ??
74+
[];
75+
expect(widgets).toHaveLength(3);
76+
// The exact shape that broke save: widgets persisted WITHOUT a layout.
77+
expect(widgets.every((w) => w.layout === undefined)).toBe(true);
78+
});
79+
});

0 commit comments

Comments
 (0)