11/**
22 * DashboardView Design Interaction Tests
33 *
4- * Verifies the fixes for:
5- * - Non-modal DesignDrawer allowing preview widget clicks
6- * - Property panel appearing above widget grid when a widget is selected
7- * - Click-to-select in preview area with highlight and property linkage
4+ * Verifies the refactored design mode:
5+ * - Inline config panel (DashboardConfigPanel / WidgetConfigPanel) on the right
6+ * - Click-to-select in preview area syncs with config panel
7+ * - Dashboard config panel shows when no widget selected
8+ * - Widget config panel shows when a widget is selected
89 */
910
1011import { describe , it , expect , vi , beforeEach } from 'vitest' ;
1112import { render , screen , fireEvent , act } from '@testing-library/react' ;
1213import { MemoryRouter , Route , Routes } from 'react-router-dom' ;
1314import { DashboardView } from '../components/DashboardView' ;
1415
15- // Track calls passed to mocked components
16- const { editorCalls, rendererCalls } = vi . hoisted ( ( ) => ( {
17- editorCalls : {
18- selectedWidgetId : null as string | null ,
19- onWidgetSelect : null as ( ( id : string | null ) => void ) | null ,
20- lastSchema : null as unknown ,
21- } ,
16+ // Track props passed to mocked components
17+ const { rendererCalls, dashboardConfigCalls, widgetConfigCalls } = vi . hoisted ( ( ) => ( {
2218 rendererCalls : {
2319 designMode : false ,
2420 selectedWidgetId : null as string | null ,
2521 onWidgetClick : null as ( ( id : string | null ) => void ) | null ,
2622 } ,
23+ dashboardConfigCalls : {
24+ open : false ,
25+ onClose : null as ( ( ) => void ) | null ,
26+ config : null as Record < string , any > | null ,
27+ } ,
28+ widgetConfigCalls : {
29+ open : false ,
30+ onClose : null as ( ( ) => void ) | null ,
31+ config : null as Record < string , any > | null ,
32+ onSave : null as ( ( config : Record < string , any > ) => void ) | null ,
33+ onFieldChange : null as ( ( field : string , value : any ) => void ) | null ,
34+ } ,
2735} ) ) ;
2836
2937// Mock MetadataProvider with a dashboard
@@ -85,32 +93,30 @@ vi.mock('@object-ui/plugin-dashboard', () => ({
8593 </ div >
8694 ) ;
8795 } ,
88- } ) ) ;
89-
90- // Mock DashboardEditor to capture selection and show property panel
91- vi . mock ( '@object-ui/plugin-designer' , ( ) => ( {
92- DashboardEditor : ( props : any ) => {
93- editorCalls . selectedWidgetId = props . selectedWidgetId ;
94- editorCalls . onWidgetSelect = props . onWidgetSelect ;
95- editorCalls . lastSchema = props . schema ;
96- const widget = props . schema ?. widgets ?. find ( ( w : any ) => w . id === props . selectedWidgetId ) ;
96+ DashboardConfigPanel : ( props : any ) => {
97+ dashboardConfigCalls . open = props . open ;
98+ dashboardConfigCalls . onClose = props . onClose ;
99+ dashboardConfigCalls . config = props . config ;
100+ if ( ! props . open ) return null ;
97101 return (
98- < div data-testid = "dashboard-editor" >
99- < span data-testid = "editor-selected" > { props . selectedWidgetId ?? 'none' } </ span >
100- { widget && (
101- < div data-testid = "editor-property-panel" >
102- < span data-testid = "editor-widget-title" > { widget . title } </ span >
103- </ div >
104- ) }
105- { props . schema ?. widgets ?. map ( ( w : any ) => (
106- < button
107- key = { w . id }
108- data-testid = { `editor-widget-${ w . id } ` }
109- onClick = { ( ) => props . onWidgetSelect ?.( w . id ) }
110- >
111- { w . title }
112- </ button >
113- ) ) }
102+ < div data-testid = "dashboard-config-panel" >
103+ < span data-testid = "dashboard-config-columns" > { props . config ?. columns ?? 'none' } </ span >
104+ < button data-testid = "dashboard-config-close" onClick = { props . onClose } > Close</ button >
105+ </ div >
106+ ) ;
107+ } ,
108+ WidgetConfigPanel : ( props : any ) => {
109+ widgetConfigCalls . open = props . open ;
110+ widgetConfigCalls . onClose = props . onClose ;
111+ widgetConfigCalls . config = props . config ;
112+ widgetConfigCalls . onSave = props . onSave ;
113+ widgetConfigCalls . onFieldChange = props . onFieldChange ;
114+ if ( ! props . open ) return null ;
115+ return (
116+ < div data-testid = "widget-config-panel" >
117+ < span data-testid = "widget-config-title" > { props . config ?. title ?? 'none' } </ span >
118+ { props . headerExtra && < div data-testid = "widget-config-header-extra" > { props . headerExtra } </ div > }
119+ < button data-testid = "widget-config-close" onClick = { props . onClose } > Close</ button >
114120 </ div >
115121 ) ;
116122 } ,
@@ -124,23 +130,19 @@ vi.mock('sonner', () => ({
124130 } ,
125131} ) ) ;
126132
127- // Mock Radix Dialog portal to render inline for testing
128- vi . mock ( '@radix-ui/react-dialog' , async ( ) => {
129- const actual = await vi . importActual ( '@radix-ui/react-dialog' ) ;
130- return {
131- ...( actual as Record < string , unknown > ) ,
132- Portal : ( { children } : { children : React . ReactNode } ) => < > { children } </ > ,
133- } ;
134- } ) ;
135-
136133beforeEach ( ( ) => {
137134 mockUpdate . mockClear ( ) ;
138- editorCalls . selectedWidgetId = null ;
139- editorCalls . onWidgetSelect = null ;
140- editorCalls . lastSchema = null ;
141135 rendererCalls . designMode = false ;
142136 rendererCalls . selectedWidgetId = null ;
143137 rendererCalls . onWidgetClick = null ;
138+ dashboardConfigCalls . open = false ;
139+ dashboardConfigCalls . onClose = null ;
140+ dashboardConfigCalls . config = null ;
141+ widgetConfigCalls . open = false ;
142+ widgetConfigCalls . onClose = null ;
143+ widgetConfigCalls . config = null ;
144+ widgetConfigCalls . onSave = null ;
145+ widgetConfigCalls . onFieldChange = null ;
144146} ) ;
145147
146148const renderDashboardView = async ( ) => {
@@ -151,143 +153,159 @@ const renderDashboardView = async () => {
151153 </ Routes >
152154 </ MemoryRouter > ,
153155 ) ;
154- // Wait for the queueMicrotask loading state to resolve
155156 await act ( async ( ) => {
156157 await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
157158 } ) ;
158159 return result ;
159160} ;
160161
161- const openDrawer = async ( ) => {
162+ const openConfigPanel = async ( ) => {
162163 await act ( async ( ) => {
163164 fireEvent . click ( screen . getByTestId ( 'dashboard-edit-button' ) ) ;
164165 } ) ;
165- // Wait for lazy-loaded DashboardEditor to resolve
166- await act ( async ( ) => {
167- await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
168- } ) ;
169166} ;
170167
171- describe ( 'Dashboard Design Mode — Non-modal Drawer Interaction ' , ( ) => {
172- it ( 'should open drawer with non-modal behavior (no blocking overlay )' , async ( ) => {
168+ describe ( 'Dashboard Design Mode — Inline Config Panel ' , ( ) => {
169+ it ( 'should show dashboard config panel when edit button is clicked (no widget selected )' , async ( ) => {
173170 await renderDashboardView ( ) ;
171+ await openConfigPanel ( ) ;
174172
175- await openDrawer ( ) ;
176-
177- // Drawer should be open
178- expect ( screen . getByTestId ( 'design-drawer' ) ) . toBeInTheDocument ( ) ;
179- // Design mode should be enabled
180173 expect ( screen . getByTestId ( 'renderer-design-mode' ) ) . toHaveTextContent ( 'true' ) ;
181- // Both renderer and editor should be visible simultaneously
182- expect ( screen . getByTestId ( 'dashboard-renderer' ) ) . toBeInTheDocument ( ) ;
183- expect ( screen . getByTestId ( 'dashboard-editor' ) ) . toBeInTheDocument ( ) ;
174+ expect ( screen . getByTestId ( 'dashboard-config-panel' ) ) . toBeInTheDocument ( ) ;
175+ expect ( screen . queryByTestId ( 'widget-config-panel' ) ) . not . toBeInTheDocument ( ) ;
184176 } ) ;
185177
186- it ( 'should allow clicking preview widgets while drawer is open ' , async ( ) => {
178+ it ( 'should show widget config panel when a widget is clicked in preview ' , async ( ) => {
187179 await renderDashboardView ( ) ;
188- await openDrawer ( ) ;
180+ await openConfigPanel ( ) ;
189181
190- // Click widget in preview area — this verifies the drawer doesn't block clicks
191182 await act ( async ( ) => {
192183 fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
193184 } ) ;
194185
195- // Widget should be selected in both renderer and editor
196- expect ( screen . getByTestId ( 'renderer-selected ' ) ) . toHaveTextContent ( 'w1 ' ) ;
197- expect ( screen . getByTestId ( 'editor-selected ') ) . toHaveTextContent ( 'w1' ) ;
186+ expect ( screen . getByTestId ( 'widget-config-panel' ) ) . toBeInTheDocument ( ) ;
187+ expect ( screen . getByTestId ( 'widget-config-title ' ) ) . toHaveTextContent ( 'Total Revenue ' ) ;
188+ expect ( screen . queryByTestId ( 'dashboard-config-panel ') ) . not . toBeInTheDocument ( ) ;
198189 } ) ;
199190
200- it ( 'should show property panel in editor when preview widget is clicked ' , async ( ) => {
191+ it ( 'should switch back to dashboard config when widget is deselected ' , async ( ) => {
201192 await renderDashboardView ( ) ;
202- await openDrawer ( ) ;
193+ await openConfigPanel ( ) ;
203194
204- // Click widget in preview
195+ // Select a widget
205196 await act ( async ( ) => {
206197 fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
207198 } ) ;
199+ expect ( screen . getByTestId ( 'widget-config-panel' ) ) . toBeInTheDocument ( ) ;
208200
209- // Property panel should show the selected widget's properties
210- expect ( screen . getByTestId ( 'editor-property-panel' ) ) . toBeInTheDocument ( ) ;
211- expect ( screen . getByTestId ( 'editor-widget-title' ) ) . toHaveTextContent ( 'Total Revenue' ) ;
201+ // Deselect by clicking null
202+ await act ( async ( ) => {
203+ rendererCalls . onWidgetClick ?.( null ) ;
204+ } ) ;
205+
206+ expect ( screen . getByTestId ( 'dashboard-config-panel' ) ) . toBeInTheDocument ( ) ;
207+ expect ( screen . queryByTestId ( 'widget-config-panel' ) ) . not . toBeInTheDocument ( ) ;
212208 } ) ;
213209
214- it ( 'should show property panel when clicking editor widget list item ' , async ( ) => {
210+ it ( 'should switch between different widgets ' , async ( ) => {
215211 await renderDashboardView ( ) ;
216- await openDrawer ( ) ;
212+ await openConfigPanel ( ) ;
213+
214+ await act ( async ( ) => {
215+ fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
216+ } ) ;
217+ expect ( screen . getByTestId ( 'widget-config-title' ) ) . toHaveTextContent ( 'Total Revenue' ) ;
217218
218- // Click widget in editor list
219219 await act ( async ( ) => {
220- fireEvent . click ( screen . getByTestId ( 'editor -widget-w2 ' ) ) ;
220+ fireEvent . click ( screen . getByTestId ( 'renderer -widget-w3 ' ) ) ;
221221 } ) ;
222+ expect ( screen . getByTestId ( 'widget-config-title' ) ) . toHaveTextContent ( 'Pipeline by Stage' ) ;
223+ } ) ;
224+
225+ it ( 'should show add-widget toolbar in edit mode' , async ( ) => {
226+ await renderDashboardView ( ) ;
227+ expect ( screen . queryByTestId ( 'dashboard-widget-toolbar' ) ) . not . toBeInTheDocument ( ) ;
228+
229+ await openConfigPanel ( ) ;
230+ expect ( screen . getByTestId ( 'dashboard-widget-toolbar' ) ) . toBeInTheDocument ( ) ;
231+ expect ( screen . getByTestId ( 'dashboard-add-metric' ) ) . toBeInTheDocument ( ) ;
232+ } ) ;
222233
223- // Property panel should show for the clicked widget
224- expect ( screen . getByTestId ( 'editor-property-panel' ) ) . toBeInTheDocument ( ) ;
225- expect ( screen . getByTestId ( 'editor-widget-title' ) ) . toHaveTextContent ( 'Revenue Trends' ) ;
226- // Preview should also reflect the selection
227- expect ( screen . getByTestId ( 'renderer-selected' ) ) . toHaveTextContent ( 'w2' ) ;
234+ it ( 'should not show DesignDrawer (no Sheet overlay)' , async ( ) => {
235+ await renderDashboardView ( ) ;
236+ await openConfigPanel ( ) ;
237+ expect ( screen . queryByTestId ( 'design-drawer' ) ) . not . toBeInTheDocument ( ) ;
228238 } ) ;
229239
230- it ( 'should switch selection between different widgets ' , async ( ) => {
240+ it ( 'should close config panel and clear selection on close ' , async ( ) => {
231241 await renderDashboardView ( ) ;
232- await openDrawer ( ) ;
242+ await openConfigPanel ( ) ;
233243
234- // Select w1
244+ // Select a widget
235245 await act ( async ( ) => {
236246 fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
237247 } ) ;
238- expect ( screen . getByTestId ( 'editor-widget-title' ) ) . toHaveTextContent ( 'Total Revenue' ) ;
239248
240- // Switch to w3
249+ // Close via widget config panel close button
241250 await act ( async ( ) => {
242- fireEvent . click ( screen . getByTestId ( 'renderer- widget-w3 ' ) ) ;
251+ fireEvent . click ( screen . getByTestId ( 'widget-config-close ' ) ) ;
243252 } ) ;
244- expect ( screen . getByTestId ( 'editor-widget-title' ) ) . toHaveTextContent ( 'Pipeline by Stage' ) ;
245- expect ( screen . getByTestId ( 'renderer-selected' ) ) . toHaveTextContent ( 'w3' ) ;
253+
254+ expect ( screen . getByTestId ( 'renderer-design-mode' ) ) . toHaveTextContent ( 'false' ) ;
255+ expect ( screen . getByTestId ( 'renderer-selected' ) ) . toHaveTextContent ( 'none' ) ;
246256 } ) ;
247257
248- it ( 'should deselect when clicking empty space in preview ' , async ( ) => {
258+ it ( 'should show delete button in widget config panel header ' , async ( ) => {
249259 await renderDashboardView ( ) ;
250- await openDrawer ( ) ;
260+ await openConfigPanel ( ) ;
251261
252- // Select a widget
253262 await act ( async ( ) => {
254263 fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
255264 } ) ;
256- expect ( screen . getByTestId ( 'editor-property-panel' ) ) . toBeInTheDocument ( ) ;
257265
258- // Deselect by calling onWidgetClick(null) (simulates background click)
266+ expect ( screen . getByTestId ( 'widget-config-header-extra' ) ) . toBeInTheDocument ( ) ;
267+ expect ( screen . getByTestId ( 'widget-delete-button' ) ) . toBeInTheDocument ( ) ;
268+ } ) ;
269+
270+ it ( 'should remove widget and switch to dashboard config when delete is clicked' , async ( ) => {
271+ await renderDashboardView ( ) ;
272+ await openConfigPanel ( ) ;
273+
259274 await act ( async ( ) => {
260- rendererCalls . onWidgetClick ?. ( null ) ;
275+ fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
261276 } ) ;
277+ expect ( screen . getByTestId ( 'widget-config-panel' ) ) . toBeInTheDocument ( ) ;
262278
263- // Property panel should be hidden
264- expect ( screen . queryByTestId ( 'editor-property-panel' ) ) . not . toBeInTheDocument ( ) ;
265- expect ( screen . getByTestId ( 'renderer-selected' ) ) . toHaveTextContent ( 'none' ) ;
279+ // Click the delete button
280+ await act ( async ( ) => {
281+ fireEvent . click ( screen . getByTestId ( 'widget-delete-button' ) ) ;
282+ } ) ;
283+
284+ // Should switch back to dashboard config (widget deselected)
285+ expect ( screen . getByTestId ( 'dashboard-config-panel' ) ) . toBeInTheDocument ( ) ;
286+ expect ( screen . queryByTestId ( 'widget-config-panel' ) ) . not . toBeInTheDocument ( ) ;
287+ // Deleted widget should be removed from the preview
288+ expect ( screen . queryByTestId ( 'renderer-widget-w1' ) ) . not . toBeInTheDocument ( ) ;
289+ // Backend should be called to persist the deletion
290+ expect ( mockUpdate ) . toHaveBeenCalled ( ) ;
266291 } ) ;
267292
268- it ( 'should clear selection when drawer is closed ' , async ( ) => {
293+ it ( 'should preserve live preview when field changes via onFieldChange ' , async ( ) => {
269294 await renderDashboardView ( ) ;
295+ await openConfigPanel ( ) ;
270296
271- // Open drawer and select
272- await openDrawer ( ) ;
273297 await act ( async ( ) => {
274298 fireEvent . click ( screen . getByTestId ( 'renderer-widget-w1' ) ) ;
275299 } ) ;
276- expect ( screen . getByTestId ( 'renderer-selected' ) ) . toHaveTextContent ( 'w1' ) ;
277300
278- // Close the drawer
279- const closeButtons = screen . getAllByRole ( 'button' , { name : / c l o s e / i } ) ;
280- const sheetCloseBtn = closeButtons . find ( ( btn ) =>
281- btn . closest ( '[data-testid="design-drawer"]' ) ,
282- ) ;
283- if ( sheetCloseBtn ) {
284- await act ( async ( ) => {
285- fireEvent . click ( sheetCloseBtn ) ;
286- } ) ;
287- }
301+ // Simulate a live field change via onFieldChange
302+ await act ( async ( ) => {
303+ widgetConfigCalls . onFieldChange ?.( 'title' , 'Live Title' ) ;
304+ } ) ;
288305
289- // Selection should be cleared
290- expect ( screen . getByTestId ( 'renderer-design-mode' ) ) . toHaveTextContent ( 'false' ) ;
291- expect ( screen . getByTestId ( 'renderer-selected' ) ) . toHaveTextContent ( 'none' ) ;
306+ // Preview should update live
307+ expect ( screen . getByTestId ( 'renderer-widget-w1' ) ) . toHaveTextContent ( 'Live Title' ) ;
308+ // Config panel should still show the widget (not reset or disappear)
309+ expect ( screen . getByTestId ( 'widget-config-panel' ) ) . toBeInTheDocument ( ) ;
292310 } ) ;
293311} ) ;
0 commit comments