@@ -29,6 +29,50 @@ const setStylesEditorSource = async (page: Page, source: string) => {
2929 await editorContent . fill ( source )
3030}
3131
32+ const getCollapseButton = ( page : Page , panelName : 'component' | 'styles' | 'preview' ) =>
33+ page . locator ( `#collapse-${ panelName } ` )
34+
35+ const getToolsButton = ( page : Page , panelName : 'component' | 'styles' ) =>
36+ page . locator ( `#tools-${ panelName } ` )
37+
38+ const ensurePanelToolsVisible = async ( page : Page , panelName : 'component' | 'styles' ) => {
39+ const button = getToolsButton ( page , panelName )
40+ const isPressed = await button . getAttribute ( 'aria-pressed' )
41+ if ( isPressed !== 'true' ) {
42+ await button . click ( )
43+ }
44+ }
45+
46+ const expectCollapseButtonState = async (
47+ page : Page ,
48+ panelName : 'component' | 'styles' | 'preview' ,
49+ {
50+ axis,
51+ direction,
52+ collapsed,
53+ disabled,
54+ } : {
55+ axis : 'vertical' | 'horizontal'
56+ direction : 'left' | 'right' | 'none'
57+ collapsed : boolean
58+ disabled ?: boolean
59+ } ,
60+ ) => {
61+ const button = getCollapseButton ( page , panelName )
62+
63+ await expect ( button ) . toHaveAttribute ( 'data-collapse-axis' , axis )
64+ await expect ( button ) . toHaveAttribute ( 'data-collapse-direction' , direction )
65+ await expect ( button ) . toHaveAttribute ( 'data-collapsed' , collapsed ? 'true' : 'false' )
66+
67+ if ( disabled !== undefined ) {
68+ if ( disabled ) {
69+ await expect ( button ) . toBeDisabled ( )
70+ } else {
71+ await expect ( button ) . toBeEnabled ( )
72+ }
73+ }
74+ }
75+
3276test ( 'renders default playground preview' , async ( { page } ) => {
3377 await waitForInitialRender ( page )
3478
@@ -54,9 +98,219 @@ test('supports layout and theme toggles', async ({ page }) => {
5498 )
5599} )
56100
101+ test ( 'side layout keeps preview panel height within editor stack height' , async ( {
102+ page,
103+ } ) => {
104+ await waitForInitialRender ( page )
105+
106+ await page . getByLabel ( 'Use side preview layout' ) . click ( )
107+ await expect ( page . locator ( '.app-grid' ) ) . toHaveClass ( / a p p - g r i d - - p r e v i e w - r i g h t / )
108+
109+ const metrics = await page . evaluate ( ( ) => {
110+ const stack = document . querySelector ( '.panels-stack--editors' )
111+ const previewPanel = document . getElementById ( 'preview-panel' )
112+ const stackHeight = stack ?. getBoundingClientRect ( ) . height ?? 0
113+ const previewHeight = previewPanel ?. getBoundingClientRect ( ) . height ?? 0
114+ const previewOverflowY = previewPanel ? getComputedStyle ( previewPanel ) . overflowY : ''
115+ return { stackHeight, previewHeight, previewOverflowY }
116+ } )
117+
118+ expect ( metrics . stackHeight ) . toBeGreaterThan ( 0 )
119+ expect ( metrics . previewHeight ) . toBeGreaterThan ( 0 )
120+ expect ( metrics . previewHeight ) . toBeLessThanOrEqual ( metrics . stackHeight + 2 )
121+ expect ( metrics . previewOverflowY ) . toBe ( 'hidden' )
122+ } )
123+
124+ test ( 'side layout config keeps preview scrolling inside preview host' , async ( {
125+ page,
126+ } ) => {
127+ await waitForInitialRender ( page )
128+
129+ await page . getByLabel ( 'Use side preview layout' ) . click ( )
130+
131+ const scrollConfig = await page . evaluate ( ( ) => {
132+ const previewPanel = document . getElementById ( 'preview-panel' )
133+ const previewHost = document . getElementById ( 'preview-host' )
134+ if ( ! previewPanel || ! previewHost ) {
135+ return null
136+ }
137+
138+ const panelStyles = getComputedStyle ( previewPanel )
139+ const styles = getComputedStyle ( previewHost )
140+ return {
141+ panelOverflowY : panelStyles . overflowY ,
142+ panelOverflowX : panelStyles . overflowX ,
143+ overflowY : styles . overflowY ,
144+ minHeight : styles . minHeight ,
145+ }
146+ } )
147+
148+ expect ( scrollConfig ) . not . toBeNull ( )
149+ expect ( scrollConfig ?. panelOverflowY ) . toBe ( 'hidden' )
150+ expect ( scrollConfig ?. panelOverflowX ) . toBe ( 'hidden' )
151+ expect ( [ 'auto' , 'scroll' ] ) . toContain ( scrollConfig ?. overflowY )
152+ expect ( scrollConfig ?. minHeight ) . toBe ( '0px' )
153+ } )
154+
155+ test ( 'expanded component and styles can shrink consistently in side layouts' , async ( {
156+ page,
157+ } ) => {
158+ await waitForInitialRender ( page )
159+
160+ for ( const layoutLabel of [ 'Use side preview layout' , 'Use left preview layout' ] ) {
161+ await page . getByLabel ( layoutLabel ) . click ( )
162+
163+ const minHeights = await page . evaluate ( ( ) => {
164+ const component = document . getElementById ( 'component-panel' )
165+ const styles = document . getElementById ( 'styles-panel' )
166+ return {
167+ component : component
168+ ? Number . parseFloat ( getComputedStyle ( component ) . minHeight )
169+ : 0 ,
170+ styles : styles ? Number . parseFloat ( getComputedStyle ( styles ) . minHeight ) : 0 ,
171+ }
172+ } )
173+
174+ expect ( minHeights . component ) . toBeGreaterThanOrEqual ( 0 )
175+ expect ( minHeights . styles ) . toBeGreaterThanOrEqual ( 0 )
176+ expect ( Math . abs ( minHeights . component - minHeights . styles ) ) . toBeLessThanOrEqual ( 1 )
177+ }
178+ } )
179+
180+ test ( 'panel collapse axis and direction adapt to active layout' , async ( { page } ) => {
181+ await waitForInitialRender ( page )
182+ await expect ( page . locator ( '.app-grid' ) ) . toHaveClass ( / a p p - g r i d / )
183+
184+ await expectCollapseButtonState ( page , 'component' , {
185+ axis : 'horizontal' ,
186+ direction : 'left' ,
187+ collapsed : false ,
188+ } )
189+ await expectCollapseButtonState ( page , 'styles' , {
190+ axis : 'horizontal' ,
191+ direction : 'right' ,
192+ collapsed : false ,
193+ } )
194+ await expectCollapseButtonState ( page , 'preview' , {
195+ axis : 'vertical' ,
196+ direction : 'none' ,
197+ collapsed : false ,
198+ } )
199+
200+ await page . getByLabel ( 'Use side preview layout' ) . click ( )
201+ await expectCollapseButtonState ( page , 'preview' , {
202+ axis : 'horizontal' ,
203+ direction : 'right' ,
204+ collapsed : false ,
205+ } )
206+ await expectCollapseButtonState ( page , 'component' , {
207+ axis : 'vertical' ,
208+ direction : 'none' ,
209+ collapsed : false ,
210+ } )
211+
212+ await page . getByLabel ( 'Use left preview layout' ) . click ( )
213+ await expectCollapseButtonState ( page , 'preview' , {
214+ axis : 'horizontal' ,
215+ direction : 'left' ,
216+ collapsed : false ,
217+ } )
218+ } )
219+
220+ test ( 'prevents collapsing all three panels at once' , async ( { page } ) => {
221+ await waitForInitialRender ( page )
222+
223+ await getCollapseButton ( page , 'component' ) . click ( )
224+ await getCollapseButton ( page , 'styles' ) . click ( )
225+
226+ await expect ( page . locator ( '#component-panel' ) ) . toHaveClass (
227+ / p a n e l - - c o l l a p s e d - h o r i z o n t a l / ,
228+ )
229+ await expect ( page . locator ( '#styles-panel' ) ) . toHaveClass ( / p a n e l - - c o l l a p s e d - h o r i z o n t a l / )
230+
231+ await expectCollapseButtonState ( page , 'preview' , {
232+ axis : 'vertical' ,
233+ direction : 'none' ,
234+ collapsed : false ,
235+ disabled : true ,
236+ } )
237+ await expect ( getCollapseButton ( page , 'preview' ) ) . toHaveAttribute (
238+ 'title' ,
239+ 'At least one panel must remain expanded.' ,
240+ )
241+
242+ await getCollapseButton ( page , 'component' ) . click ( )
243+ await expectCollapseButtonState ( page , 'preview' , {
244+ axis : 'vertical' ,
245+ direction : 'none' ,
246+ collapsed : false ,
247+ disabled : false ,
248+ } )
249+ } )
250+
251+ test ( 'does not persist panel collapse state across reload' , async ( { page } ) => {
252+ await waitForInitialRender ( page )
253+
254+ await getCollapseButton ( page , 'component' ) . click ( )
255+ await expect ( page . locator ( '#component-panel' ) ) . toHaveClass (
256+ / p a n e l - - c o l l a p s e d - h o r i z o n t a l / ,
257+ )
258+ await expectCollapseButtonState ( page , 'component' , {
259+ axis : 'horizontal' ,
260+ direction : 'left' ,
261+ collapsed : true ,
262+ } )
263+
264+ await page . reload ( )
265+ await waitForInitialRender ( page )
266+
267+ await expect ( page . locator ( '#component-panel' ) ) . not . toHaveClass (
268+ / p a n e l - - c o l l a p s e d - h o r i z o n t a l | p a n e l - - c o l l a p s e d - v e r t i c a l / ,
269+ )
270+ await expectCollapseButtonState ( page , 'component' , {
271+ axis : 'horizontal' ,
272+ direction : 'left' ,
273+ collapsed : false ,
274+ } )
275+ } )
276+
277+ test ( 'gear tools toggles default inactive and switch active/inactive per panel' , async ( {
278+ page,
279+ } ) => {
280+ await waitForInitialRender ( page )
281+
282+ const componentPanel = page . locator ( '#component-panel' )
283+ const stylesPanel = page . locator ( '#styles-panel' )
284+ const componentTools = getToolsButton ( page , 'component' )
285+ const stylesTools = getToolsButton ( page , 'styles' )
286+
287+ await expect ( componentPanel ) . toHaveClass ( / p a n e l - - t o o l s - h i d d e n / )
288+ await expect ( stylesPanel ) . toHaveClass ( / p a n e l - - t o o l s - h i d d e n / )
289+ await expect ( componentTools ) . toHaveAttribute ( 'aria-pressed' , 'false' )
290+ await expect ( stylesTools ) . toHaveAttribute ( 'aria-pressed' , 'false' )
291+
292+ await componentTools . click ( )
293+ await expect ( componentPanel ) . not . toHaveClass ( / p a n e l - - t o o l s - h i d d e n / )
294+ await expect ( componentTools ) . toHaveAttribute ( 'aria-pressed' , 'true' )
295+ await expect ( componentTools ) . toHaveAttribute ( 'title' , 'Hide component tools' )
296+
297+ await componentTools . click ( )
298+ await expect ( componentPanel ) . toHaveClass ( / p a n e l - - t o o l s - h i d d e n / )
299+ await expect ( componentTools ) . toHaveAttribute ( 'aria-pressed' , 'false' )
300+ await expect ( componentTools ) . toHaveAttribute ( 'title' , 'Show component tools' )
301+
302+ await stylesTools . click ( )
303+ await expect ( stylesPanel ) . not . toHaveClass ( / p a n e l - - t o o l s - h i d d e n / )
304+ await expect ( stylesTools ) . toHaveAttribute ( 'aria-pressed' , 'true' )
305+ await expect ( stylesTools ) . toHaveAttribute ( 'title' , 'Hide styles tools' )
306+ } )
307+
57308test ( 'renders in react mode with css modules' , async ( { page } ) => {
58309 await waitForInitialRender ( page )
59310
311+ await ensurePanelToolsVisible ( page , 'component' )
312+ await ensurePanelToolsVisible ( page , 'styles' )
313+
60314 await page . getByLabel ( 'ShadowRoot (open)' ) . uncheck ( )
61315 await page . locator ( '#render-mode' ) . selectOption ( 'react' )
62316 await page . locator ( '#style-mode' ) . selectOption ( 'module' )
@@ -117,21 +371,21 @@ test('jsx syntax errors affect status but not diagnostics toggle severity', asyn
117371test ( 'requires render button when auto render is disabled' , async ( { page } ) => {
118372 await waitForInitialRender ( page )
119373
374+ await ensurePanelToolsVisible ( page , 'component' )
375+ await ensurePanelToolsVisible ( page , 'styles' )
376+
120377 const autoRenderToggle = page . getByLabel ( 'Auto render' )
121378 const renderButton = page . getByRole ( 'button' , { name : 'Render' } )
122379 const styleMode = page . locator ( '#style-mode' )
123- const styleWarning = page . locator ( '#style-warning' )
124380
125- await expect ( styleWarning ) . toHaveText ( '' )
126381 await autoRenderToggle . uncheck ( )
127382 await expect ( renderButton ) . toBeVisible ( )
128383
129384 await styleMode . selectOption ( 'module' )
130- await expect ( styleWarning ) . toHaveText ( '' )
131385
132386 await renderButton . click ( )
133387 await expect ( page . locator ( '#status' ) ) . toHaveText ( 'Rendered' )
134- await expect ( styleWarning ) . toContainText ( 'CSS Modules are compiled in-browser' )
388+ await expect ( page . locator ( '#preview-host pre' ) ) . toHaveCount ( 0 )
135389} )
136390
137391test ( 'persists layout and theme across reload' , async ( { page } ) => {
@@ -152,30 +406,30 @@ test('persists layout and theme across reload', async ({ page }) => {
152406test ( 'renders with less style mode' , async ( { page } ) => {
153407 await waitForInitialRender ( page )
154408
409+ await ensurePanelToolsVisible ( page , 'styles' )
410+
155411 await page . getByLabel ( 'ShadowRoot (open)' ) . uncheck ( )
156412 await page . locator ( '#style-mode' ) . selectOption ( 'less' )
157413 await expect ( page . locator ( '#status' ) ) . toHaveText ( 'Rendered' )
158- await expect ( page . locator ( '#style-warning' ) ) . toContainText (
159- 'Less is compiled in-browser via @knighted/css/browser.' ,
160- )
161414 await expectPreviewHasRenderedContent ( page )
162415} )
163416
164417test ( 'renders with sass style mode' , async ( { page } ) => {
165418 await waitForInitialRender ( page )
166419
420+ await ensurePanelToolsVisible ( page , 'styles' )
421+
167422 await page . getByLabel ( 'ShadowRoot (open)' ) . uncheck ( )
168423 await page . locator ( '#style-mode' ) . selectOption ( 'sass' )
169424 await expect ( page . locator ( '#status' ) ) . toHaveText ( 'Rendered' )
170- await expect ( page . locator ( '#style-warning' ) ) . toContainText (
171- 'Sass is compiled in-browser via @knighted/css/browser.' ,
172- )
173425 await expectPreviewHasRenderedContent ( page )
174426} )
175427
176428test ( 'style compilation errors populate styles diagnostics scope' , async ( { page } ) => {
177429 await waitForInitialRender ( page )
178430
431+ await ensurePanelToolsVisible ( page , 'styles' )
432+
179433 await page . locator ( '#style-mode' ) . selectOption ( 'sass' )
180434 await setStylesEditorSource ( page , '.card { color: $missing; }' )
181435
@@ -234,6 +488,8 @@ test('clearing styles keeps diagnostics error state but resets status styling',
234488} ) => {
235489 await waitForInitialRender ( page )
236490
491+ await ensurePanelToolsVisible ( page , 'component' )
492+
237493 await setComponentEditorSource (
238494 page ,
239495 [ "const count: number = 'oops'" , 'const App = () => <button>ready</button>' ] . join (
@@ -268,6 +524,8 @@ test('clear component diagnostics removes type errors and restores rendered stat
268524} ) => {
269525 await waitForInitialRender ( page )
270526
527+ await ensurePanelToolsVisible ( page , 'component' )
528+
271529 await setComponentEditorSource (
272530 page ,
273531 [ "const count: number = 'oops'" , 'const App = () => <button>ready</button>' ] . join (
@@ -298,6 +556,8 @@ test('clear component diagnostics removes type errors and restores rendered stat
298556test ( 'clear all diagnostics removes style compile diagnostics' , async ( { page } ) => {
299557 await waitForInitialRender ( page )
300558
559+ await ensurePanelToolsVisible ( page , 'styles' )
560+
301561 await page . locator ( '#style-mode' ) . selectOption ( 'sass' )
302562 await setStylesEditorSource ( page , '.card { color: $missing; }' )
303563
0 commit comments