@@ -134,6 +134,19 @@ const readLatestWorkspaceSnapshot = async (page: import('@playwright/test').Page
134134 } )
135135}
136136
137+ const readPreviewUserStyleText = async ( page : import ( '@playwright/test' ) . Page ) => {
138+ return getPreviewFrame ( page )
139+ . locator ( 'html' )
140+ . evaluate ( ( ) => {
141+ const userStyleElement = document . getElementById ( 'knighted-preview-user-styles' )
142+ if ( ! ( userStyleElement instanceof HTMLStyleElement ) ) {
143+ return ''
144+ }
145+
146+ return userStyleElement . textContent ?? ''
147+ } )
148+ }
149+
137150test . beforeEach ( async ( { page } ) => {
138151 await resetWorkbenchStorage ( page )
139152} )
@@ -283,11 +296,7 @@ test('preview styles require explicit import from entry graph', async ({ page })
283296
284297 await expect
285298 . poll ( async ( ) => {
286- const styleContent = await getPreviewFrame ( page )
287- . locator ( 'style' )
288- . first ( )
289- . textContent ( )
290- return styleContent ?? ''
299+ return readPreviewUserStyleText ( page )
291300 } )
292301 . toContain ( 'rgb(1, 2, 3)' )
293302
@@ -302,15 +311,117 @@ test('preview styles require explicit import from entry graph', async ({ page })
302311 await expect ( page . getByRole ( 'status' , { name : 'App status' } ) ) . toHaveText ( 'Rendered' )
303312 await expect
304313 . poll ( async ( ) => {
305- const styleContent = await getPreviewFrame ( page )
306- . locator ( 'style' )
307- . first ( )
308- . textContent ( )
309- return styleContent ?? ''
314+ return readPreviewUserStyleText ( page )
310315 } )
311316 . not . toContain ( 'rgb(1, 2, 3)' )
312317} )
313318
319+ test ( 'top-level @import in user css is applied in preview iframe' , async ( { page } ) => {
320+ await waitForInitialRender ( page )
321+
322+ const importedCss = encodeURIComponent ( '.counter-button { color: rgb(11, 22, 33); }' )
323+
324+ await setWorkspaceTabSource ( page , {
325+ fileName : 'app.css' ,
326+ kind : 'styles' ,
327+ source : [
328+ `@import url("data:text/css,${ importedCss } ");` ,
329+ '.counter-button { font-weight: 700; }' ,
330+ ] . join ( '\n' ) ,
331+ } )
332+
333+ await setComponentEditorSource (
334+ page ,
335+ [
336+ "import '../styles/app.css'" ,
337+ '' ,
338+ 'const App = () => <button class="counter-button">Imported style</button>' ,
339+ '' ,
340+ ] . join ( '\n' ) ,
341+ )
342+
343+ await expect ( page . getByRole ( 'status' , { name : 'App status' } ) ) . toHaveText ( 'Rendered' )
344+
345+ await expect
346+ . poll ( async ( ) => {
347+ return getPreviewFrame ( page )
348+ . getByRole ( 'button' , { name : 'Imported style' } )
349+ . evaluate ( element => getComputedStyle ( element ) . color )
350+ } )
351+ . toBe ( 'rgb(11, 22, 33)' )
352+ } )
353+
354+ test ( 'preview iframe keeps one base and one user style node across rerenders' , async ( {
355+ page,
356+ } ) => {
357+ await waitForInitialRender ( page )
358+
359+ await setWorkspaceTabSource ( page , {
360+ fileName : 'app.css' ,
361+ kind : 'styles' ,
362+ source : [ '.counter-button { color: rgb(40, 50, 60); }' ] . join ( '\n' ) ,
363+ } )
364+
365+ await page . getByLabel ( 'Background' ) . fill ( '#123456' )
366+
367+ await setWorkspaceTabSource ( page , {
368+ fileName : 'app.css' ,
369+ kind : 'styles' ,
370+ source : [ '.counter-button { color: rgb(70, 80, 90); }' ] . join ( '\n' ) ,
371+ } )
372+
373+ await expect
374+ . poll ( async ( ) => {
375+ return getPreviewFrame ( page )
376+ . locator ( 'html' )
377+ . evaluate ( ( ) => {
378+ const baseStyleElements = document . querySelectorAll (
379+ '#knighted-preview-base-styles' ,
380+ )
381+ const userStyleElements = document . querySelectorAll (
382+ '#knighted-preview-user-styles' ,
383+ )
384+
385+ const baseStyleElement = document . getElementById ( 'knighted-preview-base-styles' )
386+ const userStyleElement = document . getElementById ( 'knighted-preview-user-styles' )
387+
388+ if (
389+ ! ( baseStyleElement instanceof HTMLStyleElement ) ||
390+ ! ( userStyleElement instanceof HTMLStyleElement )
391+ ) {
392+ return {
393+ baseCount : baseStyleElements . length ,
394+ userCount : userStyleElements . length ,
395+ ordered : false ,
396+ userText : '' ,
397+ }
398+ }
399+
400+ const styleElements = Array . from ( document . head . querySelectorAll ( 'style' ) )
401+ const baseIndex = styleElements . indexOf ( baseStyleElement )
402+ const userIndex = styleElements . indexOf ( userStyleElement )
403+
404+ return {
405+ baseCount : baseStyleElements . length ,
406+ userCount : userStyleElements . length ,
407+ ordered : baseIndex >= 0 && userIndex >= 0 && baseIndex < userIndex ,
408+ userText : userStyleElement . textContent ?? '' ,
409+ }
410+ } )
411+ } )
412+ . toMatchObject ( {
413+ baseCount : 1 ,
414+ userCount : 1 ,
415+ ordered : true ,
416+ } )
417+
418+ await expect
419+ . poll ( async ( ) => {
420+ return readPreviewUserStyleText ( page )
421+ } )
422+ . toContain ( 'rgb(70, 80, 90)' )
423+ } )
424+
314425test ( 'nested module imports can bring styles into preview graph' , async ( { page } ) => {
315426 await waitForInitialRender ( page )
316427
@@ -347,11 +458,7 @@ test('nested module imports can bring styles into preview graph', async ({ page
347458 await expect ( getPreviewFrame ( page ) . getByRole ( 'button' ) ) . toContainText ( 'Nested style' )
348459 await expect
349460 . poll ( async ( ) => {
350- const styleContent = await getPreviewFrame ( page )
351- . locator ( 'style' )
352- . first ( )
353- . textContent ( )
354- return styleContent ?? ''
461+ return readPreviewUserStyleText ( page )
355462 } )
356463 . toContain ( 'rgb(9, 8, 7)' )
357464} )
0 commit comments