@@ -29,6 +29,62 @@ const setStylesEditorSource = async (page: Page, source: string) => {
2929 await editorContent . fill ( source )
3030}
3131
32+ const getLongListComponentSource = ( count = 1200 ) =>
33+ [
34+ 'const App = () => {' ,
35+ ` const items = Array.from({ length: ${ count } }, (_, index) => ({` ,
36+ ' id: index + 1,' ,
37+ ' title: `List item ${index + 1}`' ,
38+ ' }))' ,
39+ '' ,
40+ ' return (' ,
41+ " <section style={{ padding: '12px', fontFamily: 'system-ui, sans-serif' }}>" ,
42+ " <h2 style={{ margin: '0 0 12px', fontSize: '1rem' }}>Long Preview List</h2>" ,
43+ " <ul style={{ margin: 0, paddingLeft: '1.25rem' }}>" ,
44+ ' {items.map(item => (' ,
45+ " <li key={item.id} style={{ marginBottom: '8px' }}>" ,
46+ ' <strong>{item.title}</strong>' ,
47+ ' </li>' ,
48+ ' ))}' ,
49+ ' </ul>' ,
50+ ' </section>' ,
51+ ' )' ,
52+ '}' ,
53+ ] . join ( '\n' )
54+
55+ const getCollapseButton = ( page : Page , panelName : 'component' | 'styles' | 'preview' ) =>
56+ page . locator ( `#collapse-${ panelName } ` )
57+
58+ const expectCollapseButtonState = async (
59+ page : Page ,
60+ panelName : 'component' | 'styles' | 'preview' ,
61+ {
62+ axis,
63+ direction,
64+ collapsed,
65+ disabled,
66+ } : {
67+ axis : 'vertical' | 'horizontal'
68+ direction : 'left' | 'right' | 'none'
69+ collapsed : boolean
70+ disabled ?: boolean
71+ } ,
72+ ) => {
73+ const button = getCollapseButton ( page , panelName )
74+
75+ await expect ( button ) . toHaveAttribute ( 'data-collapse-axis' , axis )
76+ await expect ( button ) . toHaveAttribute ( 'data-collapse-direction' , direction )
77+ await expect ( button ) . toHaveAttribute ( 'data-collapsed' , collapsed ? 'true' : 'false' )
78+
79+ if ( disabled !== undefined ) {
80+ if ( disabled ) {
81+ await expect ( button ) . toBeDisabled ( )
82+ } else {
83+ await expect ( button ) . toBeEnabled ( )
84+ }
85+ }
86+ }
87+
3288test ( 'renders default playground preview' , async ( { page } ) => {
3389 await waitForInitialRender ( page )
3490
@@ -54,6 +110,182 @@ test('supports layout and theme toggles', async ({ page }) => {
54110 )
55111} )
56112
113+ test ( 'side layout keeps preview panel height within editor stack height' , async ( {
114+ page,
115+ } ) => {
116+ await waitForInitialRender ( page )
117+
118+ await page . getByLabel ( 'Use side preview layout' ) . click ( )
119+ 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 / )
120+
121+ const metrics = await page . evaluate ( ( ) => {
122+ const stack = document . querySelector ( '.panels-stack--editors' )
123+ const previewPanel = document . getElementById ( 'preview-panel' )
124+ const stackHeight = stack ?. getBoundingClientRect ( ) . height ?? 0
125+ const previewHeight = previewPanel ?. getBoundingClientRect ( ) . height ?? 0
126+ const previewOverflowY = previewPanel ? getComputedStyle ( previewPanel ) . overflowY : ''
127+ return { stackHeight, previewHeight, previewOverflowY }
128+ } )
129+
130+ expect ( metrics . stackHeight ) . toBeGreaterThan ( 0 )
131+ expect ( metrics . previewHeight ) . toBeGreaterThan ( 0 )
132+ expect ( metrics . previewHeight ) . toBeLessThanOrEqual ( metrics . stackHeight + 2 )
133+ expect ( metrics . previewOverflowY ) . toBe ( 'hidden' )
134+ } )
135+
136+ test ( 'side layout config keeps preview scrolling inside preview host' , async ( {
137+ page,
138+ } ) => {
139+ await waitForInitialRender ( page )
140+
141+ await page . getByLabel ( 'Use side preview layout' ) . click ( )
142+
143+ const scrollConfig = await page . evaluate ( ( ) => {
144+ const previewPanel = document . getElementById ( 'preview-panel' )
145+ const previewHost = document . getElementById ( 'preview-host' )
146+ if ( ! previewPanel || ! previewHost ) {
147+ return null
148+ }
149+
150+ const panelStyles = getComputedStyle ( previewPanel )
151+ const styles = getComputedStyle ( previewHost )
152+ return {
153+ panelOverflowY : panelStyles . overflowY ,
154+ panelOverflowX : panelStyles . overflowX ,
155+ overflowY : styles . overflowY ,
156+ minHeight : styles . minHeight ,
157+ }
158+ } )
159+
160+ expect ( scrollConfig ) . not . toBeNull ( )
161+ expect ( scrollConfig ?. panelOverflowY ) . toBe ( 'hidden' )
162+ expect ( scrollConfig ?. panelOverflowX ) . toBe ( 'hidden' )
163+ expect ( [ 'auto' , 'scroll' ] ) . toContain ( scrollConfig ?. overflowY )
164+ expect ( scrollConfig ?. minHeight ) . toBe ( '0px' )
165+ } )
166+
167+ test ( 'expanded component and styles min-height stay consistent in side layouts' , async ( {
168+ page,
169+ } ) => {
170+ await waitForInitialRender ( page )
171+
172+ for ( const layoutLabel of [ 'Use side preview layout' , 'Use left preview layout' ] ) {
173+ await page . getByLabel ( layoutLabel ) . click ( )
174+
175+ const minHeights = await page . evaluate ( ( ) => {
176+ const component = document . getElementById ( 'component-panel' )
177+ const styles = document . getElementById ( 'styles-panel' )
178+ return {
179+ component : component
180+ ? Number . parseFloat ( getComputedStyle ( component ) . minHeight )
181+ : 0 ,
182+ styles : styles ? Number . parseFloat ( getComputedStyle ( styles ) . minHeight ) : 0 ,
183+ }
184+ } )
185+
186+ expect ( minHeights . component ) . toBeGreaterThan ( 0 )
187+ expect ( minHeights . styles ) . toBeGreaterThan ( 0 )
188+ expect ( Math . abs ( minHeights . component - minHeights . styles ) ) . toBeLessThanOrEqual ( 1 )
189+ }
190+ } )
191+
192+ test ( 'panel collapse axis and direction adapt to active layout' , async ( { page } ) => {
193+ await waitForInitialRender ( page )
194+ await expect ( page . locator ( '.app-grid' ) ) . toHaveClass ( / a p p - g r i d / )
195+
196+ await expectCollapseButtonState ( page , 'component' , {
197+ axis : 'horizontal' ,
198+ direction : 'left' ,
199+ collapsed : false ,
200+ } )
201+ await expectCollapseButtonState ( page , 'styles' , {
202+ axis : 'horizontal' ,
203+ direction : 'right' ,
204+ collapsed : false ,
205+ } )
206+ await expectCollapseButtonState ( page , 'preview' , {
207+ axis : 'vertical' ,
208+ direction : 'none' ,
209+ collapsed : false ,
210+ } )
211+
212+ await page . getByLabel ( 'Use side preview layout' ) . click ( )
213+ await expectCollapseButtonState ( page , 'preview' , {
214+ axis : 'horizontal' ,
215+ direction : 'right' ,
216+ collapsed : false ,
217+ } )
218+ await expectCollapseButtonState ( page , 'component' , {
219+ axis : 'vertical' ,
220+ direction : 'none' ,
221+ collapsed : false ,
222+ } )
223+
224+ await page . getByLabel ( 'Use left preview layout' ) . click ( )
225+ await expectCollapseButtonState ( page , 'preview' , {
226+ axis : 'horizontal' ,
227+ direction : 'left' ,
228+ collapsed : false ,
229+ } )
230+ } )
231+
232+ test ( 'prevents collapsing all three panels at once' , async ( { page } ) => {
233+ await waitForInitialRender ( page )
234+
235+ await getCollapseButton ( page , 'component' ) . click ( )
236+ await getCollapseButton ( page , 'styles' ) . click ( )
237+
238+ await expect ( page . locator ( '#component-panel' ) ) . toHaveClass (
239+ / p a n e l - - c o l l a p s e d - h o r i z o n t a l / ,
240+ )
241+ 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 / )
242+
243+ await expectCollapseButtonState ( page , 'preview' , {
244+ axis : 'vertical' ,
245+ direction : 'none' ,
246+ collapsed : false ,
247+ disabled : true ,
248+ } )
249+ await expect ( getCollapseButton ( page , 'preview' ) ) . toHaveAttribute (
250+ 'title' ,
251+ 'At least one panel must remain expanded.' ,
252+ )
253+
254+ await getCollapseButton ( page , 'component' ) . click ( )
255+ await expectCollapseButtonState ( page , 'preview' , {
256+ axis : 'vertical' ,
257+ direction : 'none' ,
258+ collapsed : false ,
259+ disabled : false ,
260+ } )
261+ } )
262+
263+ test ( 'does not persist panel collapse state across reload' , async ( { page } ) => {
264+ await waitForInitialRender ( page )
265+
266+ await getCollapseButton ( page , 'component' ) . click ( )
267+ await expect ( page . locator ( '#component-panel' ) ) . 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 / ,
269+ )
270+ await expectCollapseButtonState ( page , 'component' , {
271+ axis : 'horizontal' ,
272+ direction : 'left' ,
273+ collapsed : true ,
274+ } )
275+
276+ await page . reload ( )
277+ await waitForInitialRender ( page )
278+
279+ await expect ( page . locator ( '#component-panel' ) ) . not . toHaveClass (
280+ / 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 / ,
281+ )
282+ await expectCollapseButtonState ( page , 'component' , {
283+ axis : 'horizontal' ,
284+ direction : 'left' ,
285+ collapsed : false ,
286+ } )
287+ } )
288+
57289test ( 'renders in react mode with css modules' , async ( { page } ) => {
58290 await waitForInitialRender ( page )
59291
0 commit comments